diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 88458a696..0c1301c1f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,5 +1,5 @@ name: Bug report for the Element iOS app -description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-ios/issues) first, in case it has already been reported. +description: Report any issues that you have found with the Element app. Please check open issues first, in case it has already been reported. labels: [T-Defect] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml deleted file mode 100644 index b30282798..000000000 --- a/.github/ISSUE_TEMPLATE/config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: Element iOS Community Support - url: "https://matrix.to/#/#element-ios:matrix.org" - about: General Element iOS support questions can be asked here. - - name: Matrix Security Policy - url: https://www.matrix.org/security-disclosure-policy/ - about: Learn more about our security disclosure policy. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..8cbef5ecc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Enhancement or feature request + url: https://github.com/vector-im/element-meta/discussions/categories/ideas + about: Do you have a suggestion or feature request? + - name: Element iOS Community Support + url: https://matrix.to/#/#element-ios:matrix.org + about: General Element iOS support questions can be asked in the app Matrix room diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml deleted file mode 100644 index e776173fa..000000000 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Enhancement request -description: Do you have a suggestion or feature request? -labels: [T-Enhancement] -body: - - type: markdown - attributes: - value: | - Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/vector-im/element-meta/discussions/new?category=ideas) - - type: textarea - id: usecase - attributes: - label: Your use case - description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. - placeholder: Tell us what you would like to do! - value: | - #### What would you like to do? - - #### Why would you like to do it? - - #### How would you like to achieve it? - validations: - required: true - - type: textarea - id: alternative - attributes: - label: Have you considered any alternatives? - placeholder: A clear and concise description of any alternative solutions or features you've considered. - validations: - required: false - - type: textarea - id: additional-context - attributes: - label: Additional context - placeholder: Is there anything else you'd like to add? - validations: - required: false diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index f8d03f08b..4f6a1b3e3 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -4,33 +4,17 @@ on: # Triggers the workflow on any pull request pull_request: - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: + types: [ labeled, synchronize, opened, reopened ] env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} jobs: - check-secret: - runs-on: macos-12 - outputs: - out-key: ${{ steps.out-key.outputs.defined }} - steps: - - id: out-key - env: - P12_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} - P12_PASSWORD_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} - if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}" - run: echo "::set-output name=defined::true" build: - # Run job if secrets are available (not available for forks). - needs: [check-secret] - if: | - needs.check-secret.outputs.out-key == 'true' && - (github.event_name == 'push' || - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build'))) + # Only run for PRs that contain the trigger label. The action will fail for forks due to + # missing secrets, but there's no need to handle this as it won't run automatically. + if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build') name: Release runs-on: macos-12 diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 4ecc82442..cf409bcec 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -13,3 +13,25 @@ jobs: project: Issue triage column: Incoming repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + add_to_triage: + runs-on: ubuntu-latest + if: > + github.repository == 'vector-im/element-x-ios' + steps: + - uses: octokit/graphql-action@v2.x + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4AMlHr" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 122543abc..291360fd2 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -210,6 +210,30 @@ jobs: PROJECT_ID: "PVT_kwDOAM0swc4AArk0" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ex_plorers: + name: Add labelled issues to X-Plorer project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: Element X Feature') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ps_features1: name: Add labelled issues to PS features team 1 runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d0261217e..4b1e4af56 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ out/ vendor/ .DS_Store api_key.p8 +*.orig +*.pbxproj +*.rej # CocoaPods # diff --git a/CHANGES.md b/CHANGES.md index f62115f7c..f2bfcdb39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,147 @@ +## Changes in 1.10.5 (2023-03-13) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.26.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.1)). + + +## Changes in 1.10.4 (2023-03-07) + +🙌 Improvements + +- CryptoV2: CryptoSDK phased rollout feature ([#7374](https://github.com/vector-im/element-ios/pull/7374)) +- Analytics: Use SwiftPM for AnalyticsEvents instead of CocoaPods ([#7401](https://github.com/vector-im/element-ios/pull/7401)) +- Upgrade MatrixSDK version ([v0.26.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.0)). +- Session verification: automatically starts scanning for a QR code if we do not have a QR code to display. ([#3115](https://github.com/vector-im/element-ios/issues/3115)) +- Direct Message: manage encrypted DM in case of invite by email ([#6612](https://github.com/vector-im/element-ios/issues/6612)) + +🐛 Bugfixes + +- fix issue on timeline's bubbles not showing proper content after decrypt ([#7397](https://github.com/vector-im/element-ios/pull/7397)) +- Fixes bug about centring user in live location sharing ([#7398](https://github.com/vector-im/element-ios/pull/7398)) +- Polls: improve rendering of poll ended events. ([#7402](https://github.com/vector-im/element-ios/pull/7402)) +- Fix an issue where SAS verification would fail between two iOS devices ([#3946](https://github.com/vector-im/element-ios/issues/3946)) +- Pin SwiftOGG to a release rather than main branch to avoid breaking changes in SwiftOGG causing bugs in element-ios. ([#7388](https://github.com/vector-im/element-ios/issues/7388)) + + +## Changes in 1.10.3 (2023-02-21) + +🙌 Improvements + +- Polls: add fallback text for poll ended events. ([#7353](https://github.com/vector-im/element-ios/pull/7353)) +- Push Rules: Apply push rules client side for encrypted rooms, hiding in case of dont_notify action ([#7356](https://github.com/vector-im/element-ios/pull/7356)) +- Map Views: Show own location in map views ([#7361](https://github.com/vector-im/element-ios/pull/7361)) +- Do not reset device keys if migrating to CryptoSDK ([#7369](https://github.com/vector-im/element-ios/pull/7369)) +- Labs: Rich Text Editor: Update to version 1.1.1 ([#7370](https://github.com/vector-im/element-ios/pull/7370)) +- Updates to protocol used for Sign in with QR code. ([#7372](https://github.com/vector-im/element-ios/pull/7372)) +- Upgrade MatrixSDK version ([v0.25.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.2)). + +🐛 Bugfixes + +- A voice message is now replayable. ([#7217](https://github.com/vector-im/element-ios/issues/7217)) +- Fix an issue where a voice message recording was failing. ([#7325](https://github.com/vector-im/element-ios/issues/7325)) +- Fix an issue where a voice message disappears after being sent. ([#7326](https://github.com/vector-im/element-ios/issues/7326)) + + +## Changes in 1.10.2 (2023-02-10) + +🐛 Bugfixes + +- Fixes #7350 - Fix green dot only to appear for marked action ([#7530](https://github.com/vector-im/element-ios/issues/7530)) + + +## Changes in 1.10.1 (2023-02-07) + +✨ Features + +- Add mark as unread option for rooms ([#7253](https://github.com/vector-im/element-ios/issues/7253)) + +🙌 Improvements + +- Polls: add logic for fetching poll histories in rooms. ([#7293](https://github.com/vector-im/element-ios/pull/7293)) +- Poll: add a feature to load more polls in the poll history. ([#7303](https://github.com/vector-im/element-ios/pull/7303)) +- CryptoV2: Generate Crypto SDK store key ([#7310](https://github.com/vector-im/element-ios/pull/7310)) +- Poll: added poll detail in poll list hisotry with navigation to timeline ([#7314](https://github.com/vector-im/element-ios/pull/7314)) +- Backup: Display backup import progress ([#7319](https://github.com/vector-im/element-ios/pull/7319)) +- Polls: sync push rules with the one of normal messages. ([#7320](https://github.com/vector-im/element-ios/pull/7320)) +- CryptoV2: Reset Crypto SDK on logout ([#7323](https://github.com/vector-im/element-ios/pull/7323)) +- Polls: add error handling when syncing push rules with the ones of normal messages. ([#7324](https://github.com/vector-im/element-ios/pull/7324)) +- CryptoV2: Refresh notification service on crypto change ([#7332](https://github.com/vector-im/element-ios/pull/7332)) +- CryptoV2: Enable Crypto SDK for production ([#7333](https://github.com/vector-im/element-ios/pull/7333)) +- Polls: add automatic synchronization logic for poll push rules. ([#7335](https://github.com/vector-im/element-ios/pull/7335)) +- Polls: update poll history UI. ([#7341](https://github.com/vector-im/element-ios/pull/7341)) +- Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)). +- Hide the presence info if the presence status is unknown. ([#6597](https://github.com/vector-im/element-ios/issues/6597)) +- Inform the user about decryption errors during a voice broadcast. ([#7189](https://github.com/vector-im/element-ios/issues/7189)) +- App Layout: Removed the onboarding flow ([#7298](https://github.com/vector-im/element-ios/issues/7298)) +- Improve error handling during a voice broadcast playback. ([#7311](https://github.com/vector-im/element-ios/issues/7311)) +- Labs: Rich text editor: enable list items indentation ([#7316](https://github.com/vector-im/element-ios/issues/7316)) + + +## Changes in 1.10.0 (2023-02-02) + +🙌 Improvements + +- CryptoV2: Generate Crypto SDK store key ([#7310](https://github.com/vector-im/element-ios/pull/7310)) +- Backup: Display backup import progress ([#7319](https://github.com/vector-im/element-ios/pull/7319)) +- CryptoV2: Reset Crypto SDK on logout ([#7323](https://github.com/vector-im/element-ios/pull/7323)) +- CryptoV2: Refresh notification service on crypto change ([#7332](https://github.com/vector-im/element-ios/pull/7332)) +- CryptoV2: Enable Crypto SDK for production ([#7333](https://github.com/vector-im/element-ios/pull/7333)) +- Upgrade MatrixSDK version ([v0.25.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.0)). + + +## Changes in 1.9.17 (2023-01-26) + +🙌 Improvements + +- Analytics: Ensure E2EE never tracks UnknownError ([#7304](https://github.com/vector-im/element-ios/pull/7304)) + +🐛 Bugfixes + +- Fix a deadlock when updating the summary of a room that has a voice broadcast. ([#7300](https://github.com/vector-im/element-ios/pull/7300)) +- Space Switcher: Fix a bug where the avatars would all be the same. ([#7305](https://github.com/vector-im/element-ios/issues/7305)) + + +## Changes in 1.9.16 (2023-01-24) + +✨ Features + +- Rich Text Composer: Enable bulleted/numbered lists support ([#7238](https://github.com/vector-im/element-ios/issues/7238)) +- Rich Text Composer: Enable quote & code blocks support ([#7271](https://github.com/vector-im/element-ios/issues/7271)) +- Voice Broadcast: When deleting a voice broadcast, all data is now deleted on server side (MSC3912 implementation). ([#7283](https://github.com/vector-im/element-ios/issues/7283)) + +🙌 Improvements + +- Labs: VoiceBroadcast: Handle VoIP buttons when VB is used ([#7225](https://github.com/vector-im/element-ios/pull/7225)) +- Polls: add UI for active poll history. ([#7267](https://github.com/vector-im/element-ios/pull/7267)) +- CryptoSDK: Add labs settings to enable Crypto SDK ([#7272](https://github.com/vector-im/element-ios/pull/7272)) +- Voice Broadcast: Improved detection of voice broadcast completion during playback. ([#7273](https://github.com/vector-im/element-ios/pull/7273)) +- Remove "Leave" button on Room details screen ([#7275](https://github.com/vector-im/element-ios/pull/7275)) +- Polls: poll history UI for past polls. ([#7278](https://github.com/vector-im/element-ios/pull/7278)) +- Polls: render replies to poll events better. ([#7284](https://github.com/vector-im/element-ios/pull/7284)) +- CryptoV2: Display migration progress during startup ([#7286](https://github.com/vector-im/element-ios/pull/7286)) +- Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)). +- Voice broadcast connection error handling while recording. ([#7229](https://github.com/vector-im/element-ios/issues/7229)) +- Handle a connection issue when we try to start a new voice broadcast. ([#7234](https://github.com/vector-im/element-ios/issues/7234)) +- Rich Text Editor: https:// or mailto: scheme is automatically added when creating a link if no scheme is specified. ([#7279](https://github.com/vector-im/element-ios/issues/7279)) +- Rich Text Editor: Adding a link over a blank selection, prompts the user to create a new link with new text to replace such selection. ([#7280](https://github.com/vector-im/element-ios/issues/7280)) +- Voice Broadcast: handle the lost of connectivity with the homeserver while recording. ([#7285](https://github.com/vector-im/element-ios/issues/7285)) + +🐛 Bugfixes + +- Voice Broadcast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. ([#7257](https://github.com/vector-im/element-ios/pull/7257)) +- Fix a crash caused by the missing Avatar Service dependency. ([#7268](https://github.com/vector-im/element-ios/pull/7268)) +- The (edited) tag for messages is now light grey like on web and Android. ([#5148](https://github.com/vector-im/element-ios/issues/5148)) +- Live Location Sharing does not work on first selection after granting "Allow always" location permission. ([#7222](https://github.com/vector-im/element-ios/issues/7222)) +- Voice Broadcast: Fixed an issue where the voice broadcast audio player progress bar behaved unexpectedly. ([#7252](https://github.com/vector-im/element-ios/issues/7252)) +- Voice Broadcast: VoiceBroadcast chunks are no longer resent as voice messages ([#7261](https://github.com/vector-im/element-ios/issues/7261)) +- Timeline's links and hyperlinks match now the blue colour of Android and Web. ([#7263](https://github.com/vector-im/element-ios/issues/7263)) + +🧱 Build + +- Fix Element Alpha workflow not being able to run. ([#7256](https://github.com/vector-im/element-ios/pull/7256)) + + ## Changes in 1.9.15 (2023-01-10) ✨ Features diff --git a/CHANGES_BWI.md b/CHANGES_BWI.md index 36155ac67..fc8171c5a 100644 --- a/CHANGES_BWI.md +++ b/CHANGES_BWI.md @@ -1,3 +1,66 @@ +Changes in BWI project 2.4.0 (2023-03-14) +=================================================== + +Upstream merge ✨: + +- v1.10.5 + +Features ✨: + +Improvements 🙌: +- Better new function banner (#2638) +- No confusing error messages when backend not available (#4174) + +Bugfix 🐛: +- Fix for passphrase error (#4171) +- Fix more than one person in DM (#4209) +- Fix wrong number for room invites in chat overview (#4173) +- Fix dark font in dark mode bug (#4446) + +Translations 🗣 : + +SDK API changes ⚠️: + +Build 🧱: +- Fix Open code compile problems (#4210) + + +Changes in BWI project 2.4.0 (2023-03-14) +=================================================== + +Upstream merge ✨: + +- v1.10.2 + +Features ✨: +- Poll history + +Improvements 🙌: +- Redesigned notes room in chat overview (#4211) +- New login flow for BwM (#4231) +- Added default server settings (#4218) +- Add changeable permalink config (#3425) +- Better permalink-header handling (#4146) +- Display user search results alphabetically (#3076) +- Text changes for downtime info (#4157) +- Changed text of reset passphrase button (#4167) +- Changed pin confirmation text (#4168) +- Color changes for ui elements (#4179) +- Improved forgot password text (#4161) + +Bugfix 🐛: +- Fix for Cross-Signing error (#4303) +- Fix for status message getting cut off (#4197) +- Fix wrong number for room invites in chat overview (#4173) + +Translations 🗣 : + +SDK API changes ⚠️: + ++Build 🧱: + + + Changes in BWI project 2.3.0 (2023-02-14) =================================================== diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index d4061ccd2..5763bfa75 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -16,5 +16,5 @@ // // Version -MARKETING_VERSION = 2.3.0 +MARKETING_VERSION = 2.5.0 CURRENT_PROJECT_VERSION = 20220714163152 diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 5baa3aec1..023ced5a2 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -108,10 +108,10 @@ class BWIBuildSettings: NSObject { var bwiAllowRoomPermalink = false var bwiAllowUserPermalink = false - + var bwiCheckAppVersion = true - var bwiNotificationTimes = true + var bwiNotificationTimes = false var bwiUserLabelsAdminSettingsVisible = true var bwiUserLabelsMemberDetailsVisible = true @@ -182,14 +182,38 @@ class BWIBuildSettings: NSObject { "74c038bb4e26fb1d0fcc14474ec9ff6fe3ec158e13286a787b90a22ee638ac18", "3740163f98aeda7dba285d2af1bfc351db395868268e2759ca701f926a6605a5", "240b05d9a54999140d23f21d104209fbc5e5179366ba3a7e58c8fad763aa88bd", - "4d5b6dcf02396274be58a69c4bbeba975b529f6b19c504fc99a37892ee1cf0b5"] + "4d5b6dcf02396274be58a69c4bbeba975b529f6b19c504fc99a37892ee1cf0b5", + "0d157119821bd9d76ac4f24c7f44f56e6bb5b766a6d5ee7dad6634420e79271a" + ] // use a different badge color if the user was mentioned in a room var showMentionsInRoom = true // replace feature history link variable with the appropiate build setting - var bwiFeatureHistoryLink = "https://messenger.bwi.de/#c4783" + var bwiFeatureHistoryLink = "https://messenger.bwi.de/bwmessenger#c6110" var bwiReplaceFeatureLink = true + + // feature history file path + var bwiFeatureHistoryFilePath: String { + guard let bundleFileURL = Bundle.main.url(forResource: newFeaturesHTML, withExtension: "html") else {return ""} + + // replace feature link and copy file into document directory + if bwiReplaceFeatureLink { + if var newFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last { + newFileURL = newFileURL.appendingPathComponent(newFeaturesHTML.appending(".html")) + do { + var text = try String(contentsOf: bundleFileURL, encoding: .utf8) + text = text.replacingOccurrences(of: "$FEATURELINK", with: bwiFeatureHistoryLink) + try text.write(to: newFileURL, atomically: false, encoding: .utf8) + + // return new path + return newFileURL.path + } catch {} + } + } + + return bundleFileURL.path + } // login with matrix id should only be enabled in some configurations var bwiEnableLoginWithMatrixID = true @@ -251,6 +275,8 @@ class BWIBuildSettings: NSObject { apiKey: "", termsURL: URL(string: "https://element.io/cookie-policy")!) + var sendMessageThreshold = 5.0 + // MARK: - Bug report var bugReportEndpointUrlString = "" @@ -360,6 +386,7 @@ class BWIBuildSettings: NSObject { var settingsNotificationsShowDefault = true var settingsNotificationsShowMentions = false var settingsNotificationsShowAdvanced = false + var notificationSettingsLikeAndroidAndWeb = true // MARK: - Timeline settings var roomInputToolbarCompressionMode: BuildSettings.MediaCompressionMode = .none @@ -409,6 +436,7 @@ class BWIBuildSettings: NSObject { // MARK: - Cross-signing (bwi=true) var disableSelfUserVerification = false var additionalSelfVerfificationAlert = false + var showNoOtherDeviceError = false // MARK: - Antivirus scan (bwi=true) @@ -427,6 +455,8 @@ class BWIBuildSettings: NSObject { var onboardingShowAccountPersonalization = false var onboardingEnableNewAuthenticationFlow = true + // show app specific welcoming screens + var onboardingShowWelcomeScreens = true // --- @@ -455,7 +485,7 @@ class BWIBuildSettings: NSObject { var passwordIndicatorOnLogin = true // MARK: Displays the element base version on the settings screen - var elementBaseVersion = "1.9.15" + var elementBaseVersion = "1.10.5" var showElementBaseVersion = true @@ -496,8 +526,6 @@ class BWIBuildSettings: NSObject { var bwiLastAdminCanDowngradeHimself = false - var bwiEnableErrorTracking = false - var bwiEnableRegisterInfo = false var bwiShowHappyBirthdayCampaign = false @@ -509,7 +537,7 @@ class BWIBuildSettings: NSObject { // MARK: - Message Bubbles bwi show in our menu and only for beta builds - var bwiShowMessageBubbleSettings = true + var bwiShowMessageBubbleSettings = false // bwi disable encrypted option in message context menu var roomContextualMenuShowEncryptionOption = false @@ -525,6 +553,9 @@ class BWIBuildSettings: NSObject { // one flag for all layout changes to the element login flow var bumLoginFlowLayout = true + // one flag for app specific layout changes + var bwiLoginFlowLayout = true + // website for users in public service that want a backend var bumAdvertizementURLString = "https://messenger.bwi.de/ich-will-bum"; @@ -544,11 +575,28 @@ class BWIBuildSettings: NSObject { // internal html page for netiquette in en and de var netiquetteEnHTML = "netiquette_en" var netiquetteDeHTML = "netiquette_de" - + // MARK: - Scan server qr code - var scanServerQRCode = true + var allowScanServerQRCode = true + + // MARK: - Login with qr code var allowLoginWithQR = false // should be set by the server but we disable it with false also in the app + + // MARK: - Scan permalink qr code + var clientPermalinkBaseUrl = "" + var allowScanPermalinkQRCode = false + var showMyQRCode = false // MARK: - Maintenance var enableMaintenanceInfoOnWelcomeScreen = false + + // MARK: User Search + var sortUserSearchResultsAlphabetically = true + + // MARK: Permalinks + var permalinkPrefixSettings = false + var permalinkPrefixes: [String] = [] + + // MARK: Client error search term + var clientErrorSearchTerm = "Request failed: client error" } diff --git a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift index a3810f8cf..3834ae151 100644 --- a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift +++ b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift @@ -21,15 +21,16 @@ extension BWIBuildSettings { func overrideTargetSpecificSettings() { secondaryAppName = "BundesMessenger" + settingsScreenShowLabSettings = true authScreenShowRegister = true - showTopBanner = false + showTopBanner = true bwiShowDeveloperSettings = true bwiPersonalState = true - bwiEnableErrorTracking = true bwiMatomoTrackingDefaultState = false - showSessionManager = true + showSessionManager = false locationSharingEnabled = false bwiLocationShareButtonVisible = false + bwiLoginFlowLayout = false } } diff --git a/Config/BuM/BWIBuildSettings+BuM.swift b/Config/BuM/BWIBuildSettings+BuM.swift index a3aea700c..d2f56db07 100644 --- a/Config/BuM/BWIBuildSettings+BuM.swift +++ b/Config/BuM/BWIBuildSettings+BuM.swift @@ -23,6 +23,8 @@ extension BWIBuildSettings { secondaryAppName = "BundesMessenger" locationSharingEnabled = false bwiLocationShareButtonVisible = false + bwiLoginFlowLayout = false + authScreenShowTestServerOptions = false } } diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 81dbb2cff..4b6068a96 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -90,6 +90,9 @@ class CommonConfiguration: NSObject, Configurable { EncryptionKeyManager.shared.initKeys() sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature + + // Configure Crypto SDK feature deciding which crypto module to use + sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared } private func makeASCIIUserAgent() -> String? { diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift index 338d1cfe8..4e967ab05 100644 --- a/DesignKit/Source/ColorValues.swift +++ b/DesignKit/Source/ColorValues.swift @@ -48,5 +48,7 @@ public struct ColorValues: Colors { public let ems: UIColor + public let links: UIColor + public let namesAndAvatars: [UIColor] } diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift index bf3e9abd3..bea9b0706 100644 --- a/DesignKit/Source/Colors.swift +++ b/DesignKit/Source/Colors.swift @@ -67,6 +67,10 @@ public protocol Colors { /// Global color: The EMS brand's purple colour. var ems: ColorType { get } + /// - Links + /// - Hyperlinks + var links: ColorType { get } + /// - Names in chat timeline /// - Avatars default states that include first name letter var namesAndAvatars: [ColorType] { get } diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift index ea3ca6779..bb25d025f 100644 --- a/DesignKit/Source/ColorsSwiftUI.swift +++ b/DesignKit/Source/ColorsSwiftUI.swift @@ -21,7 +21,7 @@ import SwiftUI Struct for holding colors for use in SwiftUI. */ public struct ColorSwiftUI: Colors { - + public let accent: Color public let alert: Color @@ -48,8 +48,10 @@ public struct ColorSwiftUI: Colors { public var ems: Color - public let namesAndAvatars: [Color] + public let links: Color + public let namesAndAvatars: [Color] + init(values: ColorValues) { accent = Color(values.accent) alert = Color(values.alert) @@ -64,6 +66,7 @@ public struct ColorSwiftUI: Colors { navigation = Color(values.navigation) background = Color(values.background) ems = Color(values.ems) + links = Color(values.links) namesAndAvatars = values.namesAndAvatars.map({ Color($0) }) } } diff --git a/DesignKit/Source/ColorsUIkit.swift b/DesignKit/Source/ColorsUIkit.swift index 3add385c3..5ca20ab0b 100644 --- a/DesignKit/Source/ColorsUIkit.swift +++ b/DesignKit/Source/ColorsUIkit.swift @@ -45,6 +45,8 @@ import UIKit public let navigation: UIColor public let background: UIColor + + public let links: UIColor public let namesAndAvatars: [UIColor] @@ -61,6 +63,7 @@ import UIKit tile = values.tile navigation = values.navigation background = values.background + links = values.links namesAndAvatars = values.namesAndAvatars } } diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift index 88bd12ff3..21394475c 100644 --- a/DesignKit/Variants/Colors/Dark/DarkColors.swift +++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift @@ -34,6 +34,7 @@ public class DarkColors { navigation: UIColor(rgb:0x21262C), background: UIColor(rgb:0x15191E), ems: UIColor(rgb: 0x7E69FF), + links: UIColor(rgb: 0x0086E6), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift index 93cb3eadb..f8fa0e8e3 100644 --- a/DesignKit/Variants/Colors/Light/LightColors.swift +++ b/DesignKit/Variants/Colors/Light/LightColors.swift @@ -35,6 +35,7 @@ public class LightColors { navigation: UIColor(rgb:0xF4F6FA), background: UIColor(rgb:0xFFFFFF), ems: UIColor(rgb: 0x7E69FF), + links: UIColor(rgb: 0x0086E6), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), diff --git a/Podfile b/Podfile index 77382704f..0b4deedaa 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.24.7' +$matrixSDKVersion = '= 0.26.1' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.24.7_bwi' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.1_bwi' } # Method to import the MatrixSDK def import_MatrixSDK @@ -74,13 +74,11 @@ abstract_target 'RiotPods' do pod 'WeakDictionary', '~> 2.0' # Piwik for analytics - pod 'MatomoTracker', '~> 7.4.1' + pod 'MatomoTracker', '~> 7.5.2' # PostHog for analytics - pod 'PostHog', '~> 1.4.4' + pod 'PostHog', '~> 2.0.0' pod 'Sentry', '~> 7.15.0' - pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift', :inhibit_warnings => false - # pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' pod 'OLMKit' pod 'zxcvbn-ios' diff --git a/Podfile.lock b/Podfile.lock index eedfa4ce6..2a1836953 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,31 +14,12 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession - - AnalyticsEvents (0.1.0) - BlueCryptor (1.0.32) - BlueECC (1.2.5) - BlueRSA (1.0.200) - Down (0.11.0) - DSBottomSheet (0.3.0) - DSWaveformImage (6.1.1) - - DTCoreText (1.6.27): - - DTCoreText/Core (= 1.6.27) - - DTFoundation/Core (~> 1.7.5) - - DTFoundation/DTAnimatedGIF (~> 1.7.5) - - DTFoundation/DTHTMLParser (~> 1.7.5) - - DTFoundation/UIKit (~> 1.7.5) - - DTCoreText/Core (1.6.27): - - DTFoundation/Core (~> 1.7.5) - - DTFoundation/DTAnimatedGIF (~> 1.7.5) - - DTFoundation/DTHTMLParser (~> 1.7.5) - - DTFoundation/UIKit (~> 1.7.5) - - DTFoundation/Core (1.7.18) - - DTFoundation/DTAnimatedGIF (1.7.18) - - DTFoundation/DTHTMLParser (1.7.18): - - DTFoundation/Core - - DTFoundation/UIKit (1.7.18): - - DTFoundation/Core - - DTTJailbreakDetection (0.4.0) - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - GBDeviceInfo (7.1.0): @@ -56,31 +37,26 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatomoTracker (7.4.1): - - MatomoTracker/Core (= 7.4.1) - - MatomoTracker/Core (7.4.1) - - MatrixSDK (0.24.7): - - MatrixSDK/Core (= 0.24.7) - - MatrixSDK/Core (0.24.7): + - MatrixSDK (0.26.1): + - MatrixSDK/Core (= 0.26.1) + - MatrixSDK/Core (0.26.1): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDK/CryptoSDK + - MatrixSDKCrypto (= 0.2.1) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.24.7): - - MatrixSDKCrypto (= 0.1.7) - - MatrixSDK/JingleCallStack (0.24.7): + - MatrixSDK/JingleCallStack (0.26.1): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.7) + - MatrixSDKCrypto (0.2.1) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) - OLMKit/olmc (3.2.12) - OLMKit/olmcpp (3.2.12) - - PostHog (1.4.4) + - PostHog (2.0.0) - ReadMoreTextView (3.0.1) - Realm (10.27.0): - Realm/Headers (= 10.27.0) @@ -95,7 +71,7 @@ PODS: - Sentry/Core (7.15.0) - SideMenu (6.5.0) - SwiftBase32 (0.9.0) - - SwiftFormat/CLI (0.50.7) + - SwiftFormat/CLI (0.50.2) - SwiftGen (6.6.2) - SwiftJWT (3.6.200): - BlueCryptor (~> 1.0) @@ -103,7 +79,7 @@ PODS: - BlueRSA (~> 1.0) - KituraContracts (~> 1.2) - LoggerAPI (~> 1.7) - - SwiftLint (0.50.3) + - SwiftLint (0.49.1) - SwiftyBeaver (1.9.5) - UICollectionViewLeftAlignedLayout (1.0.2) - UICollectionViewRightAlignedLayout (0.0.3) @@ -114,12 +90,9 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: - - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - Down (~> 0.11.0) - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) - - DTCoreText (~> 1.6.25) - - DTTJailbreakDetection (~> 0.4.0) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 7.1.0) @@ -127,11 +100,10 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatomoTracker (~> 7.4.1) - - MatrixSDK (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.24.7_bwi_beta`) - - MatrixSDK/JingleCallStack (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.24.7_bwi_beta`) + - MatrixSDK (= 0.26.1) + - MatrixSDK/JingleCallStack (= 0.26.1) - OLMKit - - PostHog (~> 1.4.4) + - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - Sentry (~> 7.15.0) @@ -156,9 +128,6 @@ SPEC REPOS: - Down - DSBottomSheet - DSWaveformImage - - DTCoreText - - DTFoundation - - DTTJailbreakDetection - FLEX - FlowCommoniOS - GBDeviceInfo @@ -172,7 +141,7 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatomoTracker + - MatrixSDK - MatrixSDKCrypto - OLMKit - PostHog @@ -193,34 +162,14 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC -EXTERNAL SOURCES: - AnalyticsEvents: - :branch: release/swift - :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk - :tag: v0.24.7_bwi_beta - -CHECKOUT OPTIONS: - AnalyticsEvents: - :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f - :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk - :tag: v0.24.7_bwi_beta - SPEC CHECKSUMS: AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58 - AnalyticsEvents: 0cc8cf52da2fd464a2f39b788a295988151116ce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce - DTCoreText: ac297b565abd3b12390d33cd6e076d875f0c0a5e - DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - DTTJailbreakDetection: 5e356c5badc17995f65a83ed9483f787a0057b71 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 @@ -234,21 +183,20 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixSDK: 895929fad10b7ec9aa96d557403b44c5e3522211 - MatrixSDKCrypto: 2bd9ca41b2c644839f4e680a64897d56b3f95392 + MatrixSDK: 0a371af6c33ef20c9c5000cf2badf3d600c25d26 + MatrixSDKCrypto: 477d818bf2cc37b6cf702a290eb647bc8cf3cb1b OLMKit: da115f16582e47626616874e20f7bb92222c7a51 - PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f + PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2 Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 Sentry: 63ca44f5e0c8cea0ee5a07686b02e56104f41ef7 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 - SwiftFormat: 4fcf72ee44c7198255108c22ed7135c38a36ba6b + SwiftFormat: 710117321c55c82675c0dc03055128efbb13c38f SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c SwiftJWT: 88c412708f58c169d431d344c87bc79a87c830ae - SwiftLint: 77f7cb2b9bb81ab4a12fcc86448ba3f11afa50c6 + SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 UICollectionViewLeftAlignedLayout: 830bf6fa5bab9f9b464f62e3384f9d2e00b3c0f6 UICollectionViewRightAlignedLayout: 823eef8c567eba4a44c21bc2ffcb0d0d5f361e2d @@ -256,6 +204,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 869221f53222bc657fc29068c97625ccb2b82cb5 +PODFILE CHECKSUM: d0f3ced275c85b9eadb07bd1b92ad8ae7a40e243 COCOAPODS: 1.11.3 diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7af877671..0968cfb53 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/devicekit/DeviceKit", "state" : { - "revision" : "20e0991f3975916ab0f6d58db84d8bc64f883537", - "version" : "4.7.0" + "revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6", + "version" : "4.9.0" + } + }, + { + "identity" : "dtcoretext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTCoreText", + "state" : { + "revision" : "9d2d4d2296e5d2d852a7d3c592b817d913a5d020", + "version" : "1.6.27" + } + }, + { + "identity" : "dtfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTFoundation.git", + "state" : { + "revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3", + "version" : "1.7.18" } }, { @@ -27,12 +45,22 @@ "version" : "5.12.2" } }, + { + "identity" : "matrix-analytics-events", + "kind" : "remoteSourceControl", + "location" : "https://github.com/matrix-org/matrix-analytics-events", + "state" : { + "revision" : "2f5fa5f1e2f6c6ae1a47c33d953a3ce289167eb0", + "version" : "0.5.0" + } + }, { "identity" : "matrix-wysiwyg-composer-swift", "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "534ee5bae5e8de69ed398937b5edb7b5f21551d2" + "revision" : "addf90f3e2a6ab46bd2b2febe117d9cddb646e7d", + "version" : "1.1.1" } }, { @@ -58,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "48254824bb4248676bf7ce56014ff57b142b77eb", - "version" : "1.0.2" + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" } }, { @@ -67,7 +95,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/swift-ogg", "state" : { - "branch" : "main", + "branch" : "0.0.1", "revision" : "e9a9e7601da662fd8b97d93781ff5c60b4becf88" } } diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png deleted file mode 100644 index 95fb854c7..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png deleted file mode 100644 index 40c13c07a..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png deleted file mode 100644 index 8cb11ba5d..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png deleted file mode 100644 index 119903ee6..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@2x.png deleted file mode 100644 index 5e33559ac..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@3x.png deleted file mode 100644 index b36afc879..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@3x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png deleted file mode 100644 index 274db9f56..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png deleted file mode 100644 index 6c2ae7bbf..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png deleted file mode 100644 index 8bb136ca1..000000000 Binary files a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/BWI/new_features.imageset/Contents.json b/Riot/Assets/Images.xcassets/BWI/new_features.imageset/Contents.json new file mode 100644 index 000000000..fff4ea669 --- /dev/null +++ b/Riot/Assets/Images.xcassets/BWI/new_features.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_new-releases.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/BWI/new_features.imageset/icon_new-releases.svg b/Riot/Assets/Images.xcassets/BWI/new_features.imageset/icon_new-releases.svg new file mode 100644 index 000000000..68d70cc4c --- /dev/null +++ b/Riot/Assets/Images.xcassets/BWI/new_features.imageset/icon_new-releases.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/Contents.json b/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/Contents.json new file mode 100644 index 000000000..1b2760b4f --- /dev/null +++ b/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon_qrcode.viewfinder.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/icon_qrcode.viewfinder.svg b/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/icon_qrcode.viewfinder.svg new file mode 100644 index 000000000..d15586f35 --- /dev/null +++ b/Riot/Assets/Images.xcassets/BWI/qrcode_viewfinder.imageset/icon_qrcode.viewfinder.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json similarity index 64% rename from Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json index fd0b40307..bcb234ccc 100644 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "all_chats_onboarding3.png", + "filename" : "code_block.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "all_chats_onboarding3@2x.png", + "filename" : "code_block@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "all_chats_onboarding3@3x.png", + "filename" : "code_block@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png new file mode 100644 index 000000000..a70342a59 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png differ diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png new file mode 100644 index 000000000..8b69b41cb Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png new file mode 100644 index 000000000..21bd83678 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json b/Riot/Assets/Images.xcassets/Image.imageset/Contents.json similarity index 64% rename from Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Image.imageset/Contents.json index d6a6b5903..a19a54922 100644 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Image.imageset/Contents.json @@ -1,17 +1,14 @@ { "images" : [ { - "filename" : "all_chats_onboarding1.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "all_chats_onboarding1@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "all_chats_onboarding1@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json index 92d19c27b..fc47c46c7 100644 --- a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "Up.png", + "filename" : "Up_bwm.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Up@2x.png", + "filename" : "Up_bwm@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Up@3x.png", + "filename" : "Up_bwm@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png deleted file mode 100644 index c9556b53d..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png deleted file mode 100644 index f029fd55b..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png deleted file mode 100644 index 90d3fc8ac..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm.png new file mode 100644 index 000000000..715f742d3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@2x.png new file mode 100644 index 000000000..61ca4ab16 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@3x.png new file mode 100644 index 000000000..97442599b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up_bwm@3x.png differ diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Contents.json similarity index 55% rename from Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Contents.json index 99aa89f84..fcaa36fbf 100644 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "all_chats_onboarding2.png", + "filename" : "Up_bum.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "all_chats_onboarding2@2x.png", + "filename" : "Up_bum@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "all_chats_onboarding2@3x.png", + "filename" : "Up_bum@3x.png", "idiom" : "universal", "scale" : "3x" } @@ -19,8 +19,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true } } diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum.png new file mode 100644 index 000000000..322badcf0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@2x.png new file mode 100644 index 000000000..0526a3126 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@3x.png new file mode 100644 index 000000000..509e3fc6f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up_bum.imageset/Up_bum@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon.png index 02dc65a0b..672e903e4 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon.png and b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@2x.png index 28b0183c6..57d7144bd 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@2x.png and b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@3x.png index 4e3719595..59f1b88a1 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@3x.png and b/Riot/Assets/Images.xcassets/Room/Input/send_icon.imageset/send_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/Contents.json new file mode 100644 index 000000000..2af211851 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "send_icon_bum.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "send_icon_bum@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "send_icon_bum@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum.png new file mode 100644 index 000000000..aeb1bb058 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@2x.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@2x.png new file mode 100644 index 000000000..a326cbe1a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@3x.png b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@3x.png new file mode 100644 index 000000000..50d5a3b8c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Input/send_icon_bum.imageset/send_icon_bum@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon.png index 697ac06ee..2debff92b 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon.png and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@2x.png index 025ece33f..0512ee132 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@2x.png and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@3x.png index 98927ec85..3eb94726a 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@3x.png and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon.imageset/location_pin_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/Contents.json new file mode 100644 index 000000000..cb2cf800b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_pin_icon_bum.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_pin_icon_bum@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_pin_icon_bum@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum.png new file mode 100644 index 000000000..79c9fa56c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@2x.png new file mode 100644 index 000000000..999bf4e34 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@3x.png new file mode 100644 index 000000000..452a77a65 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_pin_icon_bum.imageset/location_pin_icon_bum@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected.png index 8771564f4..f5a19c9ed 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected.png and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@2x.png index deee479e9..2cf03802f 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@2x.png and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@3x.png index cb6195cb5..7faab4900 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@3x.png and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected.imageset/poll_checkbox_selected@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/Contents.json new file mode 100644 index 000000000..bf129ef13 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "poll_checkbox_selected_bum.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "poll_checkbox_selected_bum@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "poll_checkbox_selected_bum@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum.png new file mode 100644 index 000000000..e2f61505f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@2x.png new file mode 100644 index 000000000..ef5418485 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@3x.png new file mode 100644 index 000000000..4d191edea Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_checkbox_selected_bum.imageset/poll_checkbox_selected_bum@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json index b7fbce06b..ca79162a5 100644 --- a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_winner_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_winner_icon.imageset/Contents.json index 61c735d9b..4c8c50c72 100644 --- a/Riot/Assets/Images.xcassets/Room/Polls/poll_winner_icon.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_winner_icon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png index ffeb00aaf..7d8741a37 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png index 8582e2d23..3300d9a69 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png index e48d9a36b..9f8fd25f3 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/Contents.json new file mode 100644 index 000000000..c1019c535 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "microphone_asset_bum .png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "microphone_asset_bum@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "microphone_asset_bum@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum .png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum .png new file mode 100644 index 000000000..2509af64a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum .png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@2x.png new file mode 100644 index 000000000..bec61e88e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@3x.png new file mode 100644 index 000000000..579fbd018 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording_bum.imageset/microphone_asset_bum@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json new file mode 100644 index 000000000..fcc0b5765 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pollHistory.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg new file mode 100644 index 000000000..a0243252c --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings index a9a7b2cb6..7810fc3cc 100644 --- a/Riot/Assets/ar.lproj/Vector.strings +++ b/Riot/Assets/ar.lproj/Vector.strings @@ -455,7 +455,7 @@ "sign_up" = "الاِشتِراك"; "dismiss" = "إبعَاد"; "discard" = "اِستِبعاد"; -"abort" = "إِجهَاض"; +"abort" = "إنهاء"; "yes" = "نَعَم"; // Action @@ -1078,3 +1078,8 @@ /* The placeholder will show the email address that was entered. */ "authentication_verify_email_waiting_message" = "اتبع التعليمات المرسلة إلى %@"; "invite_to" = "الدعوة إلى %@"; +"password_policy_pwd_in_dict_error" = "تم العثور على كلمة المرور هذه في القاموس لدينا، وهي كلمة مرور غير مسموح في استخدامها."; + +// Others +"or" = "أو"; +"accessibility_selected" = "تم تحديده"; diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 2b9a25482..09bfd3421 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -31,9 +31,11 @@ "downtime_title" = "⚠ Server nicht erreichbar"; "downtime_default_message" = "Wir führen gerade Wartungsarbeiten durch. Bitte versuche es später erneut."; "downtime_alert_dismiss_button" = "Zurück"; -"settings_downtime_message_same_day" = "Der %@ steht von %@-%@ Uhr (UTC%@) nicht zur Verfügung."; -"settings_downtime_message_different_days" = "Der %@ steht von %@ Uhr (UTC%@) bis %@ Uhr (UTC%@) nicht zur Verfügung."; +"settings_downtime_message_same_day" = "Der %@ steht am %@, %@ von %@ bis %@ Uhr (UTC%@) nicht zur Verfügung. Nachrichten können in dieser Zeit nicht verschickt oder empfangen werden."; +"settings_downtime_message_different_days" = "Der %@ steht von %@, %@ Uhr (UTC%@) bis %@, %@ Uhr (UTC%@) nicht zur Verfügung. Nachrichten können in dieser Zeit nicht verschickt oder empfangen werden."; "settings_copyright" = "Copyright"; +"standard_error_alert_title_downtime" = "Der Server ist momentan nicht erreichbar. Versuche es später erneut."; +"standard_error_alert_title_no_downtime" = "Der Server ist momentan nicht erreichbar. Versuche es später erneut."; // MARK: - Server Selection (beta) @@ -145,7 +147,7 @@ "auth_user_id_placeholder" = "Benutzername"; "auth_forgot_password" = "Passwort vergessen?"; -"bwi_auth_forgot_password_alert_text" = "Falls du dein Passwort vergessen hast, wende dich an deinen Poolverwalter oder gehe auf das SelfServicePortal im IntranetBw und setze dein Passwort zurück."; +"bwi_auth_forgot_password_alert_text" = "Falls Du dein Passwort vergessen hast, wende dich an deinen Poolverwalter oder gehe auf das SelfServicePortal und setze dein Passwort zurück."; "auth_login_headline_text" = "Verbinde mit BwMessenger"; "auth_login_subheadline_text" = "Dein Messenger für unser Land"; @@ -167,13 +169,13 @@ "bwi_room_participants_section_admin" = "Admin"; "bwi_room_participants_section_moderator" = "Moderator"; "bwi_room_participants_section_member" = "Mitglied"; +"bwi_room_participants_section_invite" = "Eingeladen"; // MARK: - Settings "settings_default" = "Benachrichtigungen"; "settings_mentions_and_keywords" = "Mentions and Keywords"; "settings_other" = "Erweitert"; -"settings_notify_me_for" = "Benachrichtige mich für"; "settings_direct_messages" = "Direktnachrichten"; "settings_encrypted_direct_messages" = "Direktnachrichten"; "settings_group_messages" = "Räume"; @@ -500,11 +502,9 @@ // MARK: - Matomo -"MATOMO_USER_INFO" = "Wir brauchen Deine Hilfe, um einige Fehler im Messenger besser analysieren zu können.\nDazu erfassen wir Diagnosedaten.\nDetails dazu findest Du in der Datenschutzerklärung."; -"MATOMO_USER_INFO_BUTTON_OK" = "OK"; -"MATOMO_USER_INFO_BUTTON_MORE_INFO" = "Datenschutzerklärung"; -"MATOMO_SETTINGS_SEND_DIAGNOSTIC_DATA" = "Diagnosedaten senden"; "bwi_settings_analytics_section_header" = "Fehleranalyse"; +"bwi_settings_analytics_switch_text" = "Fehleranalyse unterstützen"; +"bwi_settings_analytics_section_footer" = "Details dazu findest du in der Datenschutzerklärung"; "bwi_analytics_alert_title" = "Fehleranalyse"; "bwi_analytics_alert_body" = "Wir brauchen Deine Hilfe, um Fehler im %@ besser analysieren zu können. Dazu würden wir gerne anonymisierte Diagnosedaten erfassen. Es werden keine Daten an Dritte übermittelt. Details findest Du in der Datenschutzerklärung.\n\nFalls Du nicht mehr mithelfen möchtest, kannst Du dies in den Einstellungen jederzeit wieder deaktivieren.\n\nMöchtest du bei der Fehler-Analyse unterstützen?"; "bwi_analytics_alert_ok_button" = "Zustimmen"; @@ -515,6 +515,9 @@ "bwi_settings_new_features_header" = "Neue Funktionen"; "bwi_settings_new_features_show_features" = "Neue Funktionen anzeigen"; +"bwi_feature_banner_header" = "Neue Funktionen"; +"bwi_feature_banner_show_more_button" = "Erfahre mehr"; +"bwi_feature_banner_advertisement_text" = "Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich \"Umfrageverlauf\")."; // MARK: - Onboarding "onboarding_splash_login_button_title" = "Loslegen"; @@ -571,6 +574,11 @@ // MARK: - New Layout "all_chats_edit_layout_show_personal_notes" = "Notizenraum anzeigen"; +// MARK: - Permalink Scan +"room_recents_scan_qr_code" = "QR Code scannen"; +"room_recents_scan_failed_title" = "Scan fehlgeschlagen"; +"room_recents_scan_failed_message" = "Dieser QR Code entspricht keinem gültigen Permalink."; + // MARK: - Context Menu All Chats "room_recents_create_empty_room" = "Neuer Raum"; "room_recents_start_chat_with" = "Neue Direktnachricht"; @@ -578,3 +586,23 @@ // MARK: - Chat creation "room_creation_title" = "Neue Direktnachricht"; + +// MARK: - Permalink +"settings_permalink_prefix_picker_title" = "Permalink Prefix"; + +// MARK: - Notification Settings +"settings_notify_me_for" = "Benachrichtige mich für"; +"room_notifs_settings_encrypted_room_notice" = ""; +"room_notifs_settings_notify_me_for" = "Einstellungen für diesen Raum"; +"room_notifs_settings_all_messages" = "Standardeinstellungen"; +"room_notifs_settings_none" = "Benachrichtigungen aus"; +"room_notifs_settings_all_description" = "Es gelten globale Benachrichtigungseinstellungen"; + +// MARK: - Notice Room +"notice_room_leave" = "%@ verließ den Raum"; + +// MARK: - Show my QR +"show_my_qr_settings_title" = "Meinen QR Code anzeigen"; +"show_my_qr_screen_title" = "Mein QR Code"; +"show_my_qr_screen_message" = "Dies ist der QR Code zu deinem Profil.\nLasse den QR Code mit der %@ App scannen, damit andere mit dir in Kontakt treten können."; + diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 307f2f12d..e2758378d 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -308,7 +308,7 @@ "room_preview_unlinked_email_warning" = "Diese Einladung wurde an %@ gesendet, welche diesem Konto nicht zugeordnet ist. Möglicherweise möchtest du dich mit einem anderen Konto anmelden oder diese E-Mail zu deinem Konto hinzufügen."; "room_preview_try_join_an_unknown_room" = "Du versuchst auf %@ zuzugreifen. Möchtest du dem Raum beitreten um teilzunehmen?"; "settings_config_identity_server" = "Identitätsserver ist %@"; -"settings_labs" = "LABORE"; +"settings_labs" = "LABOR"; "settings_sign_out_e2e_warn" = "Du wirst die Ende-zu-Ende Schlüssel verlieren. Du kannst dann auf diesem Gerät in verschlüsselten Räumen keine alten Nachrichten mehr lesen."; "settings_phone_number" = "Telefon"; "settings_add_phone_number" = "Telefonnummer hinzufügen"; @@ -2457,9 +2457,7 @@ "all_chats_all_filter" = "Alle"; "all_chats_edit_layout_show_filters" = "Filter anzeigen"; "all_chats_edit_menu_leave_space" = "%@ verlassen"; -"all_chats_onboarding_page_title3" = "Rückmeldung geben"; "room_invites_empty_view_information" = "Hier erscheinen deine Einladungen."; -"all_chats_onboarding_try_it" = "Probiere es aus"; "threads_discourage_information_1" = "Dein Heimserver unterstützt aktuell keine Threads, weshalb diese Funktion unzuverlässig sein könnte. Manche Thread-Nachrichten könnten nicht zuverlässig verfügbar sein. "; "all_chats_nothing_found_placeholder_title" = "Nichts gefunden."; "spaces_create_subspace_title" = "Sub-Space erstellen"; @@ -2475,16 +2473,10 @@ "room_access_settings_screen_private_message" = "Nur sichtbar und betretbar für eingeladene Personen."; "location_sharing_allow_background_location_message" = "Wenn du deinen Echtzeit-Standort freigeben möchtest, benötigt Element den Standortzugriff auch im Hintergrund. Um den Zugriff zu gewähren, tippe auf Einstellungen > Standort und wähle „Immer“"; "space_selector_empty_view_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Erstelle einen Space, um zu beginnen."; -"all_chats_onboarding_title" = "Was ist neu"; -"all_chats_onboarding_page_message3" = "Drücke auf dein Profil um uns Wissen zu lassen, was du denkst."; -"all_chats_onboarding_page_message2" = "Greife auf deine Spaces (unten links) schneller und einfacher denn je zu."; -"all_chats_onboarding_page_title2" = "Auf Spaces zugreifen"; -"all_chats_onboarding_page_message1" = "Um dein Element zu vereinfachen, sind Tabs nun optional. Verwalte sie mit dem Menü oben rechts."; "all_chats_empty_view_information" = "Die Komplettlösung für sichere Kommunikation unter Freunden, in Gruppen oder in Organisationen. Erstelle eine Unterhaltung oder trete einem bestehenden Raum bei, um loszulegen."; "all_chats_empty_space_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Füge einen bestehenden Raum hinzu oder erstelle einen neuen mit der Schaltfläche unten rechts."; "all_chats_edit_layout_sorting_options_title" = "Sortiere deine Nachrichten nach"; "space_detail_nav_title" = "Space-Details"; -"all_chats_onboarding_page_title1" = "Willkommen in einer neuen Übersicht!"; "all_chats_edit_menu_space_settings" = "Space-Einstellungen"; "all_chats_user_menu_settings" = "Nutzereinstellungen"; "room_recents_recently_viewed_section" = "Kürzlich angesehen"; @@ -2700,5 +2692,47 @@ "notice_voice_broadcast_live" = "Echtzeitübertragung"; "user_other_session_security_recommendation_title" = "Andere Sitzungen"; "voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen"; -"poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden"; -"voice_message_broadcast_in_progress_message" = "Du kannst kein Gespräch beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; +"poll_timeline_decryption_error" = "Evtl. werden infolge von Entschlüsselungsfehlern einige Stimmen nicht gezählt"; +"voice_message_broadcast_in_progress_message" = "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; +"poll_timeline_ended_text" = "Umfrage beendet"; +"voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen."; +"voice_broadcast_voip_cannot_start_title" = "Kann keinen Anruf beginnen"; +"poll_history_no_past_poll_text" = "In diesem Raum gibt es keine abgeschlossenen Umfragen"; +"poll_history_no_active_poll_text" = "In diesem Raum gibt es keine aktiven Umfragen"; +"poll_history_past_segment_title" = "Vergangene Umfragen"; +"poll_history_active_segment_title" = "Aktive Umfragen"; + +// MARK: - Polls history + +"poll_history_title" = "Umfrageverlauf"; +"room_details_polls" = "Umfrageverlauf"; +"accessibility_selected" = "ausgewählt"; +"voice_broadcast_playback_lock_screen_placeholder" = "Sprachübertragung"; +"voice_broadcast_connection_error_message" = "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut."; +"voice_broadcast_connection_error_title" = "Verbindungsfehler"; +"wysiwyg_composer_format_action_code_block" = "Quelltextblock umschalten"; +"wysiwyg_composer_format_action_quote" = "Zitat umschalten"; +"wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten"; +"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; +"voice_broadcast_recorder_connection_error" = "Verbindungsfehler − Aufnahme pausiert"; +"poll_timeline_reply_ended_poll" = "Beendete Umfrage"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migriere Daten\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung (zum Deaktivieren abmelden)"; +"settings_labs_confirm_crypto_sdk" = "Bitte beachte, dass diese Funktion noch experimentell ist, womöglich nicht wie erwartet funktioniert und unerwünschte Nebeneffekte haben kann. Melde dich zum deaktivieren einfach ab und erneut an. Nutze diese Funktion nach eigenem Ermessen und mit Vorsicht."; +"settings_labs_enable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung"; +"poll_history_no_past_poll_period_text" = "Für die vergangenen %@ Tage sind keine beendeten Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; +"poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; +"poll_history_load_more" = "Weitere Umfragen laden"; +"poll_history_loading_text" = "Zeige Umfragen an"; +"poll_history_fetching_error" = "Fehler beim Laden der Umfragen."; +"key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen"; +"voice_broadcast_playback_unable_to_decrypt" = "Entschlüsseln der Sprachübertragung nicht möglich."; +"home_context_menu_mark_as_unread" = "Als ungelesen markieren"; +"wysiwyg_composer_format_action_un_indent" = "Einrückung verringern"; +"wysiwyg_composer_format_action_indent" = "Einrückung erhöhen"; +"settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten."; +"poll_history_detail_view_in_timeline" = "Umfrage im Verlauf anzeigen"; +"authentication_qr_login_failure_device_not_supported" = "Die Verbindung mit diesem Gerät wird nicht unterstützt."; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index 68733a6fa..46e33cb3f 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -31,9 +31,11 @@ "downtime_title" = "⚠ Server not available"; "downtime_default_message" = "We are working on our maintenance. Please try again later."; "downtime_alert_dismiss_button" = "Back"; -"settings_downtime_message_same_day" = "The BwMessenger is not available between %@-%@ (UTC%@). Messages may not be sent or received during that time."; -"settings_downtime_message_different_days" = "The BwMessenger is not available from %@ (UTC%@) to %@ (UTC%@). Messages may not be sent or received during that time."; +"settings_downtime_message_same_day" = "The %@ is not available on %@, %@ between %@ and %@ (UTC%@). Messages may not be sent or received during that time."; +"settings_downtime_message_different_days" = "The %@ is not available from %@, %@ (UTC%@) to %@, %@ (UTC%@). Messages may not be sent or received during that time."; "settings_copyright" = "Copyright"; +"standard_error_alert_title_downtime" = "The server is currently unavailable. Try again later."; +"standard_error_alert_title_no_downtime" = "The server is currently unavailable. Try again later."; // MARK: - Server Selection (beta) @@ -124,7 +126,7 @@ "authentication_terms_title" = "Server policies"; "auth_user_id_placeholder" = "User name"; "auth_forgot_password" = "Forgot password?"; -"bwi_auth_forgot_password_alert_text" = "If you have forgotton your password, contact your pool manager or go to the SelfServicePortal at IntranetBw and reset your password."; +"bwi_auth_forgot_password_alert_text" = "If you have forgotton your password, contact your pool manager or go to the SelfServicePortal and reset your password."; "auth_forgot_password_error_no_configured_identity_server" = "No identity server is configured: add one to reset your password."; "auth_login_headline_text" = "Connect with BwMessenger"; "auth_login_subheadline_text" = "Your Messenger for our country"; @@ -137,6 +139,7 @@ "bwi_room_participants_section_admin" = "Admin"; "bwi_room_participants_section_moderator" = "Moderator"; "bwi_room_participants_section_member" = "Member"; +"bwi_room_participants_section_invite" = "Invited"; // MARK: - Settings @@ -189,8 +192,8 @@ "key_verification_this_session_title" = "Verify this session"; "device_verification_other_login_verify_wait_title" = "Verify session"; "device_verification_self_verify_wait_title" = "Verify this session"; -"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Other recovery method"; -"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Other recovery method"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Recovery ke"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Recovery key"; "device_verification_self_verify_wait_recover_secrets_additional_information" = "Use your recovery key to decrypt all your messages."; "device_verification_self_verify_wait_information" = "Verify your login with a device you are already logged in with to access your encrypted messages."; "device_verification_self_verify_wait_information_more" = ""; @@ -379,11 +382,9 @@ // MARK: - Matomo -"MATOMO_USER_INFO" = "To detect problems much easier and improve the app we need your support.\nThis app collects analytics data.\nDetails about this can be found in our privacy policy."; -"MATOMO_USER_INFO_BUTTON_OK" = "OK"; -"MATOMO_USER_INFO_BUTTON_MORE_INFO" = "Privacy Police"; -"MATOMO_SETTINGS_SEND_DIAGNOSTIC_DATA" = "Send analyze data"; "bwi_settings_analytics_section_header" = "Error analysis"; +"bwi_settings_analytics_switch_text" = "Support error analysis"; +"bwi_settings_analytics_section_footer" = "Details are in the privacy policy"; "bwi_analytics_alert_title" = "Error analytics"; "bwi_analytics_alert_body" = "WWe need your help in collecting issues to improve %@. For that reason we would like to gather anonymized diagnostic data. No data will be transmitted to third parties. Details can be found in the privacy policy.\n\nIf you don't want to help anymore, you can deactivate this in the settings at any time.\n\nDo you want to support us?"; "bwi_analytics_alert_ok_button" = "Agree"; @@ -394,6 +395,9 @@ "bwi_settings_new_features_header" = "New Features"; "bwi_settings_new_features_show_features" = "Show new features"; +"bwi_feature_banner_header" = "New Features"; +"bwi_feature_banner_show_more_button" = "Learn more"; +"bwi_feature_banner_advertisement_text" = "You can now see a poll history in the room details."; // MARK: - Onboarding "onboarding_splash_login_button_title" = "Let's go"; @@ -450,6 +454,11 @@ // MARK: - New Layout "all_chats_edit_layout_show_personal_notes" = "Show personal notes"; +// MARK: - Permalink Scan +"room_recents_scan_qr_code" = "Scan QR Code"; +"room_recents_scan_failed_title" = "Scan failed"; +"room_recents_scan_failed_message" = "This qr code does not conform to a permlink."; + // MARK: Context Menu All Chats "room_recents_create_empty_room" = "New room"; "room_recents_start_chat_with" = "New chat"; @@ -457,3 +466,23 @@ // MARK: - Chat creation "room_creation_title" = "New chat"; + +// MARK: - Permalink +"settings_permalink_prefix_picker_title" = "Permalink Prefix"; + +// MARK: - Notification Settings +"settings_notify_me_for" = "Notify me for"; +"room_notifs_settings_encrypted_room_notice" = ""; +"room_notifs_settings_notify_me_for" = "Settings for this room"; +"room_notifs_settings_all_messages" = "Default settings"; +"room_notifs_settings_none" = "Notifications off"; +"room_notifs_settings_all_description" = "Uses the global notification settings"; + +// MARK: - Notice Room +"notice_room_leave" = "%@ left"; + +// MARK: - Show my QR +"show_my_qr_settings_title" = "Show my QR code"; +"show_my_qr_screen_title" = "My QR Code"; +"show_my_qr_screen_message" = "This is the QR code to your profile.\nLet others scan the QR code with the %@ app so that they can get in touch with you."; + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 04d0cd693..f9eaaef5d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -96,6 +96,7 @@ // Accessibility "accessibility_checkbox_label" = "checkbox"; "accessibility_button_label" = "button"; +"accessibility_selected" = "selected"; // MARK: Onboarding "onboarding_splash_register_button_title" = "Create account"; @@ -238,6 +239,7 @@ "authentication_qr_login_loading_signed_in" = "You are now signed in on your other device."; "authentication_qr_login_failure_title" = "Linking failed"; +"authentication_qr_login_failure_device_not_supported" = "Linking with this device is not supported."; "authentication_qr_login_failure_invalid_qr" = "QR code is invalid."; "authentication_qr_login_failure_request_denied" = "The request was denied on the other device."; "authentication_qr_login_failure_request_timed_out" = "The linking wasn’t completed in the required time."; @@ -363,6 +365,7 @@ "room_creation_invite_another_user" = "User ID, name or email"; "room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email."; "room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again."; +"room_creation_only_one_email_invite" = "You can only invite one email at a time"; // Room recents "room_recents_directory_section" = "ROOM DIRECTORY"; @@ -697,7 +700,7 @@ Tap the + to start adding people."; "settings_phone_contacts" = "PHONE CONTACTS"; "settings_advanced" = "ADVANCED"; "settings_about" = "ABOUT"; -"settings_labs" = "LABS"; +"settings_labs" = "LAB"; "settings_flair" = "Show flair where allowed"; "settings_devices" = "SESSIONS"; "settings_cryptography" = "CRYPTOGRAPHY"; @@ -758,6 +761,7 @@ Tap the + to start adding people."; "settings_your_keywords" = "Your Keywords"; "settings_new_keyword" = "Add new Keyword"; "settings_mentions_and_keywords_encryption_notice" = "You won’t get notifications for mentions & keywords in encrypted rooms on mobile."; +"settings_push_rules_error" = "An error occurred when updating your notification preferences. Please try to toggle your option again."; "settings_enable_callkit" = "Integrated calling"; "settings_callkit_info" = "Receive incoming calls on your lock screen. See your %@ calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; @@ -803,6 +807,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; +"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; @@ -982,6 +989,7 @@ Tap the + to start adding people."; "room_details_title_for_dm" = "Details"; "room_details_people" = "Members"; "room_details_files" = "Uploads"; +"room_details_polls" = "Poll history"; "room_details_search" = "Search room"; "room_details_integrations" = "Integrations"; "room_details_settings" = "Settings"; @@ -1464,6 +1472,7 @@ Tap the + to start adding people."; // Recover from private key "key_backup_recover_from_private_key_info" = "Restoring backup…"; +"key_backup_recover_from_private_key_progress" = "%@%% Complete"; // Recover from passphrase @@ -1522,7 +1531,7 @@ Tap the + to start adding people."; "device_verification_cancelled_by_me" = "The verification has been cancelled. Reason: %@"; "device_verification_error_cannot_load_device" = "Cannot load session information."; -// Mark: Incoming +// MARK: Incoming "device_verification_incoming_title" = "Incoming Verification Request"; "device_verification_incoming_description_1" = "Verify this session to mark it as trusted. Trusting sessions of partners gives you extra peace of mind when using end-to-end encrypted messages."; "device_verification_incoming_description_2" = "Verifying this session will mark it as trusted, and also mark your session as trusted to the partner."; @@ -1562,9 +1571,11 @@ Tap the + to start adding people."; "device_verification_self_verify_wait_new_sign_in_title" = "Verify this login"; "device_verification_self_verify_wait_information" = "Verify this session from one of your other sessions, granting it access to encrypted messages.\n\nUse the latest %@ on your other devices:"; "device_verification_self_verify_wait_additional_information" = "This works with %@ and other cross-signing capable Matrix clients."; +"device_verification_self_verify_open_on_other_device_title" = "Open %@ on your other device"; +"device_verification_self_verify_open_on_other_device_information" = "You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions."; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Use Security Key"; "device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Use Security Phrase or Key"; -"device_verification_self_verify_wait_recover_secrets_additional_information" = "If you can't access an existing session"; +"device_verification_self_verify_wait_recover_secrets_additional_help" = "Can't access an existing %@ session?"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Checking for other verification capabilities ..."; // MARK: Verify @@ -1740,6 +1751,12 @@ Tap the + to start adding people."; "key_verification_verify_qr_code_scan_other_code_success_title" = "Code validated!"; "key_verification_verify_qr_code_scan_other_code_success_message" = "QR code has been successfully validated."; +"key_verification_scan_qr_code_title" = "Scan QR code"; +"key_verification_scan_qr_code_information_other_user" = "Point your camera at the QR code displayed on their device to verify their session"; +"key_verification_scan_qr_code_information_other_device" = "Point your camera at the QR code displayed on your other device to verify this session"; +"key_verification_scan_qr_code_information_other_session" = "Point your camera at the QR code displayed on your other device to verify your session"; +"key_verification_scan_qr_code_information_new_session" = "Point your camera at the QR code displayed on your other device to verify your new session"; + // MARK: Scan confirmation // Scanning @@ -1974,6 +1991,7 @@ Tap the + to start adding people."; // MARK: - Launch loading +"launch_loading_migrating_data" = "Migrating data\n%@ %%"; "launch_loading_server_syncing" = "Syncing with the server"; "launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)"; "launch_loading_processing_response" = "Processing data\n%@ %%"; @@ -1993,6 +2011,7 @@ Tap the + to start adding people."; "home_context_menu_normal_priority" = "Normal priority"; "home_context_menu_leave" = "Leave"; "home_context_menu_mark_as_read" = "Mark as read"; +"home_context_menu_mark_as_unread" = "Mark as unread"; "home_syncing" = "Syncing"; // MARK: - Favourites @@ -2011,12 +2030,12 @@ Tap the + to start adding people."; "share_invite_link_room_text" = "Hey, join this room on %@"; "share_invite_link_space_text" = "Hey, join this space on %@"; -// Mark: - Room avatar view +// MARK: - Room avatar view "room_avatar_view_accessibility_label" = "avatar"; "room_avatar_view_accessibility_hint" = "Change room avatar"; -// Mark: - Room creation introduction cell +// MARK: - Room creation introduction cell "room_intro_cell_add_participants_action" = "Add people"; @@ -2033,7 +2052,7 @@ Tap the + to start adding people."; "room_intro_cell_information_dm_sentence2" = "Only the two of you are in this conversation, no one else can join."; "room_intro_cell_information_multiple_dm_sentence2" = "Only you are in this conversation, unless any of you invites someone to join."; -// Mark: - Room invite +// MARK: - Room invite "room_invite_to_space_option_title" = "To %@"; "room_invite_to_space_option_detail" = "They can explore %@, but won’t be a member of %@."; @@ -2042,7 +2061,7 @@ Tap the + to start adding people."; "room_invite_not_enough_permission" = "You do not have permission to invite people to this room"; "space_invite_not_enough_permission" = "You do not have permission to invite people to this space"; -// Mark: - Spaces +// MARK: - Spaces "space_feature_unavailable_title" = "Spaces aren’t here yet"; "space_feature_unavailable_subtitle" = "Spaces aren't on iOS yet, but you can use them now on Web and Desktop"; @@ -2099,7 +2118,7 @@ Tap the + to start adding people."; "spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer."; -// Mark: - Space Creation +// MARK: - Space Creation "spaces_creation_hint" = "Spaces are a new way to group rooms and people."; "spaces_creation_visibility_title" = "What type of space do you want to create?"; @@ -2158,7 +2177,7 @@ Tap the + to start adding people."; "spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; -// Mark: Leave space +// MARK: Leave space "leave_space_action" = "Leave space"; "leave_space_and_one_room" = "Leave space and 1 room"; @@ -2171,17 +2190,17 @@ Tap the + to start adding people."; "room_event_action_reaction_more" = "%@ more"; -// Mark: Avatar +// MARK: Avatar "space_avatar_view_accessibility_label" = "avatar"; "space_avatar_view_accessibility_hint" = "Change space avatar"; -// Mark: - User avatar view +// MARK: - User avatar view "user_avatar_view_accessibility_label" = "avatar"; "user_avatar_view_accessibility_hint" = "Change user avatar"; -// Mark: - Side menu +// MARK: - Side menu "side_menu_reveal_action_accessibility_label" = "Left panel"; "side_menu_action_invite_friends" = "Invite friends"; @@ -2191,7 +2210,7 @@ Tap the + to start adding people."; "side_menu_app_version" = "Version %@"; "side_menu_coach_message" = "Swipe right or tap to see all rooms"; -// Mark: - Voice Messages +// MARK: - Voice Messages "voice_message_release_to_send" = "Hold to record, release to send"; "voice_message_remaining_recording_time" = "%@s left"; @@ -2200,12 +2219,13 @@ Tap the + to start adding people."; "voice_message_broadcast_in_progress_title" = "Can't start voice message"; "voice_message_broadcast_in_progress_message" = "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message"; -// Mark: - Voice broadcast +// MARK: - Voice Broadcast "voice_broadcast_unauthorized_title" = "Can't start a new voice broadcast"; "voice_broadcast_permission_denied_message" = "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions."; "voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one."; "voice_broadcast_already_in_progress_message" = "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; +"voice_broadcast_playback_lock_screen_placeholder" = "Voice broadcast"; "voice_broadcast_live" = "Live"; "voice_broadcast_tile" = "Voice broadcast"; "voice_broadcast_time_left" = "%@ left"; @@ -2213,8 +2233,14 @@ Tap the + to start adding people."; "voice_broadcast_stop_alert_title" = "Stop live broadcasting?"; "voice_broadcast_stop_alert_description" = "Are you sure you want to stop your live broadcast? This will end the broadcast, and the full recording will be available in the room."; "voice_broadcast_stop_alert_agree_button" = "Yes, stop"; +"voice_broadcast_voip_cannot_start_title" = "Can’t start a call"; +"voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call."; +"voice_broadcast_connection_error_title" = "Connection error"; +"voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later."; +"voice_broadcast_recorder_connection_error" = "Connection error - Recording paused"; +"voice_broadcast_playback_unable_to_decrypt" = "Unable to decrypt this voice broadcast."; -// Mark: - Version check +// MARK: - Version check "version_check_banner_title_supported" = "We’re ending support for iOS %@"; "version_check_banner_subtitle_supported" = "We will soon be ending support for %@ on iOS %@. To continue using %@ to its full potential, we advise you to upgrade your version of iOS."; @@ -2230,7 +2256,7 @@ Tap the + to start adding people."; "version_check_modal_subtitle_deprecated" = "We've been working on enhancing %@ for a faster and more polished experience. Unfortunately your current version of iOS is not compatible with some of those fixes and is no longer supported.\nWe're advising you to upgrade your operating system to use %@ to its full potential."; "version_check_modal_action_title_deprecated" = "Find out how"; -// Mark: - All Chats +// MARK: - All Chats "all_chats_title" = "All chats"; "all_chats_section_title" = "Chats"; @@ -2265,21 +2291,15 @@ Tap the + to start adding people."; "all_chats_edit_menu_leave_space" = "Leave %@"; "all_chats_edit_menu_space_settings" = "Space settings"; -"all_chats_onboarding_page_title1" = "Welcome to a new view!"; -"all_chats_onboarding_page_message1" = "To simplify your Element, tabs are now optional. Manage them using the top-right menu."; -"all_chats_onboarding_page_title2" = "Access Spaces"; -"all_chats_onboarding_page_message2" = "Access your Spaces (bottom-left) faster and easier than ever before."; -"all_chats_onboarding_page_title3" = "Give Feedback"; -"all_chats_onboarding_page_message3" = "Tap your profile to let us know what you think."; -"all_chats_onboarding_title" = "What's new"; -"all_chats_onboarding_try_it" = "Try it out"; - -// Mark: - Room invites +// MARK: - Room invites "room_invites_empty_view_title" = "Nothing new."; "room_invites_empty_view_information" = "This is where your invites appear."; -// Mark: - Space Selector +"room_waiting_other_participants_title" = "Waiting for users to join %@"; +"room_waiting_other_participants_message" = "Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted"; + +// MARK: - Space Selector "space_selector_title" = "My spaces"; "space_selector_empty_view_title" = "No spaces yet."; @@ -2289,7 +2309,21 @@ Tap the + to start adding people."; "space_detail_nav_title" = "Space detail"; "space_invite_nav_title" = "Space invite"; -// Mark: - Polls +// MARK: - Polls history + +"poll_history_title" = "Poll history"; +"poll_history_loading_text" = "Displaying polls"; +"poll_history_active_segment_title" = "Active polls"; +"poll_history_past_segment_title" = "Past polls"; +"poll_history_no_active_poll_text" = "There are no active polls in this room"; +"poll_history_no_past_poll_text" = "There are no past polls in this room"; +"poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months"; +"poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months"; +"poll_history_detail_view_in_timeline" = "View poll in timeline"; +"poll_history_load_more" = "Load more polls"; +"poll_history_fetching_error" = "Error fetching polls."; + +// MARK: - Polls "poll_edit_form_create_poll" = "Create poll"; @@ -2353,6 +2387,8 @@ Tap the + to start adding people."; "poll_timeline_ended_text" = "Ended the poll"; +"poll_timeline_reply_ended_poll" = "Ended poll"; + // MARK: - Location sharing "location_sharing_title" = "Location"; @@ -2538,7 +2574,7 @@ To enable access, tap Settings> Location and select Always"; "user_session_overview_session_details_button_title" = "Session details"; -// Mark: - WYSIWYG Composer +// MARK: - WYSIWYG Composer // Send Media Actions "wysiwyg_composer_start_action_media_picker" = "Photo Library"; @@ -2557,6 +2593,12 @@ To enable access, tap Settings> Location and select Always"; "wysiwyg_composer_format_action_strikethrough" = "Apply underline format"; "wysiwyg_composer_format_action_link" = "Apply link format"; "wysiwyg_composer_format_action_inline_code" = "Apply inline code format"; +"wysiwyg_composer_format_action_unordered_list" = "Toggle bulleted list"; +"wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list"; +"wysiwyg_composer_format_action_code_block" = "Toggle code block"; +"wysiwyg_composer_format_action_quote" = "Toggle quote"; +"wysiwyg_composer_format_action_indent" = "Increase indentation"; +"wysiwyg_composer_format_action_un_indent" = "Decrease indentation"; // Links "wysiwyg_composer_link_action_text" = "Text"; diff --git a/Riot/Assets/et.lproj/Localizable.strings b/Riot/Assets/et.lproj/Localizable.strings index 9f1002800..b3a015217 100644 --- a/Riot/Assets/et.lproj/Localizable.strings +++ b/Riot/Assets/et.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ jagas oma asukohta"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ alustas ringhäälingukõnet"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index d84b26c76..f9eb1b318 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1473,7 +1473,7 @@ // Mark: - Polls -"poll_edit_form_create_poll" = "Koosta üks küsitlus"; +"poll_edit_form_create_poll" = "Loo selline küsitlus"; "settings_discovery_accept_terms" = "Nõustu isikutuvastusserveri tingimustega"; "poll_timeline_not_closed_action" = "Sobib"; "poll_timeline_not_closed_subtitle" = "Palun proovi uuesti"; @@ -1548,9 +1548,9 @@ // Onboarding "onboarding_splash_register_button_title" = "Loo kasutajakonto"; "poll_edit_form_poll_type_closed_description" = "Tulemusi kuvame vaid siis, kui küsitlus on lõppenud"; -"poll_edit_form_poll_type_closed" = "Küsitlus on lõppenud"; +"poll_edit_form_poll_type_closed" = "Suletud valikutega küsitlus"; "poll_edit_form_poll_type_open_description" = "Osalejad näevad tulemusi peale oma valiku salvestamist"; -"poll_edit_form_poll_type_open" = "Ava küsitlus"; +"poll_edit_form_poll_type_open" = "Avatud valikutega küsitlus"; "poll_edit_form_update_failure_subtitle" = "Palun proovi uuesti"; "poll_edit_form_update_failure_title" = "Küsitluse muutmine ei õnnestunud"; "poll_edit_form_poll_type" = "Küsitluse tüüp"; @@ -2417,14 +2417,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Uut teavet ei leidu."; -"all_chats_onboarding_try_it" = "Proovi nüüd"; -"all_chats_onboarding_title" = "Mida on meil uut"; -"all_chats_onboarding_page_message3" = "Kui soovid meile teada anda oma arvamustest, siis klõpsi oma profiili ikooni."; -"all_chats_onboarding_page_title3" = "Jaga tagasisidet"; -"all_chats_onboarding_page_message2" = "Kogukonnad leiad alt vasakult kiiremini ja lihtsamini, kui varem."; -"all_chats_onboarding_page_title2" = "Ligipääs kogukondadele"; -"all_chats_onboarding_page_message1" = "Et Element'i kasutamine oleks lihtsam, siis kaardid on nüüd valikulised. Neid saad hallata ülal paremal avanevast menüüst."; -"all_chats_onboarding_page_title1" = "Meie liidesel on nüüd uus vaade!"; "all_chats_nothing_found_placeholder_message" = "Proovi muuta oma otsingut."; "all_chats_nothing_found_placeholder_title" = "Mitte midagi ei leidu."; "all_chats_empty_unreads_placeholder_message" = "Kui sul on lugemata sõnumeid, siis nad on siit leitavad."; @@ -2640,3 +2632,45 @@ "poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata"; "voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu"; "voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne"; +"poll_timeline_ended_text" = "Küsitlus on lõppenud"; +"voice_broadcast_voip_cannot_start_description" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne."; +"voice_broadcast_voip_cannot_start_title" = "Kõne algatamine ei õnnestu"; +"poll_history_no_past_poll_text" = "Selles jututoas pole varasemaid küsitlusi"; +"poll_history_no_active_poll_text" = "Selles jututoas pole käimasolevaid küsitlusi"; +"poll_history_past_segment_title" = "Varasemad küsitlused"; +"poll_history_active_segment_title" = "Käimasolevad küsitlused"; + +// MARK: - Polls history + +"poll_history_title" = "Küsitluste ajalugu"; +"room_details_polls" = "Küsitluste ajalugu"; +"accessibility_selected" = "valitud"; +"voice_broadcast_playback_lock_screen_placeholder" = "Ringhäälingukõne"; +"voice_broadcast_connection_error_message" = "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti."; +"voice_broadcast_connection_error_title" = "Ühenduse viga"; +"wysiwyg_composer_format_action_quote" = "Lülita tsiteerimine sisse/välja"; +"wysiwyg_composer_format_action_code_block" = "Lülita koodiblokk sisse/välja"; +"wysiwyg_composer_format_action_ordered_list" = "Lülita nummerdatud loend sisse/välja"; +"wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja"; +"voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud"; +"poll_timeline_reply_ended_poll" = "Lõppenud küsitlus"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine (väljalülitamiseks pead välja logima)"; +"settings_labs_confirm_crypto_sdk" = "Palun arvesta, et see funktsionaalsus on alles katseline ja ei pruugi toimida eesmärgipäraselt. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Jätka ettevaatlikult ja omal äranägemisel."; +"settings_labs_enable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine"; +"poll_history_load_more" = "Laadi veel küsitlusi"; +"poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; +"poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; +"poll_history_loading_text" = "Küsitluste kuvamise ootel"; +"poll_history_fetching_error" = "Viga küsitluste laadimisel."; +"key_backup_recover_from_private_key_progress" = "%@%% tehtud"; +"voice_broadcast_playback_unable_to_decrypt" = "Selle ringhäälingukõne dekrüptimine ei õnnestu."; +"home_context_menu_mark_as_unread" = "Märgi mitteloetuks"; +"wysiwyg_composer_format_action_un_indent" = "Vähenda taandrida"; +"wysiwyg_composer_format_action_indent" = "Suurenda taandrida"; +"settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada."; +"poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel"; +"authentication_qr_login_failure_device_not_supported" = "Sidumine selle seadmega ei ole toetatud."; diff --git a/Riot/Assets/fa.lproj/InfoPlist.strings b/Riot/Assets/fa.lproj/InfoPlist.strings index 8b1378917..c53594b36 100644 --- a/Riot/Assets/fa.lproj/InfoPlist.strings +++ b/Riot/Assets/fa.lproj/InfoPlist.strings @@ -1 +1,11 @@ + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد."; +"NSLocationWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد."; +"NSFaceIDUsageDescription" = "برای دسترسی به برنامه تان، از face Id استفاده میشود."; +"NSCalendarsUsageDescription" = "ملاقات های برنامه ریزی شده خود را در برنامه ببینید."; +"NSContactsUsageDescription" = "برای یافتن مخاطبانتان در ماتریکس، اینها را با سرور هویت شما به اشتراک خواهیم گذاشت."; +"NSMicrophoneUsageDescription" = "المنت برای ضبط صدا، فیلم برداری و ارسال پیام صوتی، دسترسی به میکروفون را نیاز دارد."; +"NSPhotoLibraryUsageDescription" = "برای انتخاب و آپلود تصاویر و ویدیو ها از گالری خود، اجازه دسترسی به گالری را بدهید."; +// Permissions usage explanations +"NSCameraUsageDescription" = "دوربین برای فیلم و تصویر برداری و آپلود آنها استفاده میشود."; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index fe51dcd78..a7f7b720c 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -1290,3 +1290,59 @@ "stop" = "توقف"; "joining" = "پیوستن"; "enable" = "فعال"; +"authentication_server_selection_generic_error" = "در این آدرس سروری نیست! لطفا صحت آن را بررسی کنید."; +"authentication_server_selection_server_url" = "آدرس هوم سرور"; +"authentication_server_selection_register_message" = "آدرس سرورتان چیست؟ این آدرس ذخیره سازی اطلاعات شماست"; +"authentication_server_selection_register_title" = "هوم سرور خود را انتخاب کنید"; +"authentication_server_selection_login_message" = "آدرس سرورتان چیست؟"; +"authentication_server_selection_login_title" = "اتصال به هوم سرور"; +"authentication_login_with_qr" = "ورود با QR کد"; +"authentication_server_info_title_login" = "جایی که مکالماتتان قرار میگیرند"; +"authentication_login_forgot_password" = "فراموشی رمز عبور"; +"authentication_login_username" = "نام کاربری، ایمیل، یا شماره تلفن"; +"authentication_login_title" = "خوش برگشتید!"; +"authentication_server_info_title" = "جایی که مکالماتتان قرار میگیرند"; +"authentication_registration_password_footer" = "باید 8 حرف یا بیشتر باشد"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "بقیه میتوانند شما را پیدا کنند %@"; +"authentication_registration_username_footer" = "نمیتوانید بعدا تغییرش دهید"; +"authentication_registration_username" = "نام کاربری"; + +// MARK: Authentication +"authentication_registration_title" = "حسابتان را بسازید"; +"onboarding_celebration_button" = "بزن بریم"; +"onboarding_celebration_message" = "برای ویرایش پروفایلتان، به تنظیمات بروید"; +"onboarding_celebration_title" = "خوب به نظر میرسد!"; +"onboarding_avatar_accessibility_label" = "تصویر پروفایل"; +"onboarding_avatar_message" = "زمان آن رسیده که به نامتان، تصویر اضافه کنید"; +"onboarding_avatar_title" = "یک عکس پروفایل اضافه کنید"; +"onboarding_display_name_max_length" = "نام نمایشی شما باید کمتر از 256 حرف باشد"; +"onboarding_display_name_hint" = "میتواند بعدا آن را تغییر دهید"; +"onboarding_display_name_placeholder" = "نام نمایشی"; +"onboarding_display_name_message" = "این نام هنگام ارسال پیام ها نمایش داده میشود."; +"onboarding_display_name_title" = "یک نام نمایشی انتخاب کنید"; +"onboarding_personalization_skip" = "این مرحله را رد کن"; +"onboarding_personalization_save" = "ذخیره و ادامه"; +"onboarding_congratulations_home_button" = "مرا به خانه ببر"; +"onboarding_congratulations_personalize_button" = "شخصی سازی پروفایل"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "حسابتان %@ ایجاد شد"; +"onboarding_congratulations_title" = "تبریک!"; +"onboarding_use_case_existing_server_button" = "اتصال به سرور"; +"onboarding_use_case_existing_server_message" = "دنبال اتصال به یک سرور موجود هستید؟"; +"onboarding_use_case_skip_button" = "این سوال را رد کن"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "هنوز مطمئن نیستید؟ %@"; +"onboarding_use_case_community_messaging" = "اجتماعات"; +"onboarding_use_case_work_messaging" = "تیم ها"; +"onboarding_use_case_personal_messaging" = "خانواده و دوستان"; +"onboarding_use_case_message" = "ما به شما کمک میکنیم که متصل شوید"; +"onboarding_use_case_title" = "با چه کسانی بیشتر چت میکنید؟"; +"onboarding_splash_page_4_message" = "المنت برای محیط های شغلی عالی است چرا که توسط امن ترین سازمان های جهانی، استفاده میشود."; +"onboarding_splash_page_4_title_no_pun" = "ارسال پیام بین اعضای تیمتان."; +"onboarding_splash_page_3_message" = "رمزنگاری کامل بدون نیاز به شماره تلفن، بدون وجود تبلیغات و دیتاکاوی."; +"onboarding_splash_page_3_title" = "پیام رسانی امن."; +"onboarding_splash_page_2_message" = "انتخاب مکان ذخیره سازی پیام هایتان، برایتان کنترل و استقلال را از طریق اتصال به ماتریکس به ارمغان می‌آورد."; +"onboarding_splash_page_2_title" = "تحت کنترل شماست."; +"onboarding_splash_page_1_message" = "یک ارتباط امن و مستقل که سطح حریم شخصی آن دقیقا مشابه ارتباط رو در رو در منزل شماست."; +"accessibility_selected" = "انتخاب شده"; diff --git a/Riot/Assets/fr.lproj/Localizable.strings b/Riot/Assets/fr.lproj/Localizable.strings index 64f1ed513..aae0fdf79 100644 --- a/Riot/Assets/fr.lproj/Localizable.strings +++ b/Riot/Assets/fr.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ a partagé sa localisation"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ a lancé une diffusion vocale"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index ed4a3820f..4767e596c 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -80,7 +80,7 @@ "auth_reset_password_success_message" = "Le mot de passe de votre compte Matrix a été réinitialisé.\n\nVous avez été déconnecté de toutes vos sessions et ne recevrez plus de notifications. Pour réactiver les notifications, reconnectez-vous sur chaque appareil."; "auth_add_email_and_phone_warning" = "L’inscription avec un e-mail et un numéro de téléphone à la fois n’est pas prise en charge tant que l’API n'existe pas. Seul votre numéro de téléphone sera pris en compte. Vous pourrez ajouter l’adresse e-mail dans vos options de profil."; // Chat creation -"room_creation_title" = "Nouvelle discussion"; +"room_creation_title" = "Nouveau message direct"; "room_creation_account" = "Compte"; "room_creation_appearance" = "Apparence"; "room_creation_appearance_name" = "Nom"; @@ -103,7 +103,7 @@ "room_recents_no_conversation" = "Aucun salon"; "room_recents_low_priority_section" = "PRIORITÉ BASSE"; "room_recents_invites_section" = "INVITATIONS"; -"room_recents_start_chat_with" = "Commencer une discussion"; +"room_recents_start_chat_with" = "Nouveau message direct"; "room_recents_create_empty_room" = "Créer un salon"; "room_recents_join_room" = "Rejoindre le salon"; "room_recents_join_room_title" = "Rejoindre un salon"; @@ -111,7 +111,7 @@ // People tab "people_invites_section" = "INVITATIONS"; "people_conversation_section" = "DISCUSSIONS"; -"people_no_conversation" = "Aucune discussion"; +"people_no_conversation" = "Aucun message direct"; // Rooms tab "room_directory_no_public_room" = "Aucun salon public disponible"; // Groups tab @@ -166,20 +166,20 @@ "room_participants_now" = "maintenant"; "room_participants_ago" = "d’inactivité"; "room_participants_action_section_admin_tools" = "Outils d’administration"; -"room_participants_action_section_direct_chats" = "Conversations privées"; +"room_participants_action_section_direct_chats" = "Messages directs"; "room_participants_action_section_devices" = "Sessions"; "room_participants_action_section_other" = "Options"; "room_participants_action_invite" = "Inviter"; "room_participants_action_leave" = "Quitter ce salon"; "room_participants_action_remove" = "Exclure de ce salon"; -"room_participants_action_ban" = "Bannir de ce salon"; +"room_participants_action_ban" = "Interdire l’accès au salon (définitif)"; "room_participants_action_unban" = "Révoquer le bannissement"; "room_participants_action_ignore" = "Masquer tous les messages de cet utilisateur"; "room_participants_action_unignore" = "Afficher tous les messages de cet utilisateur"; "room_participants_action_set_default_power_level" = "Rétrograder en utilisateur normal"; "room_participants_action_set_moderator" = "Nommer modérateur"; "room_participants_action_set_admin" = "Nommer administrateur"; -"room_participants_action_start_new_chat" = "Commencer une nouvelle discussion"; +"room_participants_action_start_new_chat" = "Nouveau message direct"; "room_participants_action_start_voice_call" = "Commencer un appel audio"; "room_participants_action_start_video_call" = "Commencer un appel vidéo"; "room_participants_action_mention" = "Mentionner"; @@ -314,7 +314,7 @@ "room_details_favourite_tag" = "Favoris"; "room_details_low_priority_tag" = "Priorité basse"; "room_details_mute_notifs" = "Désactiver les notifications"; -"room_details_direct_chat" = "Discussion directe"; +"room_details_direct_chat" = "Message direct"; "room_details_access_section" = "Qui peut accéder à ce salon ?"; "room_details_access_section_invited_only" = "Seules les personnes qui ont été invitées"; "room_details_access_section_anyone_apart_from_guest" = "Tous ceux qui connaissent le lien du salon, à part les visiteurs"; @@ -1723,7 +1723,7 @@ "set_default_power_level" = "Réinitialiser le rang"; "set_moderator" = "Nommer modérateur"; "set_admin" = "Nommer administrateur"; -"start_chat" = "Nouvelle conversation privée"; +"start_chat" = "Nouveau message direct"; "start_voice_call" = "Commencer un appel audio"; "start_video_call" = "Commencer un appel vidéo"; "mention" = "Mentionner"; @@ -1959,8 +1959,8 @@ "membership_ban" = "Banni"; "num_members_one" = "%@ utilisateur"; "num_members_other" = "%@ utilisateurs"; -"kick" = "Expulser"; -"ban" = "Bannir"; +"kick" = "Retirer du salon (réversible)"; +"ban" = "Interdire l’accès au salon (définitif)"; "unban" = "Révoquer le bannissement"; "message_unsaved_changes" = "Il y a des modifications non enregistrées. Quitter les annulera."; // Login Screen @@ -2440,7 +2440,6 @@ "room_access_space_chooser_other_spaces_section_info" = "Ce sont probablement des choses auxquelles les autres admins de %@ participent."; "authentication_choose_password_not_verified_message" = "Vérifiez votre boîte de réception"; "authentication_choose_password_not_verified_title" = "Email non vérifié"; -"all_chats_onboarding_page_title3" = "Donner mon avis"; // MARK: User sessions management @@ -2460,20 +2459,13 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Rien de neuf."; -"all_chats_onboarding_try_it" = "Essayez"; -"all_chats_onboarding_title" = "Quoi de neuf"; -"all_chats_onboarding_page_message3" = "Appuyez sur votre profil pour nous faire vos retours."; -"all_chats_onboarding_page_message2" = "Accédez à vos espaces (en bas à gauche) plus rapidement et facilement qu’avant."; -"all_chats_onboarding_page_title2" = "Accéder aux espaces"; -"all_chats_onboarding_page_message1" = "Pour simplifier Element, les onglets sont désormais facultatifs. Gérez les depuis le menu en haut à droite."; -"all_chats_onboarding_page_title1" = "Bienvenu dans une nouvelle vue !"; "all_chats_edit_menu_space_settings" = "Paramètres de l’espace"; "all_chats_edit_menu_leave_space" = "Quitter %@"; "all_chats_user_menu_settings" = "Paramètres utilisateur"; "room_recents_recently_viewed_section" = "Récemment vus"; "all_chats_nothing_found_placeholder_message" = "Essayez d’affiner votre recherche."; "all_chats_nothing_found_placeholder_title" = "Aucun résultat."; -"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non-lus s’afficheront lorsque vous en aurez."; +"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non lus s’afficheront lorsque vous en aurez."; "all_chats_empty_list_placeholder_title" = "Plus rien à voir."; "all_chats_empty_view_information" = "La messagerie sécurisée tout en un pour les équipes, les amis, et les organisations. Créez une discussion ou rejoignez un salon pour démarrer."; "all_chats_empty_space_information" = "Les espaces sont un nouveau moyen de grouper les salons et les gens. Ajoutez un salon, ou créez en un nouveau à l’aide du bouton en bas à droite."; @@ -2489,14 +2481,14 @@ "all_chats_edit_layout_add_filters_title" = "Filtrez vos messages"; "all_chats_edit_layout_add_section_message" = "Épinglez des sections à l’accueil pour y accéder plus rapidement"; "all_chats_edit_layout_add_section_title" = "Ajouter une section à l’accueil"; -"all_chats_edit_layout_unreads" = "Non-lus"; +"all_chats_edit_layout_unreads" = "Non lus"; "all_chats_edit_layout_recents" = "Récents"; "all_chats_edit_layout" = "Préférences d’agencement"; "all_chats_section_title" = "Discussions"; // Mark: - All Chats -"all_chats_title" = "Tous mes chats"; +"all_chats_title" = "Accueil"; "spaces_subspace_creation_visibility_message" = "L’espace créé sera ajouté à %@."; "spaces_subspace_creation_visibility_title" = "Quel type de sous-espace voulez-vous créer ?"; "spaces_explore_rooms_format" = "Parcourir %@"; @@ -2537,7 +2529,7 @@ "device_name_desktop" = "%@ Bureau"; "user_inactive_session_item_with_date" = "Inactif depuis 90 jours ou plus (%@)"; "user_inactive_session_item" = "Inactif depuis 90 jours ou plus"; -"user_session_item_details" = "%@ · Dernière activité %@"; +"user_session_item_details" = "%1$@ · %2$@"; // First item is client name and second item is session display name "user_session_name" = "%@ : %@"; @@ -2614,3 +2606,136 @@ "manage_session_name_info" = "Gardez en tête que les noms des sessions sont aussi visibles par les personnes avec qui vous communiquez. %@"; "manage_session_name_hint" = "Personnaliser les noms des sessions peut vous aider à reconnaître vos appareils plus facilement."; "settings_labs_enable_wysiwyg_composer" = "Essayez le compositeur de messages visuel"; +"settings_labs_enable_voice_broadcast" = "Diffusion vocale"; +"wysiwyg_composer_format_action_un_indent" = "Diminuer le retrait"; +"wysiwyg_composer_format_action_indent" = "Augmenter le retrait"; +"wysiwyg_composer_format_action_code_block" = "Bloc de code"; +"wysiwyg_composer_start_action_stickers" = "Autocollants"; +"user_session_rename_session_title" = "Renommer les sessions"; +"user_session_verified_session_description" = "Les sessions vérifiées sont toutes celles où vous vous êtes connecté à Element grâce à vos identifiants ou celles pour lesquelles vous avez confirmé votre identité à l'aide d'une autre session.\n\nCela signifie que vous êtes en possession de toutes les clés requises pour déchiffrer vos messages et montrer aux autres utilisateurs que vous faites confiance à cette session."; +"poll_history_loading_text" = "Afficher les sondages"; +"voice_message_broadcast_in_progress_title" = "Impossible de démarrer l'enregistrement vocal"; +"home_context_menu_mark_as_unread" = "Marquer comme non lu"; +"launch_loading_processing_response" = "Traitement des données\n%@ %%"; +"notice_voice_broadcast_ended_by_you" = "Vous avez terminé une diffusion vocale."; +"notice_voice_broadcast_ended" = "%@ a terminé une diffusion vocale."; +"notice_voice_broadcast_live" = "Diffusion en direct"; +"deselect_all" = "Tout désélectionner"; +"wysiwyg_composer_link_action_edit_title" = "Modifier le lien"; +"wysiwyg_composer_link_action_create_title" = "Créer un lien"; +"wysiwyg_composer_link_action_link" = "Lien"; + +// Links +"wysiwyg_composer_link_action_text" = "Texte"; +"wysiwyg_composer_format_action_quote" = "Citation"; +"wysiwyg_composer_format_action_ordered_list" = "Liste numérique"; +"wysiwyg_composer_format_action_unordered_list" = "Liste à puces"; +"wysiwyg_composer_format_action_inline_code" = "Formater comme code informatique"; +"wysiwyg_composer_format_action_link" = "Formater comme lien"; +"wysiwyg_composer_format_action_strikethrough" = "Souligner"; +"wysiwyg_composer_format_action_underline" = "Barrer"; +"wysiwyg_composer_format_action_italic" = "Mettre en italique"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Mettre en caractères gras"; +"wysiwyg_composer_start_action_voice_broadcast" = "Diffusion vocale"; +"wysiwyg_composer_start_action_text_formatting" = "Formatage du texte"; +"wysiwyg_composer_start_action_camera" = "Appareil photo"; +"wysiwyg_composer_start_action_location" = "Position"; +"wysiwyg_composer_start_action_polls" = "Sondages"; +"wysiwyg_composer_start_action_attachments" = "Pièces jointes"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Galerie photo"; +"user_session_details_last_activity" = "Dernière activité"; +"user_session_item_details_last_activity" = "Dernière activité %@"; +"user_other_session_menu_sign_out_sessions" = "Déconnecter %@ sessions"; +"user_other_session_selected_count" = "%@ sélectionnées"; +"user_other_session_menu_select_sessions" = "Sélectionnez des sessions"; +"user_other_session_clear_filter" = "Effacer les filtres"; +"user_other_session_no_unverified_sessions" = "Aucune session non vérifiée trouvée."; +"user_other_session_no_verified_sessions" = "Aucune session vérifiée trouvée."; +"user_other_session_no_inactive_sessions" = "Aucune session inactive trouvée."; +"user_other_session_filter_menu_inactive" = "Inactives"; +"user_other_session_filter_menu_unverified" = "Non vérifiées"; +"user_other_session_filter_menu_verified" = "Vérifiées"; +"user_other_session_filter_menu_all" = "Toutes les sessions"; +"user_other_session_filter" = "Filtrer"; +"user_other_session_verified_sessions_header_subtitle" = "Pour augmenter la sécurité, veuillez déconnecter toutes les sessions qui vous semblent inconnues ou que vous n'utilisez plus."; +"user_other_session_current_session_details" = "Votre session actuelle"; +"user_other_session_security_recommendation_title" = "Autres sessions"; +"user_session_rename_session_description" = "D'autres utilisateurs des conversations et salons que vous rejoignez peuvent consulter la liste complète de vos session.\n\nCela leur permet de confirmer qu'ils communiquent bien avec vous, mais cela signifie également qu'ils verront le nom que vous donnez à vos sessions."; +"user_session_inactive_session_description" = "Les sessions inactives sont celles qui n'ont pas été utilisées depuis un certain temps, mais qui continuent de recevoir des clés de chiffrement.\n\nÉliminer ces sessions inactives augmente la sécurité et les performances, et facilite l'identification de nouvelles connexions suspectes."; +"user_session_inactive_session_title" = "Sessions inactives"; +"user_session_permanently_unverified_session_description" = "Cette session de prend pas en charge le chiffrement et ne peut donc être vérifiée.\n\nVous ne pourrez pas intervenir dans les salons où le chiffrement est activé en utilisant cette session.\n\nPour une sécurité et confidentialité optimale, il est recommandé d'utiliser des clients Matrix qui prennent en charge le chiffrement."; +"user_session_unverified_session_description" = "Les sessions non vérifiez sont celles qui sont connectées avec vos identifiants, mais qui n'ont pas passé les vérifications croisées.\n\nVous devriez passer en revue ces sessions car elles pourraient témoigner d'un usage malicieux de votre compte."; +"user_session_unverified_session_title" = "Session non vérifiée"; +"user_session_verified_session_title" = "Sessions vérifiées"; +"user_session_got_it" = "Entendu"; +"user_other_session_verified_additional_info" = "Cette session est prête à l'échange de messages."; +"user_other_session_permanently_unverified_additional_info" = "Cette session ne prend pas en charge le chiffrement et ne peut donc être vérifiée."; +"user_other_session_unverified_additional_info" = "Vérifier ou déconnecter cette session pour une sécurité et une fiabilité accrue."; +"user_session_verification_unknown_additional_info" = "Vérifier la session actuelle pour révéler l'état de vérification de cette session."; +"user_session_verification_unknown_short" = "Inconnu"; +"user_session_verification_unknown" = "État de vérification inconnu"; +"user_sessions_hide_location_info" = "Masquer l'adresse IP"; +"user_sessions_show_location_info" = "Montrer l'adresse IP"; +"poll_timeline_reply_ended_poll" = "Sondage terminé"; +"poll_timeline_ended_text" = "Sondage clos"; +"poll_timeline_decryption_error" = "Des erreurs de déchiffrement pourrait empêcher certains votes d'être comptabilisés"; +"poll_history_fetching_error" = "Erreur au cours de la récupération des sondages."; +"poll_history_load_more" = "Charger plus de sondages"; +"poll_history_no_past_poll_period_text" = "Il n'y a pas eu de sondages les %@ derniers jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs"; +"poll_history_no_active_poll_period_text" = "Il n'y a pas eu de sondages depuis %@ jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs"; +"poll_history_detail_view_in_timeline" = "Consulter la chronologie des sondages"; +"poll_history_no_past_poll_text" = "Il n'y a pas de sondage précédent dans ce salon"; +"poll_history_no_active_poll_text" = "Il n'y a aucun sondage en cours dans ce salon"; +"poll_history_past_segment_title" = "Sondages précédents"; +"poll_history_active_segment_title" = "Sondages en cours"; + +// MARK: - Polls history + +"poll_history_title" = "Historique des sondages"; +"voice_broadcast_playback_unable_to_decrypt" = "Impossible de déchiffrer cette diffusion vocale."; +"voice_broadcast_recorder_connection_error" = "Erreur de connexion - Enregistrement interrompu"; +"voice_broadcast_connection_error_message" = "Nous sommes malheureusement dans l'impossibilité de démarrer un enregistrement maintenant. Veuillez réessayer plus tard."; +"voice_broadcast_connection_error_title" = "Erreur de connexion"; +"voice_broadcast_voip_cannot_start_description" = "Vous ne pouvez pas démarrer d'appel car vous enregistrez déjà une diffusion en direct. Veuillez interrompre votre diffusion pour lancer un appel."; +"voice_broadcast_voip_cannot_start_title" = "Impossible de démarrer l'appel"; +"voice_broadcast_stop_alert_agree_button" = "Oui, terminer"; +"voice_broadcast_stop_alert_description" = "Êtes vous sûr de vouloir interrompre votre diffusion vocale ? Cela mettra fin à la diffusion et rendra l'enregistrement disponible dans le salon."; +"voice_broadcast_stop_alert_title" = "Arrêter la diffusion vocale ?"; +"voice_broadcast_buffering" = "Mise en mémoire tampon..."; +"voice_broadcast_time_left" = "%@ restant"; +"voice_broadcast_tile" = "Diffusion vocale"; +"voice_broadcast_live" = "En direct"; +"voice_broadcast_playback_lock_screen_placeholder" = "Diffusion vocale"; +"voice_broadcast_playback_loading_error" = "Impossible de lire cette diffusion vocale."; +"voice_broadcast_blocked_by_someone_else_message" = "Quelqu'un d'autre est déjà en train d'enregistrer une diffusion vocale. Veuillez attendre la fin de la leur pour en démarrer une nouvelle."; +"voice_broadcast_already_in_progress_message" = "Vous êtes déjà en train d'enregistrer une diffusion vocale. Veuillez y mettre fin avant d'en démarrer une nouvelle."; +"voice_broadcast_permission_denied_message" = "Vous n'avez pas les autorisations nécessaires pour démarrer une diffusion vocal dans ce salon. Contactez un administrateur pour qu'il vous octroie la permission."; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "Impossible de démarrer une nouvelle diffusion vocale"; +"voice_message_broadcast_in_progress_message" = "Vous ne pouvez pas démarrer d'enregistrement vocal car vous diffusez en direct. Veuillez interrompre votre diffusion pour démarrer l'enregistrement vocal"; +"launch_loading_server_syncing_nth_attempt" = "Synchronisation avec le serveur\n(%@ tentatives)"; +"launch_loading_server_syncing" = "Synchronisation avec le serveur"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migration des données\n%@ %%"; +"key_backup_recover_from_private_key_progress" = "%@%% Fini"; +"room_details_polls" = "Historique des sondages"; +"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout avec Rust (se déconnecter pour désactiver)"; +"settings_labs_confirm_crypto_sdk" = "Cette option activera le nouveau moteur de chiffrement de bout en bout, plus rapide et plus fiable, écrit en Rust. Une fois activé vous devrez vous déconnecter pour le désactiver. Voulez-vous continuer ?"; +"settings_labs_enable_crypto_sdk" = "Chiffrement de bout en bout en Rust"; +"settings_push_rules_error" = "Nous avons rencontré une erreur lors de la mise à jours de vos préférences de notification. Veuillez réactiver l'option."; +"password_policy_pwd_in_dict_error" = "Ce mot de passe a été trouvé dans un dictionnaire, et son usage n'est donc pas autorisé."; +"password_policy_weak_pwd_error" = "Ce mot de passe est trop faible. Il doit contenir au moins 8 caractères, dont au moins une majuscule, une minuscule, un chiffre et un caractère spécial."; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "Mot de passe trop court"; +"accessibility_selected" = "sélectionné"; diff --git a/Riot/Assets/hu.lproj/Localizable.strings b/Riot/Assets/hu.lproj/Localizable.strings index 82161237e..fee155510 100644 --- a/Riot/Assets/hu.lproj/Localizable.strings +++ b/Riot/Assets/hu.lproj/Localizable.strings @@ -120,3 +120,6 @@ /* New video message from a specific person, not referencing a room. */ "VIDEO_FROM_USER" = "%@ videót küldött"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ hang közvetítést indított"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 53debbedb..10761d695 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2467,14 +2467,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Semmi új."; -"all_chats_onboarding_try_it" = "Próbáld ki"; -"all_chats_onboarding_title" = "Újdonságok"; -"all_chats_onboarding_page_message3" = "Koppints a profilodra és mond el mit gondolsz."; -"all_chats_onboarding_page_title3" = "Visszajelzés adása"; -"all_chats_onboarding_page_message2" = "A terekhez való hozzáférés (balra lent) gyorsabb és egyszerűbb mint valaha."; -"all_chats_onboarding_page_title2" = "Hozzáférés a terekhez"; -"all_chats_onboarding_page_message1" = "Element egyszerűsítéséhez a lapok mostantól választhatók. Beállítani a jobb felső menüből lehet."; -"all_chats_onboarding_page_title1" = "Üdv az új kinézetben!"; "all_chats_nothing_found_placeholder_message" = "Próbáld meg a keresést módosítani."; "all_chats_nothing_found_placeholder_title" = "Nincs találat."; "all_chats_empty_unreads_placeholder_message" = "Ez az a hely ahol az olvasatlan üzeneteid megjelennek, ha lesznek."; @@ -2688,3 +2680,45 @@ "poll_timeline_decryption_error" = "Visszafejtési hibák miatt néhány szavazat nem kerül beszámításra"; "voice_message_broadcast_in_progress_message" = "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához"; "voice_message_broadcast_in_progress_title" = "Hang üzenetet nem lehet elindítani"; +"poll_timeline_ended_text" = "Szavazás vége"; +"voice_broadcast_voip_cannot_start_description" = "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához."; +"voice_broadcast_voip_cannot_start_title" = "Nem sikerült hívást indítani"; +"poll_history_no_past_poll_text" = "Nincsenek régi szavazások ebben a szobában"; +"poll_history_no_active_poll_text" = "Nincsenek aktív szavazások ebben a szobában"; +"poll_history_past_segment_title" = "Régi szavazások"; +"poll_history_active_segment_title" = "Aktív szavazások"; + +// MARK: - Polls history + +"poll_history_title" = "Szavazás alakulása"; +"room_details_polls" = "Szavazás alakulása"; +"accessibility_selected" = "kiválasztva"; +"wysiwyg_composer_format_action_quote" = "Idézet be/ki"; +"wysiwyg_composer_format_action_code_block" = "Kód blokk be/ki"; +"wysiwyg_composer_format_action_ordered_list" = "Számozott lista ki-,bekapcsolása"; +"wysiwyg_composer_format_action_unordered_list" = "Lista ki-,bekapcsolása"; +"poll_timeline_reply_ended_poll" = "Lezárt szavazások"; +"voice_broadcast_recorder_connection_error" = "Kapcsolódási hiba – Felvétel szüneteltetve"; +"voice_broadcast_connection_error_message" = "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később."; +"voice_broadcast_connection_error_title" = "Kapcsolat hiba"; +"voice_broadcast_playback_lock_screen_placeholder" = "Hang közvetítés"; +"poll_history_load_more" = "Még több szavazás betöltése"; +"poll_history_no_past_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; +"poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; +"poll_history_loading_text" = "Szavazások megjelenítése"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Adatok migrálása\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Rust végpontok közötti titkosítás (kikapcsoláshoz kijelentkezés szükséges)"; +"settings_labs_confirm_crypto_sdk" = "Ez a funkció még kísérleti fázisban van. Lehet, hogy nem az elvártnak megfelelően fog működni és előre nem látható következménye lehet. A funkció kikapcsolásához egyszerű ki-, és bejelentkezés szükséges. Használata csak saját felelősségre."; +"settings_labs_enable_crypto_sdk" = "Rust végpontok közötti titkosítás"; +"home_context_menu_mark_as_unread" = "Olvasatlannak jelöl"; +"poll_history_fetching_error" = "Szavazás betöltési hiba."; +"voice_broadcast_playback_unable_to_decrypt" = "A hang közvetítés nem fejthető vissza."; +"key_backup_recover_from_private_key_progress" = "%@%% kész"; +"wysiwyg_composer_format_action_un_indent" = "Behúzás csökkentése"; +"wysiwyg_composer_format_action_indent" = "Behúzás növelése"; +"poll_history_detail_view_in_timeline" = "Szavazás megjelenítése az idővonalon"; +"settings_push_rules_error" = "Hiba történt az értesítések beállításának frissítésekor. Próbáld meg az beállítást újra átkapcsolni."; +"authentication_qr_login_failure_device_not_supported" = "Ezzel az eszközzel való összeköttetés nem támogatott."; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index d6449680c..b453818aa 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2672,14 +2672,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Belum ada yang baru."; -"all_chats_onboarding_try_it" = "Coba"; -"all_chats_onboarding_title" = "Apa yang baru"; -"all_chats_onboarding_page_message3" = "Ketuk profil Anda untuk memberi tahu kami bagaimana menurut Anda."; -"all_chats_onboarding_page_title3" = "Berikan Masukan"; -"all_chats_onboarding_page_message2" = "Akses Space Anda (di kiri bawah) dengan lebih cepat dan lebih mudah dari sebelumnya."; -"all_chats_onboarding_page_title2" = "Akses Space"; -"all_chats_onboarding_page_message1" = "Untuk membuat Element Anda lebih sederhana, fitur tab sekarang opsional. Kelola menggunakan menu kanan atas."; -"all_chats_onboarding_page_title1" = "Selamat datang di tampilan yang baru!"; "all_chats_nothing_found_placeholder_message" = "Coba atur pencarian Anda."; "all_chats_nothing_found_placeholder_title" = "Tidak ada yang ditemukan."; "all_chats_empty_unreads_placeholder_message" = "Ini di mana pesan Anda yang belum dibaca akan ditampilkan, ketika Anda menerimanya."; @@ -2893,5 +2885,47 @@ "notice_voice_broadcast_live" = "Siaran langsung"; "user_other_session_security_recommendation_title" = "Sesi lainnya"; "poll_timeline_decryption_error" = "Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung"; -"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara selagi Anda merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara"; +"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara"; "voice_message_broadcast_in_progress_title" = "Tidak dapat memulai pesan suara"; +"poll_timeline_ended_text" = "Mengakhiri pemungutan suara"; +"voice_broadcast_voip_cannot_start_description" = "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan."; +"voice_broadcast_voip_cannot_start_title" = "Tidak dapat memulai sebuah panggilan"; +"poll_history_no_past_poll_text" = "Tidak ada pemungutan suara masa lalu di ruangan ini"; +"poll_history_no_active_poll_text" = "Tidak ada pemungutan suara yang aktifk di ruangan ini"; +"poll_history_past_segment_title" = "Pemungutan suara sebelumnya"; +"poll_history_active_segment_title" = "Pemungutan suara aktif"; + +// MARK: - Polls history + +"poll_history_title" = "Riwayat pemungutan suara"; +"room_details_polls" = "Riwayat pemungutan suara"; +"accessibility_selected" = "dipilih"; +"voice_broadcast_playback_lock_screen_placeholder" = "Siaran suara"; +"wysiwyg_composer_format_action_quote" = "Saklar kutipan"; +"wysiwyg_composer_format_action_code_block" = "Saklar blok kode"; +"wysiwyg_composer_format_action_ordered_list" = "Saklar daftar bernomor"; +"wysiwyg_composer_format_action_unordered_list" = "Saklar daftar bulat"; +"voice_broadcast_connection_error_message" = "Sayangnya kami tidak dapat memulai sebuah rekaman saat ini. Silakan coba lagi nanti."; +"voice_broadcast_connection_error_title" = "Kesalahan koneksi"; +"voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda"; +"poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung Rust (keluar dari akun untuk menonaktifkan)"; +"settings_labs_confirm_crypto_sdk" = "Ketahui bahwa fitur ini masih dalam masa eksperimental, ini mungkin tidak berfungsi seperti yang diharapkan dan dapat memiliki konsekuensi yang tidak terduga. Untuk mengembalikan fitur, cukup keluar dari akun dan masuk kembali ke akun. Gunakan dengan pengetahuan dan risiko Anda."; +"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung Rust"; +"poll_history_load_more" = "Muat lebih banyak pemungutan suara"; +"poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya"; +"poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; +"poll_history_loading_text" = "Menampilkan pemungutan suara"; +"poll_history_fetching_error" = "Terjadi kesalahan mendapatkan pemungutan suara."; +"key_backup_recover_from_private_key_progress" = "%@%% Selesai"; +"voice_broadcast_playback_unable_to_decrypt" = "Tidak dapat mendekripsi siaran suara ini."; +"home_context_menu_mark_as_unread" = "Tandai sebagai belum dibaca"; +"wysiwyg_composer_format_action_un_indent" = "Kurangi indentasi"; +"wysiwyg_composer_format_action_indent" = "Tambahkan indentasi"; +"poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa"; +"settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda."; +"authentication_qr_login_failure_device_not_supported" = "Penautan dengan perangkat ini tidak didukung."; diff --git a/Riot/Assets/is.lproj/Localizable.strings b/Riot/Assets/is.lproj/Localizable.strings index ec3fdaace..d47171514 100644 --- a/Riot/Assets/is.lproj/Localizable.strings +++ b/Riot/Assets/is.lproj/Localizable.strings @@ -170,3 +170,6 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ ný skilaboð í %@, %@ og fleirum"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ byrjaði talútsendingu"; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index aba02e2d6..1a0e5981e 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -1283,7 +1283,7 @@ "settings_add_3pid_invalid_password_message" = "Ógild auðkenni"; "settings_add_3pid_password_title_msidsn" = "Bæta við símanúmeri"; "settings_add_3pid_password_title_email" = "Bæta við tölvupóstfangi"; -"settings_labs_enable_threads" = "Skilaboð í spjallþráðum"; +"settings_labs_enable_threads" = "Spjallþræðir skilaboða"; "settings_labs_enabled_polls" = "Kannanir"; "settings_integrations_allow_button" = "Sýsla með samþættingar"; "settings_new_keyword" = "Bæta við nýju stikkorði"; @@ -2132,7 +2132,6 @@ "user_sessions_overview_title" = "Setur"; "space_selector_create_space" = "Búa til svæði"; -"all_chats_onboarding_try_it" = "Prófaðu það"; "all_chats_edit_menu_space_settings" = "Stillingar svæðis"; "all_chats_edit_menu_leave_space" = "Yfirgefa %@"; "room_recents_recently_viewed_section" = "Nýlega skoðað"; @@ -2234,7 +2233,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Ekkert nýtt."; -"all_chats_onboarding_page_title1" = "Velkomin í nýja sýn!"; "all_chats_nothing_found_placeholder_message" = "Reyndu að aðlaga leitina þína."; "all_chats_edit_layout_alphabetical_order" = "Raða A-Ö"; "all_chats_edit_layout_activity_order" = "Raða eftir virkni"; @@ -2317,7 +2315,7 @@ "device_name_mobile" = "%@ fyrir farsíma"; "device_name_web" = "%@ á vefnum"; "device_name_desktop" = "%@ fyrir einkatölvur"; -"user_session_item_details" = "%@ · Síðasta virkni %@"; +"user_session_item_details" = "%1$@ · %2$@"; "location_sharing_live_loading" = "Hleð inn rauntímastaðsetningu..."; "location_sharing_live_list_item_time_left" = "%@ fór"; "location_sharing_map_credits_title" = "© Höfundarréttur"; @@ -2330,9 +2328,6 @@ // Mark: - Space Selector "space_selector_title" = "Svæðin mín"; -"all_chats_onboarding_title" = "Hvað er nýtt"; -"all_chats_onboarding_page_title3" = "Gefðu umsögn"; -"all_chats_onboarding_page_title2" = "Aðgangur að svæðum"; "all_chats_user_menu_settings" = "Notandastillingar"; "all_chats_edit_layout_pin_spaces_title" = "Festu svæðin þín"; @@ -2368,3 +2363,119 @@ // MARK: Authentication "authentication_registration_title" = "Búðu til aðganginn þinn"; +"notice_voice_broadcast_ended_by_you" = "Þú endaðir talútsendingu."; +"notice_voice_broadcast_ended" = "%@ endaði talútsendingu."; +"notice_voice_broadcast_live" = "Bein útsending"; +"deselect_all" = "Afvelja allt"; +"wysiwyg_composer_link_action_edit_title" = "Breyta tengli"; +"wysiwyg_composer_link_action_create_title" = "Búa til tengil"; +"wysiwyg_composer_link_action_link" = "Tengill"; + + + +// Links +"wysiwyg_composer_link_action_text" = "Texti"; +"wysiwyg_composer_start_action_voice_broadcast" = "Útvörpun tals"; +"wysiwyg_composer_start_action_text_formatting" = "Sníðing texta"; +"wysiwyg_composer_start_action_camera" = "Myndavél"; +"wysiwyg_composer_start_action_location" = "Staðsetning"; +"wysiwyg_composer_start_action_polls" = "Kannanir"; +"wysiwyg_composer_start_action_attachments" = "Viðhengi"; +"wysiwyg_composer_start_action_stickers" = "Límmerki"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Ljósmyndasafn"; +"user_session_overview_session_details_button_title" = "Nánar um setuna"; +"user_session_overview_session_title" = "Seta"; +"user_session_overview_current_session_title" = "Núverandi seta"; +"user_session_details_application_url" = "Slóð (URL)"; +"user_session_details_application_version" = "Útgáfa"; +"user_session_details_application_name" = "Heiti"; +"user_session_details_device_os" = "Stýrikerfi"; +"user_session_details_device_browser" = "Vafri"; +"user_session_details_device_model" = "Gerð"; +"user_session_details_device_ip_location" = "Staðsetning IP-vistfangs"; +"user_session_details_device_ip_address" = "IP-vistfang"; +"user_session_details_last_activity" = "Síðasta virkni"; +"user_session_details_session_id" = "Auðkenni setu"; +"user_session_details_session_name" = "Nafn á setu"; +"user_session_details_device_section_header" = "Tæki"; +"user_session_details_application_section_header" = "Forrit"; +"user_session_details_session_section_header" = "Seta"; +"user_session_details_title" = "Nánar um setuna"; +"device_type_name_unknown" = "Óþekkt"; +"device_type_name_mobile" = "Farsími"; +"device_type_name_web" = "Vefur"; +"device_type_name_desktop" = "Borðtölva"; +"user_other_session_selected_count" = "%@ valið"; +"user_other_session_clear_filter" = "Hreinsa síu"; +"user_other_session_no_unverified_sessions" = "Engar óstaðfestar setur fundust."; +"user_other_session_no_verified_sessions" = "Engar staðfestar setur fundust."; +"user_other_session_no_inactive_sessions" = "Engar óvirkar setur fundust."; +"user_other_session_filter_menu_inactive" = "Óvirkt"; +"user_other_session_filter_menu_unverified" = "Óstaðfestar"; +"user_other_session_filter_menu_verified" = "Staðfestar"; +"user_other_session_filter_menu_all" = "Allar setur"; +"user_other_session_filter" = "Sía"; +"user_other_session_security_recommendation_title" = "Aðrar setur"; +"user_session_inactive_session_title" = "Óvirkar setur"; +"user_session_unverified_session_title" = "Óstaðfest seta"; +"user_session_verified_session_title" = "Sannreyndar setur"; +"user_session_got_it" = "Náði því"; +"user_session_push_notifications" = "Ýti-tilkynningar"; +"user_session_verification_unknown_short" = "Óþekkt"; +"user_session_verification_unknown" = "Óþekkt staða sannvottunar"; +"user_sessions_view_all_action" = "Skoða öll (%d)"; +"user_sessions_overview_link_device" = "Tengja tæki"; +"user_sessions_overview_current_session_section_title" = "Núverandi seta"; +"user_sessions_hide_location_info" = "Fela IP-vistfang"; +"user_sessions_show_location_info" = "Birta IP-vistfang"; +"user_sessions_overview_other_sessions_section_title" = "Aðrar setur"; +"user_sessions_overview_security_recommendations_inactive_title" = "Óvirkar setur"; +"user_sessions_overview_security_recommendations_unverified_title" = "Óstaðfestar setur"; +"user_sessions_overview_security_recommendations_section_title" = "Ráðleggingar varðandi öryggi"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"location_sharing_live_lab_promotion_activation" = "Virkja deilingu rauntímastaðsetninga"; +"location_sharing_live_timer_incoming" = "Í beinni til %@"; +"poll_timeline_reply_ended_poll" = "Lauk könnun"; +"poll_timeline_ended_text" = "Lauk könnuninni"; +"poll_history_past_segment_title" = "Fyrri kannanir"; +"poll_history_active_segment_title" = "Virkar kannanir"; + +// MARK: - Polls history + +"poll_history_title" = "Breytingaskrá könnunar"; +"all_chats_user_menu_accessibility_label" = "Valmynd notandans"; +"voice_broadcast_connection_error_title" = "Villa í tengingu"; +"voice_broadcast_voip_cannot_start_title" = "Get ekki hafið símtal"; +"voice_broadcast_stop_alert_agree_button" = "Já, stöðva"; +"voice_broadcast_buffering" = "Hleð í biðminni..."; +"voice_broadcast_time_left" = "%@ eftir"; +"voice_broadcast_tile" = "Útvörpun tals"; +"voice_broadcast_live" = "Beint"; +"voice_broadcast_playback_lock_screen_placeholder" = "Útvörpun tals"; + +// Unverified sessions +"key_verification_alert_title" = "Þú ert með óstaðfestar setur"; +"sign_out_confirmation_message" = "Ertu viss um að þú viljir skrá þig út?"; + +// MARK: Sign out warning + +"sign_out" = "Skrá út"; +"secure_key_backup_setup_cancel_alert_message" = "Ef þú hættir við núna, geturðu tapað dulrituðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum.\n\nÞú getur víka sett upp örugga afritun og sýslað með dulritunarlyklana þína í stillingunum."; +"room_details_polls" = "Breytingaskrá könnunar"; +"manage_session_sign_out_other_sessions" = "Skrá út úr öllum öðrum setum"; +"manage_session_rename" = "Endurnefna setu"; +"settings_labs_enable_voice_broadcast" = "Útvörpun tals"; +"authentication_qr_login_failure_retry" = "Reyna aftur"; +"authentication_qr_login_loading_connecting_device" = "Tengist við tæki"; +"authentication_qr_login_scan_title" = "Skanna QR-kóða"; +"authentication_qr_login_start_title" = "Skanna QR-kóða"; +"authentication_login_with_qr" = "Skrá inn með QR-kóða"; diff --git a/Riot/Assets/it.lproj/Localizable.strings b/Riot/Assets/it.lproj/Localizable.strings index bb8b3e707..3232ff2a4 100644 --- a/Riot/Assets/it.lproj/Localizable.strings +++ b/Riot/Assets/it.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ ha condiviso la sua posizione"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ ha iniziato una trasmissione vocale"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 35bd94c35..6d2cbe086 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2445,14 +2445,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Niente di nuovo."; -"all_chats_onboarding_try_it" = "Provalo"; -"all_chats_onboarding_title" = "Novità"; -"all_chats_onboarding_page_message3" = "Tocca il tuo profilo per farci sapere cosa ne pensi."; -"all_chats_onboarding_page_title3" = "Invia un feedback"; -"all_chats_onboarding_page_message2" = "Accedi ai tuoi spazi (in basso a sinistra) più velocemente e più facilmente che mai."; -"all_chats_onboarding_page_title2" = "Accedi agli spazi"; -"all_chats_onboarding_page_message1" = "Per semplificare Element, le schede ora sono opzionali. Gestiscile usando il menu in alto a destra."; -"all_chats_onboarding_page_title1" = "Benvenuti ad una nuova panoramica!"; "all_chats_nothing_found_placeholder_message" = "Prova a cambiare la tua ricerca."; "all_chats_nothing_found_placeholder_title" = "Non è stato trovato niente."; "all_chats_empty_unreads_placeholder_message" = "Qui è dove verranno mostrati i messaggi non letti, quando ne avrai qualcuno."; @@ -2665,3 +2657,48 @@ "notice_voice_broadcast_live" = "Trasmissione in diretta"; "wysiwyg_composer_format_action_inline_code" = "Applica formato codice interlinea"; "user_other_session_security_recommendation_title" = "Altre sessioni"; +"poll_timeline_ended_text" = "Sondaggio terminato"; +"poll_timeline_decryption_error" = "A causa di errori di decifrazione, alcuni voti potrebbero non venire contati"; +"voice_broadcast_voip_cannot_start_description" = "Non puoi avviare una chiamata perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare una chiamata."; +"voice_broadcast_voip_cannot_start_title" = "Impossibile avviare una chiamata"; +"voice_message_broadcast_in_progress_title" = "Impossibile iniziare il messaggio vocale"; +"voice_message_broadcast_in_progress_message" = "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale"; +"poll_history_no_past_poll_text" = "In questa stanza non ci sono sondaggi passati"; +"poll_history_no_active_poll_text" = "In questa stanza non ci sono sondaggi attivi"; +"poll_history_past_segment_title" = "Sondaggi passati"; +"poll_history_active_segment_title" = "Sondaggi attivi"; + +// MARK: - Polls history + +"poll_history_title" = "Cronologia sondaggi"; +"voice_broadcast_playback_lock_screen_placeholder" = "Trasmissione vocale"; +"room_details_polls" = "Cronologia sondaggi"; +"accessibility_selected" = "selezionato"; +"wysiwyg_composer_format_action_quote" = "Attiva/disattiva citazione"; +"wysiwyg_composer_format_action_code_block" = "Attiva/disattiva blocco di codice"; +"wysiwyg_composer_format_action_ordered_list" = "Attiva/disattiva elenco numerato"; +"wysiwyg_composer_format_action_unordered_list" = "Attiva/disattiva elenco puntato"; +"poll_timeline_reply_ended_poll" = "Sondaggio terminato"; +"voice_broadcast_recorder_connection_error" = "Errore di connessione - Registrazione in pausa"; +"voice_broadcast_connection_error_message" = "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi."; +"voice_broadcast_connection_error_title" = "Errore di connessione"; +"poll_history_load_more" = "Carica più sondaggi"; +"poll_history_no_past_poll_period_text" = "Non ci sono sondaggi passati negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti"; +"poll_history_no_active_poll_period_text" = "Non ci sono sondaggi attivi negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti"; +"poll_history_loading_text" = "Visualizzazione sondaggi"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrazione dati\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end Rust (disconnettiti per disattivarla)"; +"settings_labs_confirm_crypto_sdk" = "Si noti che questa funzione, essendo ancora in fase sperimentale, potrebbe non funzionare come previsto e potrebbe avere conseguenze indesiderate. Per disattivare la funzione, è sufficiente disconnettersi e riaccedere. Utilizzare a propria discrezione e con cautela."; +"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end Rust"; +"wysiwyg_composer_format_action_un_indent" = "Diminuisci indentazione"; +"wysiwyg_composer_format_action_indent" = "Aumenta indentazione"; +"poll_history_fetching_error" = "Errore di recupero dei sondaggi."; +"voice_broadcast_playback_unable_to_decrypt" = "Impossibile decifrare questa trasmissione vocale."; +"home_context_menu_mark_as_unread" = "Segna come non letto"; +"key_backup_recover_from_private_key_progress" = "%@%% Completato"; +"poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale"; +"settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione."; +"authentication_qr_login_failure_device_not_supported" = "Il collegamento con questo dispositivo non è supportato."; diff --git a/Riot/Assets/ja.lproj/InfoPlist.strings b/Riot/Assets/ja.lproj/InfoPlist.strings index d99e8eb80..cae22a109 100644 --- a/Riot/Assets/ja.lproj/InfoPlist.strings +++ b/Riot/Assets/ja.lproj/InfoPlist.strings @@ -1,9 +1,9 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真撮影、動画撮影に使用されます。"; -"NSPhotoLibraryUsageDescription" = "フォトライブラリは、写真や動画の送信に使用されます。"; +"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真、動画の撮影とアップロードに使用されます。"; +"NSPhotoLibraryUsageDescription" = "フォトへのアクセスを許可すると、写真や動画をライブラリーからアップロードできるようになります。"; "NSMicrophoneUsageDescription" = "Elementは通話、動画撮影、ボイスメッセージの録音にマイクへのアクセスを必要としています。"; -"NSContactsUsageDescription" = "Elementは、あなたが連絡先をチャットに招待できるように、連絡先を表示します。"; +"NSContactsUsageDescription" = "あなたのIDサーバーに共有され、Matrixで連絡先を発見するのに使用されます。"; "NSCalendarsUsageDescription" = "予定されているミーティングをアプリで確認することができます。"; "NSFaceIDUsageDescription" = "Face IDはアプリへのアクセスに使用されます。"; "NSLocationWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。"; -"NSLocationAlwaysAndWhenInUseUsageDescription" = "あなたが他の人に位置を共有するとき、Elementは地図をその人に表示するアクセス権が必要です。"; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。"; diff --git a/Riot/Assets/ja.lproj/Localizable.strings b/Riot/Assets/ja.lproj/Localizable.strings index 4a63be21b..c76236c89 100644 --- a/Riot/Assets/ja.lproj/Localizable.strings +++ b/Riot/Assets/ja.lproj/Localizable.strings @@ -1,9 +1,9 @@ /* New message from a specific person, not referencing a room */ -"MSG_FROM_USER" = "%@ さんからメッセージ"; +"MSG_FROM_USER" = "%@さんがメッセージを送信しました"; /* New message from a specific person in a named room */ -"MSG_FROM_USER_IN_ROOM" = "%@ さんが %@ へ発言"; +"MSG_FROM_USER_IN_ROOM" = "%@さんが%@に投稿しました"; /* New message from a specific person, not referencing a room. Content included. */ -"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; +"MSG_FROM_USER_WITH_CONTENT" = "%@:%@"; /* New message from a specific person in a named room. Content included. */ "MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ in %@: %@"; /* New action message from a specific person, not referencing a room. */ @@ -12,62 +12,62 @@ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ /* New action message from a specific person in a named room. */ -"IMAGE_FROM_USER_IN_ROOM" = "%@ さんが写真を投稿 %@ in %@"; +"IMAGE_FROM_USER_IN_ROOM" = "%@さんが写真%@を%@に投稿しました"; /* Multiple unread messages in a room */ -"UNREAD_IN_ROOM" = "%@ 新しいメッセージ in %@"; +"UNREAD_IN_ROOM" = "%@件の新しいメッセージが%@にあります"; /* Multiple unread messages from a specific person, not referencing a room */ -"MSGS_FROM_USER" = "%@ 新しいメッセージ in %@"; +"MSGS_FROM_USER" = "%@件の新しいメッセージが%@にあります"; /* Multiple unread messages from two people */ -"MSGS_FROM_TWO_USERS" = "%@ 新しいメッセージ from %@ and %@"; +"MSGS_FROM_TWO_USERS" = "%@件の新しいメッセージを%@と%@から受信しました"; /* Multiple unread messages from three people */ -"MSGS_FROM_THREE_USERS" = "%@ 新しいメッセージ from %@, %@ and %@"; +"MSGS_FROM_THREE_USERS" = "%@件の新しいメッセージを%@、%@、%@から受信しました"; /* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ -"MSGS_FROM_TWO_PLUS_USERS" = "%@ 新しいメッセージ from %@, %@ 他"; +"MSGS_FROM_TWO_PLUS_USERS" = "%@件の新しいメッセージを%@、%@、ほか数人から受信しました"; /* Multiple messages in two rooms */ -"MSGS_IN_TWO_ROOMS" = "%@ 新しいメッセージ in %@ and %@"; +"MSGS_IN_TWO_ROOMS" = "%@件の新しいメッセージが%@と%@にあります"; /* Look, stuff's happened, alright? Just open the app. */ -"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 新しいメッセージ in %@, %@ 他"; +"MSGS_IN_TWO_PLUS_ROOMS" = "%@件の新しいメッセージが%@、%@などにあります"; /* A user has invited you to a chat */ -"USER_INVITE_TO_CHAT" = "%@ さんがあなたを対話に招待しました"; +"USER_INVITE_TO_CHAT" = "%@さんがあなたをチャットに招待しました"; /* A user has invited you to an (unamed) group chat */ -"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ さんがあなたをルームへ招待しました"; +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@さんがあなたをグループチャットに招待しました"; /* A user has invited you to a named room */ -"USER_INVITE_TO_NAMED_ROOM" = "%@ さんがルーム %@ へ招待しました"; +"USER_INVITE_TO_NAMED_ROOM" = "%@さんがルーム %@ に招待しました"; /* Incoming one-to-one voice call */ -"VOICE_CALL_FROM_USER" = "%@ さんから通話着信"; +"VOICE_CALL_FROM_USER" = "%@さんから通話着信"; /* Incoming one-to-one video call */ -"VIDEO_CALL_FROM_USER" = "%@ さんから映像つき通話着信"; +"VIDEO_CALL_FROM_USER" = "%@さんからビデオ通話の着信"; /* Incoming unnamed voice conference invite from a specific person */ -"VOICE_CONF_FROM_USER" = "%@ さんから会議通話の着信"; +"VOICE_CONF_FROM_USER" = "%@さんからグループ通話の着信"; /* Incoming unnamed video conference invite from a specific person */ -"VIDEO_CONF_FROM_USER" = "%@ さんから映像つき会議通話の着信"; +"VIDEO_CONF_FROM_USER" = "%@さんからビデオグループ通話の着信"; /* Incoming named voice conference invite from a specific person */ -"VOICE_CONF_NAMED_FROM_USER" = "会議通話の着信 from %@: '%@'"; +"VOICE_CONF_NAMED_FROM_USER" = "%@さんからグループ通話の着信:'%@'"; /* Incoming named video conference invite from a specific person */ -"VIDEO_CONF_NAMED_FROM_USER" = "映像つき会議通話の着信 from %@: '%@'"; +"VIDEO_CONF_NAMED_FROM_USER" = "%@さんからビデオグループ通話の着信:'%@'"; /* A single unread message in a room */ -"SINGLE_UNREAD_IN_ROOM" = "%@にメッセージを受け取りました"; +"SINGLE_UNREAD_IN_ROOM" = "%@でメッセージを受信しました"; /* A single unread message */ -"SINGLE_UNREAD" = "あなたはメッセージを受け取りました"; +"SINGLE_UNREAD" = "メッセージを受信しました"; /** Key verification **/ "KEY_VERIFICATION_REQUEST_FROM_USER" = "%@は認証を要求しています"; /* New message indicator on a room */ -"MESSAGE_IN_X" = "%@ 内のメッセージ"; +"MESSAGE_IN_X" = "%@内のメッセージ"; /* Sticker from a specific person, not referencing a room. */ -"STICKER_FROM_USER" = "%@ さんからのスタンプ"; +"STICKER_FROM_USER" = "%@さんがステッカーを送信しました"; /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@(%@ から)"; /* Group call from user, CallKit caller name */ -"GROUP_CALL_FROM_USER" = "%@ (グループ通話)"; +"GROUP_CALL_FROM_USER" = "%@(グループ通話)"; "MESSAGE_PROTECTED" = "新しいメッセージ"; /* New message indicator from a DM */ -"MESSAGE_FROM_X" = "%@ からのメッセージ"; +"MESSAGE_FROM_X" = "%@さんからのメッセージ"; /** Notification messages **/ @@ -78,52 +78,55 @@ "Notification" = "通知"; /* New message reply from a specific person in a named room. */ -"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ さんが %@ で返信"; +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@さんが%@で返信しました"; /* New message reply from a specific person, not referencing a room. */ -"REPLY_FROM_USER_TITLE" = "%@ さんが返信"; +"REPLY_FROM_USER_TITLE" = "%@さんが返信しました"; /** Reactions **/ /* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ -"REACTION_FROM_USER" = "%@ さんが %@ とリアクション"; +"REACTION_FROM_USER" = "%@さんが%@でリアクションしました"; /* A user has reacted to a message, but the reaction content is unknown */ -"GENERIC_REACTION_FROM_USER" = "%@ さんがリアクション"; +"GENERIC_REACTION_FROM_USER" = "%@さんがリアクションを送信しました"; /* New file message from a specific person, not referencing a room. */ -"LOCATION_FROM_USER" = "%@ さんが位置情報を共有"; +"LOCATION_FROM_USER" = "%@さんが位置情報を共有しました"; /* New voice message from a specific person, not referencing a room. */ -"VOICE_MESSAGE_FROM_USER" = "%@ さんが音声メッセージを送信"; +"VOICE_MESSAGE_FROM_USER" = "%@さんが音声メッセージを送信しました"; /* New video message from a specific person, not referencing a room. */ -"VIDEO_FROM_USER" = "%@ さんが動画を送信"; +"VIDEO_FROM_USER" = "%@さんが動画を送信しました"; /** Media Messages **/ /* New image message from a specific person, not referencing a room. */ -"PICTURE_FROM_USER" = "%@ さんが写真を送信"; +"PICTURE_FROM_USER" = "%@さんが写真を送信しました"; /* A user added a Jitsi call to a room */ -"GROUP_CALL_STARTED" = "グループ通話が開始されました"; +"GROUP_CALL_STARTED" = "グループ通話を開始しました"; /* A user's membership has updated in an unknown way */ -"USER_MEMBERSHIP_UPDATED" = "%@ がプロフィールを更新しました"; +"USER_MEMBERSHIP_UPDATED" = "%@さんがプロフィールを更新しました"; /* A user has change their avatar */ -"USER_UPDATED_AVATAR" = "%@ がアバター画像を変更しました"; +"USER_UPDATED_AVATAR" = "%@さんがアバターを変更しました"; /* A user has change their name to a new name which we don't know */ -"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ が名前を変更しました"; +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@さんが名前を変更しました"; /** Membership Updates **/ /* A user has change their name to a new name */ -"USER_UPDATED_DISPLAYNAME" = "%@ が名前を %@ に変更しました"; +"USER_UPDATED_DISPLAYNAME" = "%@さんが名前を%@に変更しました"; /* New file message from a specific person, not referencing a room. */ -"FILE_FROM_USER" = "%@ がファイルを送信しました: %@"; +"FILE_FROM_USER" = "%@がファイルを送信しました:%@"; /* New audio message from a specific person, not referencing a room. */ -"AUDIO_FROM_USER" = "%@ が音声ファイルを送信しました: %@"; +"AUDIO_FROM_USER" = "%@が音声ファイルを送信しました:%@"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@さんが音声配信を開始しました"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 88f0760ee..676a02c9a 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -8,7 +8,7 @@ "view" = "表示"; "next" = "次へ"; "back" = "戻る"; -"continue" = "続ける"; +"continue" = "続行"; "create" = "作成"; "start" = "開始"; "leave" = "退出"; @@ -20,27 +20,27 @@ "cancel" = "キャンセル"; "save" = "保存"; "join" = "参加"; -"decline" = "断る"; -"accept" = "受諾"; +"decline" = "拒否"; +"accept" = "同意"; "preview" = "プレビュー"; "camera" = "カメラ"; "voice" = "音声"; "video" = "動画"; -"active_call" = "通話開始"; -"active_call_details" = "通話開始(%@)"; +"active_call" = "実施中の通話"; +"active_call_details" = "実施中の通話(%@)"; "later" = "後で"; "rename" = "名前変更"; "collapse" = "折りたたむ"; -"send_to" = "%@さんへ送信"; -"sending" = "送信中"; +"send_to" = "%@へ送信"; +"sending" = "送信しています"; // Authentication "auth_login" = "ログイン"; -"auth_register" = "利用者登録"; +"auth_register" = "登録"; "auth_submit" = "受諾"; -"auth_skip" = "省く"; -"auth_send_reset_email" = "初期化メール送信"; -"auth_return_to_login" = "ログイン画面へ戻る"; -"auth_user_id_placeholder" = "ユーザー名または電子メール"; +"auth_skip" = "スキップ"; +"auth_send_reset_email" = "リセット用メールを送信"; +"auth_return_to_login" = "ログイン画面に戻る"; +"auth_user_id_placeholder" = "電子メールまたはユーザー名"; "auth_password_placeholder" = "パスワード"; "auth_new_password_placeholder" = "新しいパスワード"; "auth_user_name_placeholder" = "ユーザー名"; @@ -48,15 +48,15 @@ "auth_email_placeholder" = "メールアドレス"; "auth_optional_phone_placeholder" = "電話番号(任意)"; "auth_phone_placeholder" = "電話番号"; -"auth_repeat_password_placeholder" = "パスワード再確認"; -"auth_repeat_new_password_placeholder" = "新しいパスワードを再確認"; +"auth_repeat_password_placeholder" = "パスワードを再確認"; +"auth_repeat_new_password_placeholder" = "Matrixアカウントの新しいパスワードを確認"; "auth_home_server_placeholder" = "URL (例 https://matrix.org)"; "auth_identity_server_placeholder" = "URL (例 https://vector.im)"; -"auth_invalid_login_param" = "ユーザー名かパスワードが正しくありません"; -"auth_invalid_user_name" = "ユーザー名は半角英数字、ドット、ハイフン、アンダスコアのみで記して下さい"; -"auth_invalid_password" = "パスワードが短すぎます(最小6文字)"; +"auth_invalid_login_param" = "ユーザー名とパスワードの一方あるいは両方が正しくありません"; +"auth_invalid_user_name" = "ユーザー名には半角英数字、ドット、ハイフン、アンダースコアのみを使用してください"; +"auth_invalid_password" = "パスワードが短すぎます(最小6文字)"; "auth_invalid_email" = "メールアドレスの形式が正しくありません"; -"auth_invalid_phone" = "正しくない電話番号のようです"; +"auth_invalid_phone" = "電話番号の形式が正しくありません"; "auth_missing_password" = "パスワードが入力されていません"; "auth_add_email_message" = "電子メールアドレスを登録すると, 誰かがあなたを検索をしたり, パスワード紛失時に初期化のメールを送ることができます."; "auth_add_phone_message" = "電話番号を登録すると, 誰かがあなたを電話番号で検索できるようになります."; @@ -67,38 +67,38 @@ "auth_missing_email_or_phone" = "メールアドレスまたは電話番号が入力されていません"; "auth_email_in_use" = "このメールアドレスは既に使用されています"; "auth_phone_in_use" = "この電話番号は既に使用されています"; -"auth_untrusted_id_server" = "この認証サーバーは信用されていません"; +"auth_untrusted_id_server" = "この認証サーバーは信頼されていません"; "auth_password_dont_match" = "パスワードが一致しません"; "auth_username_in_use" = "ユーザー名は既に使用されています"; -"auth_forgot_password" = "パスワードを忘れましたか?"; +"auth_forgot_password" = "Matrixのアカウントのパスワードを忘れましたか?"; "auth_email_not_found" = "電子メールの送信に失敗しました:メールアドレスが見つかりません"; -"auth_use_server_options" = "接続先サーバーを指定する(追加設定)"; -"auth_email_validation_message" = "登録を続行するには電子メールを確認して下さい"; -"auth_msisdn_validation_title" = "認証を確認中"; -"auth_msisdn_validation_message" = "SMSで認証番号を送りました。以下にその番号を入力してください。"; +"auth_use_server_options" = "接続先サーバーを指定(高度)"; +"auth_email_validation_message" = "登録を続行するには電子メールを確認してください"; +"auth_msisdn_validation_title" = "認証の保留中"; +"auth_msisdn_validation_message" = "SMSで認証コードを送りました。以下にコードを入力してください。"; "auth_msisdn_validation_error" = "電話番号を認証できません。"; "auth_recaptcha_message" = "このホームサーバーは、あなたがロボットではないことの確認を求めています"; -"auth_reset_password_message" = "Matrixのアカウントのパスワードを初期化するには、アカウントに登録されているメールアドレスを入力してください:"; +"auth_reset_password_message" = "Matrixのアカウントのパスワードを再設定するには、アカウントに登録されているメールアドレスを入力してください:"; "auth_reset_password_missing_email" = "あなたのアカウントに登録されたメールアドレスの入力が必要です。"; "auth_reset_password_missing_password" = "新しいパスワードの入力が必要です。"; -"auth_reset_password_email_validation_message" = "%@ へ電子メールが送信されました。リンクをたどったら以下をクリックしてください。"; -"auth_reset_password_next_step_button" = "メールアドレスを認証しました"; -"auth_reset_password_error_unauthorized" = "メールアドレスの確認に失敗しました:電子メールのリンクをクリックしたことを確認してください"; -"auth_reset_password_error_not_found" = "あなたのメールアドレスは、接続先サーバー上のMatrix IDと関連付けられていないようです。"; -"auth_reset_password_success_message" = "あなたのパスワードは初期化されました。\n\nあなたは全てのセッションから切断しており、プッシュ通知を受け取ることはありません。通知を再度有効にするには、各端末に再度ログインします。"; -"auth_add_email_and_phone_warning" = "電子メールと電話番号の同時登録は、まだシステムが対応できません。電話番号だけの登録は可能です。お手数おかけしますが、後ほど個人情報設定からメールアドレスを登録してください。"; +"auth_reset_password_email_validation_message" = "%@ へ電子メールを送信しました。電子メール内のリンクを開いた後、以下をクリックしてください。"; +"auth_reset_password_next_step_button" = "メールアドレスを確認しました"; +"auth_reset_password_error_unauthorized" = "メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください"; +"auth_reset_password_error_not_found" = "あなたのメールアドレスは、このホームサーバー上のMatrix IDと関連付けられていないようです。"; +"auth_reset_password_success_message" = "あなたのMatrixのアカウントのパスワードは初期化されました。\n\n全てのセッションからログアウトしたため、プッシュ通知は送信されません。通知を再度有効にするには、各端末で再度ログインしてください。"; +"auth_add_email_and_phone_warning" = "電子メールと電話番号の両方による登録は、まだサポートしていません。電話番号のみでの登録を受け付けています。メールアドレスは、設定内のプロフィールから後ほど追加できます。"; // Chat creation "room_creation_title" = "チャットを開始"; "room_creation_account" = "アカウント"; "room_creation_appearance" = "外観"; "room_creation_appearance_name" = "名前"; "room_creation_appearance_picture" = "チャット画像(任意)"; -"room_creation_privacy" = "個人情報保護"; +"room_creation_privacy" = "プライバシー"; "room_creation_private_room" = "この会話は非公開です"; "room_creation_public_room" = "この会話は公開されています"; "room_creation_make_public" = "公開"; "room_creation_make_public_prompt_title" = "このチャットを公開しますか?"; -"room_creation_make_public_prompt_msg" = "このチャットを公開してもよろしいですか?誰でもあなたのメッセージを読んでチャットに参加できます。"; +"room_creation_make_public_prompt_msg" = "このチャットを公開してよろしいですか?誰でもあなたのメッセージを読み、チャットに参加できます。"; "room_creation_keep_private" = "非公開に保つ"; "room_creation_make_private" = "非公開にする"; "room_creation_wait_for_creation" = "ルームは既に作成されています。お待ちください。"; @@ -113,290 +113,290 @@ "room_recents_invites_section" = "招待中"; "room_recents_start_chat_with" = "チャットを開始"; "room_recents_create_empty_room" = "ルームを作成"; -"room_recents_join_room" = "ルームへ参加"; -"room_recents_join_room_title" = "ルームへ参加"; +"room_recents_join_room" = "ルームに参加"; +"room_recents_join_room_title" = "ルームに参加"; "room_recents_join_room_prompt" = "ルームIDまたはルームのエイリアスを入力"; // People tab "people_invites_section" = "招待中"; "people_conversation_section" = "会話"; -"people_no_conversation" = "会話なし"; +"people_no_conversation" = "会話がありません"; // Rooms tab "room_directory_no_public_room" = "利用可能な公開ルームはありません"; // Search "search_rooms" = "ルーム"; "search_messages" = "メッセージ"; "search_people" = "連絡先"; -"search_files" = "添付ファイル"; +"search_files" = "ファイル"; "search_default_placeholder" = "検索"; "search_people_placeholder" = "ユーザーID、表示名、電子メールで検索"; -"search_no_result" = "結果なし"; +"search_no_result" = "結果がありません"; "search_in_progress" = "検索しています…"; // Directory "directory_cell_title" = "ルーム一覧を見る"; -"directory_cell_description" = "%tuつのルーム"; +"directory_cell_description" = "%tu個のルーム"; "directory_search_results_title" = "ルーム一覧の検索結果"; "directory_searching_title" = "ルーム一覧を検索しています…"; "directory_search_fail" = "一覧を取得できませんでした"; // Contacts "contacts_address_book_section" = "端末の電話帳"; -"contacts_address_book_matrix_users_toggle" = "Matrix利用者のみ"; -"contacts_address_book_no_contact" = "端末内電話帳に連絡先がありません"; -"contacts_address_book_permission_required" = "端末内電話帳へのアクセス権限が必要です"; +"contacts_address_book_matrix_users_toggle" = "Matrixのユーザーのみ"; +"contacts_address_book_no_contact" = "端末の電話帳に連絡先がありません"; +"contacts_address_book_permission_required" = "端末の電話帳へのアクセス権限が必要です"; "contacts_user_directory_section" = "ユーザー一覧"; -"contacts_user_directory_offline_section" = "ユーザー一覧 (オフライン)"; +"contacts_user_directory_offline_section" = "ユーザー一覧(オフライン)"; // Chat participants "room_participants_title" = "参加者"; "room_participants_add_participant" = "参加者を追加"; -"room_participants_one_participant" = "参加者1名"; -"room_participants_multi_participants" = "参加者%d名"; -"room_participants_leave_prompt_title" = "ルームを退出"; -"room_participants_leave_prompt_msg" = "ルームを退出してよろしいですか?"; +"room_participants_one_participant" = "1人の参加者"; +"room_participants_multi_participants" = "%d人の参加者"; +"room_participants_leave_prompt_title" = "ルームから退出"; +"room_participants_leave_prompt_msg" = "ルームから退出してよろしいですか?"; "room_participants_remove_prompt_title" = "確認"; -"room_participants_remove_prompt_msg" = "本当に%@をチャットから退去させますか?"; +"room_participants_remove_prompt_msg" = "%@をチャットから追放してよろしいですか?"; "room_participants_remove_third_party_invite_msg" = "サードパーティの招待を削除することは、APIが存在するまでサポートされていません"; "room_participants_invite_prompt_title" = "確認"; -"room_participants_invite_prompt_msg" = "%@をチャットに招待してよろしいですか?"; -"room_participants_filter_room_members" = "ルームメンバーを検索"; +"room_participants_invite_prompt_msg" = "%@をこのチャットに招待してよろしいですか?"; +"room_participants_filter_room_members" = "ルームのメンバーを検索"; "room_participants_invite_another_user" = "ユーザーID、名前、電子メールで検索、招待"; "room_participants_invite_malformed_id_title" = "招待エラー"; -"room_participants_invite_malformed_id" = "不正なIDです。メールアドレスを用いるか、'@localpart:domain'のようなMatrix IDを使用してください"; +"room_participants_invite_malformed_id" = "不正なIDです。メールアドレスを用いるか、'@localpart:domain'の形式のMatrix IDを使用してください"; "room_participants_invited_section" = "招待中"; "room_participants_online" = "オンライン"; "room_participants_offline" = "オフライン"; "room_participants_unknown" = "不明"; -"room_participants_idle" = "アイドル"; +"room_participants_idle" = "待機中"; "room_participants_now" = "現在"; "room_participants_ago" = "前"; -"room_participants_action_section_admin_tools" = "管理者権限操作"; -"room_participants_action_section_direct_chats" = "非公開のチャット"; -"room_participants_action_section_devices" = "セッション一覧"; +"room_participants_action_section_admin_tools" = "管理者ツール"; +"room_participants_action_section_direct_chats" = "ダイレクトメッセージ"; +"room_participants_action_section_devices" = "セッション"; "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; -"room_participants_action_leave" = "このルームを退出"; -"room_participants_action_remove" = "このルームから削除"; +"room_participants_action_leave" = "このルームから退出"; +"room_participants_action_remove" = "このルームから追放"; "room_participants_action_ban" = "このルームからブロック"; "room_participants_action_unban" = "ブロックを解除"; -"room_participants_action_ignore" = "このユーザーの発言を全て非表示にする"; -"room_participants_action_unignore" = "このユーザーの発言を全て表示"; +"room_participants_action_ignore" = "このユーザーのメッセージを全て非表示にする"; +"room_participants_action_unignore" = "このユーザーのメッセージを全て表示"; "room_participants_action_set_default_power_level" = "権限を一般ユーザーへ変更"; "room_participants_action_set_moderator" = "権限をモデレーターへ変更"; "room_participants_action_set_admin" = "権限を管理者へ変更"; "room_participants_action_start_new_chat" = "チャットを開始"; "room_participants_action_start_voice_call" = "音声通話を開始"; -"room_participants_action_start_video_call" = "映像付き音声通話を開始"; +"room_participants_action_start_video_call" = "ビデオ通話を開始"; "room_participants_action_mention" = "メンション"; // Chat -"room_jump_to_first_unread" = "最初の未読位置へ移動"; -"room_new_message_notification" = "%d件の新しい発言"; -"room_new_messages_notification" = "%d件の新しい発言"; -"room_one_user_is_typing" = "%@さんが入力しています…"; -"room_two_users_are_typing" = "%@さん、%@さんが入力しています…"; -"room_many_users_are_typing" = "%@さん、%@さん他が入力しています…"; -"room_message_placeholder" = "返信を送る(未暗号化)…"; -"encrypted_room_message_placeholder" = "暗号文を送信…"; -"room_message_short_placeholder" = "ここに送信文を入力…"; +"room_jump_to_first_unread" = "最新の未読へ移動"; +"room_new_message_notification" = "%d件の新しいメッセージ"; +"room_new_messages_notification" = "%d件の新しいメッセージ"; +"room_one_user_is_typing" = "%@が入力しています…"; +"room_two_users_are_typing" = "%@と%@が入力しています…"; +"room_many_users_are_typing" = "%@、%@他が入力しています…"; +"room_message_placeholder" = "メッセージを送信(暗号化されていません)…"; +"encrypted_room_message_placeholder" = "暗号化されたメッセージを送信…"; +"room_message_short_placeholder" = "メッセージを送信…"; "room_offline_notification" = "サーバーとの接続が失われました。"; "room_unsent_messages_notification" = "メッセージを送信できませんでした。"; -"room_unsent_messages_unknown_devices_notification" = "未知のセッションが存在するために文章が送信されませんでした。"; -"room_ongoing_conference_call" = "会議通話実施中。%@または%@で参加してください。"; -"room_ongoing_conference_call_with_close" = "会議通話実施中。%@または%@で参加してください。%@。"; +"room_unsent_messages_unknown_devices_notification" = "不明なセッションが存在するため、メッセージの送信に失敗しました。"; +"room_ongoing_conference_call" = "グループ通話を実施中。%@または%@で参加してください。"; +"room_ongoing_conference_call_with_close" = "グループ通話を実施中。%@または%@で参加してください。%@。"; "room_ongoing_conference_call_close" = "閉じる"; -"room_conference_call_no_power" = "このルームで会議通話を管理する権限が必要です"; +"room_conference_call_no_power" = "このルームでグループ通話を管理するための権限が必要です"; "room_prompt_resend" = "全て再送信"; -"room_prompt_cancel" = "全て中止"; -"room_resend_unsent_messages" = "未送信の文を再送信"; -"room_delete_unsent_messages" = "未送信の文を削除"; +"room_prompt_cancel" = "全てキャンセル"; +"room_resend_unsent_messages" = "未送信のメッセージを再送信"; +"room_delete_unsent_messages" = "未送信のメッセージを削除"; "room_event_action_copy" = "コピー"; "room_event_action_quote" = "引用"; "room_event_action_redact" = "削除"; -"room_event_action_more" = "さらに"; +"room_event_action_more" = "その他"; "room_event_action_share" = "共有"; "room_event_action_permalink" = "メッセージへのリンクをコピー"; -"room_event_action_view_source" = "ソースを表示"; -"room_event_action_report" = "発言を報告"; -"room_event_action_report_prompt_reason" = "この発言を報告する理由"; +"room_event_action_view_source" = "ソースコードを表示"; +"room_event_action_report" = "コンテンツを報告"; +"room_event_action_report_prompt_reason" = "このコンテンツを報告する理由"; "room_event_action_report_prompt_ignore_user" = "このユーザーからの全ての発言を非表示にしますか?"; "room_event_action_save" = "保存"; "room_event_action_resend" = "再送信"; "room_event_action_delete" = "削除"; -"room_event_action_cancel_send" = "送信中止"; -"room_event_action_cancel_download" = "ダウンロード中止"; -"room_event_action_view_encryption" = "暗号についての情報"; -"room_warning_about_encryption" = "エンドツーエンド暗号化はベータ版であり、信頼性が低い場合があります。\n\n発言を保護するためにはまだ信用すべきではありません。\n\n端末が参加するより前の発言履歴を復号化することはまだできません。\n\n暗号化された発言は、まだ暗号化を実装していないクライアントでは表示されません。"; -"room_event_failed_to_send" = "送信失敗"; +"room_event_action_cancel_send" = "送信をキャンセル"; +"room_event_action_cancel_download" = "ダウンロードをキャンセル"; +"room_event_action_view_encryption" = "暗号化についての情報"; +"room_warning_about_encryption" = "エンドツーエンド暗号化はベータ版のため、信頼性が低い場合があります。\n\nデータを保護するためにはまだ信用すべきではありません。\n\n端末が参加する以前の履歴を復号化することはまだできません。\n\n暗号化されたメッセージは、暗号化を実装していないクライアントでは表示できません。"; +"room_event_failed_to_send" = "送信に失敗しました"; // Unknown devices -"unknown_devices_alert_title" = "ルームに未知のセッションが存在します"; -"unknown_devices_alert" = "このルームには、確認されていない未知のセッションが含まれています。\nすなわち、セッションがをユーザー本人が所有しているという保証はありません。\n続ける前に各セッションの確認を行うことをおすすめしますが、確認することなく発言を再送信することができます。"; -"unknown_devices_send_anyway" = "とにかく送る"; -"unknown_devices_call_anyway" = "とにかく通話"; -"unknown_devices_answer_anyway" = "とにかく応答"; -"unknown_devices_verify" = "確認…"; -"unknown_devices_title" = "未知のセッション"; +"unknown_devices_alert_title" = "ルームに不明なセッションが存在します"; +"unknown_devices_alert" = "このルームには、未認証のセッションが含まれています。\nセッションをユーザー本人が所有しているという保証はありません。\n続行する前に各セッションの認証を行うことを推奨しますが、認証せずメッセージを再送信することもできます。"; +"unknown_devices_send_anyway" = "送信"; +"unknown_devices_call_anyway" = "通話"; +"unknown_devices_answer_anyway" = "応答"; +"unknown_devices_verify" = "認証…"; +"unknown_devices_title" = "不明なセッション"; // Room Title "room_title_new_room" = "新しいルーム"; "room_title_multiple_active_members" = "全%@人中%@人が回線接続"; "room_title_one_active_member" = "全%@人中%@人が回線接続"; -"room_title_invite_members" = "招待中"; -"room_title_members" = "%@名のメンバー"; -"room_title_one_member" = "1名のメンバー"; +"room_title_invite_members" = "メンバーを招待"; +"room_title_members" = "%@人のメンバー"; +"room_title_one_member" = "1人のメンバー"; // Room Preview -"room_preview_invitation_format" = "あなたは%@さんに呼ばれてこのルームへ参加しました"; +"room_preview_invitation_format" = "%@があなたをこのルームに招待しました。"; "room_preview_subtitle" = "現在表示しているのはルームのプレビューです。メッセージの送信などは行えません。"; -"room_preview_unlinked_email_warning" = "このアカウントに関連付けられていない%@宛に招待が送信されました。別のアカウントでログインするか、メールアドレスをこのアカウントに追加することができます。"; -"room_preview_try_join_an_unknown_room" = "%@ に参加しますか?"; +"room_preview_unlinked_email_warning" = "この招待は、このアカウントに関連付けられていない%@に送信されました。別のアカウントでログインするか、このメールアドレスを自分のアカウントに追加してください。"; +"room_preview_try_join_an_unknown_room" = "%@にアクセスしようとしています。この会話に参加しますか?"; "room_preview_try_join_an_unknown_room_default" = "ルーム"; // Settings "settings_title" = "設定"; -"account_logout_all" = "全てのアカウントを回線切断"; +"account_logout_all" = "全てのアカウントをログアウト"; "settings_config_no_build_info" = "ビルド情報がありません"; -"settings_mark_all_as_read" = "全ての発言を既読にする"; +"settings_mark_all_as_read" = "全てのメッセージを既読にする"; "settings_report_bug" = "バグレポート"; -"settings_config_home_server" = "接続先サーバーは %@"; -"settings_config_identity_server" = "認証サーバは %@"; +"settings_config_home_server" = "ホームサーバーは %@ です"; +"settings_config_identity_server" = "IDサーバー:%@"; "settings_config_user_id" = "%@でログインしています"; -"settings_user_settings" = "利用者設定"; +"settings_user_settings" = "ユーザー設定"; "settings_notifications_settings" = "通知設定"; "settings_calls_settings" = "通話"; "settings_user_interface" = "端末操作表示"; -"settings_ignored_users" = "無視する相手"; -"settings_contacts" = "端末の電話帳"; -"settings_advanced" = "拡張設定"; +"settings_ignored_users" = "無視しているユーザー"; +"settings_contacts" = "端末の連絡先"; +"settings_advanced" = "高度な設定"; "settings_other" = "その他"; -"settings_labs" = "実験的"; +"settings_labs" = "ラボ"; "settings_devices" = "セッション"; "settings_cryptography" = "暗号化"; "settings_sign_out" = "サインアウト"; -"settings_sign_out_confirmation" = "本当によろしいですか?"; -"settings_sign_out_e2e_warn" = "あなたはエンドツーエンド暗号鍵を失ってしまいます。この端末で暗号化されたルームの昔の発言を読むことができなくなります。"; +"settings_sign_out_confirmation" = "よろしいですか?"; +"settings_sign_out_e2e_warn" = "エンドツーエンド暗号鍵が消去されます。この端末では、暗号化されたルームの過去のメッセージを読むことができなくなってしまいます。"; "settings_profile_picture" = "プロフィール画像"; "settings_display_name" = "表示名"; "settings_first_name" = "名"; "settings_surname" = "姓"; "settings_remove_prompt_title" = "確認"; -"settings_remove_email_prompt_msg" = "メールアドレス %@ を本当に削除してよろしいですか?"; -"settings_remove_phone_prompt_msg" = "電話番号 %@ を本当に削除してよろしいですか?"; -"settings_email_address" = "電子メール"; +"settings_remove_email_prompt_msg" = "メールアドレス %@ を削除してよろしいですか?"; +"settings_remove_phone_prompt_msg" = "電話番号 %@ を削除してよろしいですか?"; +"settings_email_address" = "メールアドレス"; "settings_email_address_placeholder" = "あなたのメールアドレスを入力してください"; "settings_add_email_address" = "メールアドレスを追加"; "settings_phone_number" = "電話番号"; "settings_add_phone_number" = "電話番号を追加"; "settings_night_mode" = "夜間おやすみモード"; -"settings_fail_to_update_profile" = "自己紹介設定の更新に失敗しました"; +"settings_fail_to_update_profile" = "プロフィールの更新に失敗しました"; "settings_enable_push_notif" = "この端末での通知"; -"settings_show_decrypted_content" = "復号化された文章を表示"; -"settings_global_settings_info" = "あなたの%@ webクライアント上で、全体の通知設定が可能です"; -"settings_pin_rooms_with_missed_notif" = "逃した通知があるルームを固定"; +"settings_show_decrypted_content" = "復号化された内容を表示"; +"settings_global_settings_info" = "全体の通知設定は %@ webクライアントで行えます"; +"settings_pin_rooms_with_missed_notif" = "逃した通知があるルームをピン止め"; "settings_ui_language" = "言語"; -"settings_ui_theme" = "外観"; +"settings_ui_theme" = "テーマ"; "settings_ui_theme_auto" = "自動"; "settings_ui_theme_light" = "ライト"; "settings_ui_theme_dark" = "ダーク"; "settings_ui_theme_picker_title" = "外観を選択"; "settings_ui_theme_picker_message" = "色反転設定の端末では、「自動」を使ってください"; -"settings_unignore_user" = "%@さんからのメッセージを見ますか?"; +"settings_unignore_user" = "%@さんからのメッセージを表示しますか?"; "settings_contacts_discover_matrix_users" = "電子メールと電話番号をユーザの検索に使用"; "settings_contacts_phonebook_country" = "電話帳の国番号"; "settings_labs_e2e_encryption" = "エンドツーエンド暗号化"; -"settings_labs_e2e_encryption_prompt_message" = "暗号化の設定を完了するためには再度ログインしてください。"; +"settings_labs_e2e_encryption_prompt_message" = "暗号化の設定を完了するには、再度ログインしてください。"; "settings_labs_matrix_apps" = "Matrixアプリ"; -"settings_labs_create_conference_with_jitsi" = "jitsiの会議通話を作成"; +"settings_labs_create_conference_with_jitsi" = "Jitsiで会議通話を作成"; "settings_version" = "バージョン %@"; -"settings_olm_version" = "Olmバージョン %@"; +"settings_olm_version" = "Olmのバージョン %@"; "settings_copyright" = "著作権"; "settings_term_conditions" = "利用規約"; -"settings_privacy_policy" = "個人情報保護方針"; -"settings_third_party_notices" = "外部ライブラリの規約"; +"settings_privacy_policy" = "プライバシーポリシー"; +"settings_third_party_notices" = "外部ライブラリーのライセンス"; "settings_send_crash_report" = "匿名利用状況と誤動作情報を送信"; -"settings_enable_rageshake" = "バグレポートのため端末を振る"; -"settings_clear_cache" = "一時保存を消去"; -"settings_change_password" = "パスワード変更"; -"settings_old_password" = "今までのパスワード"; +"settings_enable_rageshake" = "端末を振って不具合を報告"; +"settings_clear_cache" = "キャッシュを消去"; +"settings_change_password" = "パスワードを変更"; +"settings_old_password" = "以前のパスワード"; "settings_new_password" = "新しいパスワード"; -"settings_confirm_password" = "パスワード確認"; -"settings_fail_to_update_password" = "パスワードの更新に失敗しました"; -"settings_password_updated" = "あなたのパスワードは更新されました"; -"settings_crypto_device_name" = "セッション名: "; -"settings_crypto_device_id" = "\nセッションID: "; -"settings_crypto_device_key" = "\nセッションキー:\n"; +"settings_confirm_password" = "パスワードを確認"; +"settings_fail_to_update_password" = "Matrixのアカウントのパスワードの更新に失敗しました"; +"settings_password_updated" = "Matrixのアカウントのパスワードを更新しました"; +"settings_crypto_device_name" = "セッション名: "; +"settings_crypto_device_id" = "\nセッションID: "; +"settings_crypto_device_key" = "\nセッションキー:\n"; "settings_crypto_export" = "鍵をエクスポート"; -"settings_crypto_blacklist_unverified_devices" = "認証されたセッションのみで暗号化"; +"settings_crypto_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; // Room Details "room_details_title" = "ルームの詳細"; "room_details_people" = "メンバー"; "room_details_files" = "アップロード"; "room_details_settings" = "設定"; -"room_details_photo" = "ルームのアイコン画像"; +"room_details_photo" = "ルームの画像"; "room_details_room_name" = "ルーム名"; "room_details_topic" = "トピック"; "room_details_favourite_tag" = "お気に入り"; "room_details_low_priority_tag" = "低優先度"; -"room_details_mute_notifs" = "発言があっても通知しない"; -"room_details_direct_chat" = "対話"; +"room_details_mute_notifs" = "通知をミュート"; +"room_details_direct_chat" = "ダイレクトメッセージ"; "room_details_access_section" = "このルームにアクセスできる人は?"; "room_details_access_section_invited_only" = "招待された人のみ"; "room_details_access_section_anyone_apart_from_guest" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_no_address_warning" = "このルームへのリンクを作成するには、ルームのアドレスが必要です"; -"room_details_access_section_directory_toggle" = "ルーム一覧へ公開"; -"room_details_history_section" = "発言履歴を閲覧できる人"; +"room_details_access_section_directory_toggle" = "このルームをルーム一覧に掲載"; +"room_details_history_section" = "履歴を閲覧できる人は?"; "room_details_history_section_anyone" = "誰でも"; -"room_details_history_section_members_only" = "メンバーのみ (この設定を選択した時点から)"; +"room_details_history_section_members_only" = "メンバーのみ(この設定を選択した時点から)"; "room_details_history_section_members_only_since_invited" = "メンバーのみ(招待を送った時点から)"; -"room_details_history_section_members_only_since_joined" = "メンバーのみ (参加した時点から)"; -"room_details_history_section_prompt_title" = "個人情報の警告"; -"room_details_history_section_prompt_msg" = "発言履歴を読むことができる人の変更は、以後の発言にのみ適用されます。既存の発言履歴の可視性は変更されません。"; +"room_details_history_section_members_only_since_joined" = "メンバーのみ(参加した時点から)"; +"room_details_history_section_prompt_title" = "プライバシーに関する警告"; +"room_details_history_section_prompt_msg" = "履歴の閲覧権限に関する変更は、今後、このルームで表示されるメッセージにのみ適用されます。既存の履歴の見え方には影響しません。"; "room_details_addresses_section" = "アドレス"; "room_details_no_local_addresses" = "このルームにはローカルアドレスがありません"; "room_details_new_address" = "新しいアドレスを追加"; "room_details_new_address_placeholder" = "新しいアドレスを追加(例 #foo%@)"; -"room_details_addresses_invalid_address_prompt_title" = "不正なエイリアスのフォーマット"; -"room_details_addresses_invalid_address_prompt_msg" = "%@はエイリアスの正しいフォーマットではありません"; +"room_details_addresses_invalid_address_prompt_title" = "エイリアスの形式が正しくありません"; +"room_details_addresses_invalid_address_prompt_msg" = "%@はエイリアスの正しい形式ではありません"; "room_details_addresses_disable_main_address_prompt_title" = "メインアドレスの警告"; -"room_details_addresses_disable_main_address_prompt_msg" = "メインアドレスが設定されていません。このルームのメインアドレスは無作為に選択、設定されます"; +"room_details_addresses_disable_main_address_prompt_msg" = "メインアドレスが設定されていません。このルームのメインアドレスはランダムに設定されます"; "room_details_banned_users_section" = "ブロックされたユーザー"; -"room_details_advanced_section" = "拡張設定"; -"room_details_advanced_room_id" = "ルームの固有ID:"; -"room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告: 有効後にこれを無効にすることはできません!)"; -"room_details_advanced_e2e_encryption_enabled" = "このルームの発言は暗号化されています"; -"room_details_advanced_e2e_encryption_disabled" = "このルームの発言は暗号化されていません。"; -"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証されたセッションのみで暗号化"; +"room_details_advanced_section" = "高度な設定"; +"room_details_advanced_room_id" = "ルームID:"; +"room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告:有効にした後に無効にすることはできません!)"; +"room_details_advanced_e2e_encryption_enabled" = "このルームでは暗号化が有効です"; +"room_details_advanced_e2e_encryption_disabled" = "このルームでは暗号化が有効ではありません。"; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; "room_details_fail_to_update_avatar" = "ルームのアイコン画像の更新に失敗"; "room_details_fail_to_update_room_name" = "ルーム名の更新に失敗"; -"room_details_fail_to_update_topic" = "ルームの説明の更新に失敗"; +"room_details_fail_to_update_topic" = "トピックの更新に失敗"; "room_details_fail_to_update_room_guest_access" = "ゲストによるルームへのアクセスの設定更新に失敗"; "room_details_fail_to_update_room_join_rule" = "参加ルールの更新に失敗"; -"room_details_fail_to_update_room_directory_visibility" = "ルーム一覧の可視設定の更新に失敗"; -"room_details_fail_to_update_history_visibility" = "発言履歴の可視範囲の設定更新に失敗"; +"room_details_fail_to_update_room_directory_visibility" = "ルーム一覧の見え方の更新に失敗"; +"room_details_fail_to_update_history_visibility" = "履歴の見え方の設定更新に失敗"; "room_details_fail_to_add_room_aliases" = "新しいルームアドレスの追加に失敗"; "room_details_fail_to_remove_room_aliases" = "ルームアドレスの削除に失敗"; "room_details_fail_to_update_room_canonical_alias" = "メインアドレスの更新に失敗"; -"room_details_fail_to_update_room_direct" = "ルームの対話タグの変更に失敗"; -"room_details_fail_to_enable_encryption" = "ルームの暗号化の開始に失敗"; +"room_details_fail_to_update_room_direct" = "このルームのダイレクトフラグのアップデートに失敗"; +"room_details_fail_to_enable_encryption" = "このルームの暗号化の有効化に失敗"; "room_details_save_changes_prompt" = "変更を保存しますか?"; -"room_details_set_main_address" = "メインアドレスを設定"; +"room_details_set_main_address" = "メインアドレスに設定"; "room_details_unset_main_address" = "メインアドレスの設定を解除"; -"room_details_copy_room_id" = "ルーム固有IDをコピー"; +"room_details_copy_room_id" = "ルームIDをコピー"; "room_details_copy_room_address" = "ルームのアドレスをコピー"; "room_details_copy_room_url" = "ルームのURLをコピー"; // Read Receipts "read_receipts_list" = "既読一覧を見る"; "receipt_status_read" = "既読状況: "; // Media picker -"media_picker_library" = "ライブラリ"; +"media_picker_library" = "ライブラリー"; "media_picker_select" = "選択"; // Directory "directory_title" = "ルーム一覧"; -"directory_server_picker_title" = "ルーム一覧を選択"; -"directory_server_all_rooms" = "%@ サーバー上の全てのルーム"; +"directory_server_picker_title" = "ルームディレクトリーを選択"; +"directory_server_all_rooms" = "%@サーバー上の全てのルーム"; "directory_server_all_native_rooms" = "全てのMatrix連携ルーム"; -"directory_server_type_homeserver" = "公開ルーム一覧を表示するための接続サーバーを入力してください"; +"directory_server_type_homeserver" = "公開ルームの一覧を表示するホームサーバーを入力してください"; "directory_server_placeholder" = "matrix.org"; // Events formatter -"event_formatter_member_updates" = "%tu権限が変更されました"; -"event_formatter_widget_added" = "%@ウィジェットが %@ さんにより追加されました"; -"event_formatter_widget_removed" = "%@ウィジェットが %@ さんにより削除されました"; -"event_formatter_jitsi_widget_added" = "音声会議が%@ さんにより追加されました"; -"event_formatter_jitsi_widget_removed" = "音声会議が%@ さんにより削除されました"; +"event_formatter_member_updates" = "%tu個のメンバーシップの変更"; +"event_formatter_widget_added" = "%@のウィジェットが%@により追加されました"; +"event_formatter_widget_removed" = "%@のウィジェットが%@により削除されました"; +"event_formatter_jitsi_widget_added" = "VoIP会議が%@により追加されました"; +"event_formatter_jitsi_widget_removed" = "VoIP会議が%@により削除されました"; // Others "or" = "または"; "you" = "あなた"; @@ -404,89 +404,89 @@ "yesterday" = "昨日"; "network_offline_prompt" = "インターネットへの接続が切れているようです。"; "public_room_section_title" = "公開ルーム(%@ にて):"; -"bug_report_prompt" = "前回アプリが異常終了しました。バグレポートを送信しますか?"; -"rage_shake_prompt" = "あなたは不満があって端末を揺らしているようです。バグレポートをしますか?"; +"bug_report_prompt" = "前回アプリケーションがクラッシュしました。クラッシュレポートを送信しますか?"; +"rage_shake_prompt" = "あなたは不満で端末を振っているようです。バグレポートを報告しますか?"; "do_not_ask_again" = "再び表示しない"; -"camera_access_not_granted" = "%@はカメラを使用する権限を持っていません。個人情報保護設定の変更をお願いします"; +"camera_access_not_granted" = "%@にはカメラを使用する権限がありません。プライバシー設定を変更してください"; "large_badge_value_k_format" = "%.1fK"; // room display name "room_displayname_room_invite" = "招待"; -"room_displayname_two_members" = "%@ と %@"; +"room_displayname_two_members" = "%@と%@"; "room_displayname_no_title" = "だれもいない部屋"; // Call -"call_incoming_voice_prompt" = "%@ さんから通話の着信中"; -"call_incoming_video_prompt" = "%@ さんから映像つき通話の着信中"; +"call_incoming_voice_prompt" = "%@から通話の着信中"; +"call_incoming_video_prompt" = "%@からビデオ通話の着信中"; "call_incoming_voice" = "着信中…"; "call_incoming_video" = "ビデオ通話の着信中…"; "call_already_displayed" = "既に通話中です。"; -"call_jitsi_error" = "会議通話への参加に失敗しました。"; +"call_jitsi_error" = "グループ通話への参加に失敗しました。"; // No VoIP support -"no_voip_title" = "通話着信中"; -"no_voip" = "%@さんから通話の着信がありましたが、%@は通話をまだサポートしていません。\nこの通知を無視して、別の端末から着信に応答することも、拒否することもできます。"; +"no_voip_title" = "着信中"; +"no_voip" = "%@があなたを呼び出していますが、%@はまだ通話をサポートしていません。\nこの通知を無視して別の端末から着信に応答することも、または着信を拒否することもできます。"; // Crash report // Crypto "e2e_need_log_in_again" = "再度ログインして、このセッションのエンドツーエンド暗号鍵を生成し、公開鍵をホームサーバーに送信する必要があります。\nご迷惑をおかけしますが、ご了承ください。"; // Bug report "bug_report_title" = "バグレポート"; -"bug_report_description" = "誤動作の内容と状況の説明をお願い致します。あなたは何をしましたか?何が起こると思いますか?実際何が起こったのですか?"; -"bug_crash_report_title" = "異常終了報告"; -"bug_crash_report_description" = "異常停止する前にあなたがしていたことを記してください:"; -"bug_report_logs_description" = "開発者が問題を診断するために、このElementのログがバグレポートと一緒に送信されます。上記文章のみを送信したい場合は以下のチェックを解除してください:"; +"bug_report_description" = "不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか?"; +"bug_crash_report_title" = "クラッシュレポート"; +"bug_crash_report_description" = "クラッシュする前にあなたがしていたことを記してください:"; +"bug_report_logs_description" = "開発者が問題を診断するために、このElementのログがバグレポートと一緒に送信されます。上記の文章のみを送信したい場合は、以下のチェックを解除してください:"; "bug_report_send_logs" = "ログを送信"; -"bug_report_send_screenshot" = "画面のスクリーンショット画像を送信"; -"bug_report_progress_zipping" = "ログを収集"; +"bug_report_send_screenshot" = "スクリーンショットの画像を送信"; +"bug_report_progress_zipping" = "ログを収集しています"; "bug_report_progress_uploading" = "報告を送信しています"; "bug_report_send" = "送信"; // Widget -"widget_no_power_to_manage" = "あなたがこのルームでウィジェットを管理するための権限が必要です"; +"widget_no_power_to_manage" = "このルームでウィジェットを管理するための権限が必要です"; "widget_creation_failure" = "ウィジェットの作成に失敗しました"; // Widget Integration Manager "widget_integration_need_to_be_able_to_invite" = "それを行うにはユーザーを招待する権限が必要です。"; "widget_integration_unable_to_create" = "ウィジェットを作成できません。"; "widget_integration_failed_to_send_request" = "リクエストの送信に失敗しました。"; -"widget_integration_room_not_recognised" = "このルームでは認められません。"; -"widget_integration_positive_power_level" = "権限の数値は正の整数で入力してください。"; -"widget_integration_must_be_in_room" = "あなたはこのルームに所属していません。"; -"widget_integration_no_permission_in_room" = "あなたはこのルームで権限がありません。"; -"widget_integration_missing_room_id" = "ルーム固有IDの要求に失敗しました。"; -"widget_integration_missing_user_id" = "ユーザー固有IDの要求に失敗しました。"; +"widget_integration_room_not_recognised" = "このルームは認識されていません。"; +"widget_integration_positive_power_level" = "権限レベルは正の整数でなければなりません。"; +"widget_integration_must_be_in_room" = "あなたはこのルームのメンバーではありません。"; +"widget_integration_no_permission_in_room" = "このルームでそれを行う権限がありません。"; +"widget_integration_missing_room_id" = "リクエストにroom_idがありません。"; +"widget_integration_missing_user_id" = "リクエストにuser_idがありません。"; "widget_integration_room_not_visible" = "ルーム %@ は見えません。"; // Share extension "share_extension_auth_prompt" = "メインのアプリにログインしてコンテンツを共有"; -"share_extension_failed_to_encrypt" = "送信に失敗しました。このルームの暗号設定をメインの端末で確認して下さい"; +"share_extension_failed_to_encrypt" = "送信に失敗しました。このルームの暗号設定をメインの端末で確認してください"; "room_details_advanced_e2e_encryption_prompt_message" = "End-to-end暗号化は実験的なものであり、信頼性が低い場合があります。\n\n発言を保護するためにはまだそれを信用すべきではありません。\n\n端末は、まだ参加する前の発言履歴を復号化することはできません。\n\n部屋の暗号化が今から有効になったら、もう無効にすることはできません。\n\n暗号化された発言は、まだ暗号化を実装していないアプリでは表示されません。"; "settings_enable_callkit" = "呼び出しの統合"; -"settings_pin_rooms_with_unread" = "未読のあるルームを固定"; +"settings_pin_rooms_with_unread" = "未読メッセージがあるルームをピン止め"; "title_groups" = "コミュニティー"; "room_recents_server_notice_section" = "システムアラート"; // Groups tab "group_invite_section" = "招待"; "group_section" = "コミュニティー"; -"room_message_reply_to_placeholder" = "返信を送る(暗号化されていない)…"; +"room_message_reply_to_placeholder" = "返信を送る(暗号化されていません)…"; "room_do_not_have_permission_to_post" = "このルームに投稿する権限がありません"; -"encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送信…"; +"encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送る…"; "room_message_reply_to_short_placeholder" = "返信を送る…"; -"room_event_action_view_decrypted_source" = "復号化されたソースを見る"; +"room_event_action_view_decrypted_source" = "復号化されたソースコードを表示"; "room_event_action_kick_prompt_reason" = "このユーザーを追放する理由"; -"room_action_send_photo_or_video" = "写真か動画を送る"; -"room_action_send_sticker" = "スタンプ送信"; +"room_action_send_photo_or_video" = "写真または動画を送信"; +"room_action_send_sticker" = "ステッカーを送信"; "room_replacement_information" = "このルームは置き換えられており、アクティブではありません。"; -"room_replacement_link" = "こちらから継続中の会話を確認する。"; +"room_replacement_link" = "こちらから継続中の会話を確認。"; "room_predecessor_information" = "このルームは別の会話の続きです。"; -"room_predecessor_link" = "以前のメッセージを見るには、ここをタップしてください。"; -"room_resource_limit_exceeded_message_contact_2_link" = "サービス管理者に連絡"; +"room_predecessor_link" = "ここをタップすると、以前のメッセージを表示します。"; +"room_resource_limit_exceeded_message_contact_2_link" = "サービス管理者に連絡してください"; "room_resource_limit_exceeded_message_contact_3" = " このサービスの使用を継続するには。"; -"room_resource_usage_limit_reached_message_1_default" = "このホームサーバーはリソース制限の1つを超えています "; -"room_resource_usage_limit_reached_message_1_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数制限を超えています "; +"room_resource_usage_limit_reached_message_1_default" = "このホームサーバーはリソースの上限に達しました "; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数の上限に達しました "; "room_resource_usage_limit_reached_message_2" = "一部のユーザーはログインできなくなります。"; "room_resource_usage_limit_reached_message_contact_3" = " この制限を増やすには。"; -"settings_deactivate_account" = "無効化したアカウント"; +"settings_deactivate_account" = "アカウントの無効化"; "settings_labs_room_members_lazy_loading" = "遅延ロードルームのメンバー"; "settings_labs_room_members_lazy_loading_error_message" = "あなたのホームサーバーはまだルームメンバーの遅延ロードをサポートしていません。 後で試してください。"; -"settings_deactivate_my_account" = "アカウントを無効にします"; +"settings_deactivate_my_account" = "アカウントを永久に無効にする"; "room_details_flair_section" = "コミュニティーの特色を表示"; "room_details_new_flair_placeholder" = "新しいコミュニティーIDを追加(例 +foo%@)"; -"room_details_flair_invalid_id_prompt_title" = "無効な形式"; +"room_details_flair_invalid_id_prompt_title" = "不正な形式です"; "room_details_flair_invalid_id_prompt_msg" = "%@はコミュニティーの有効な識別子ではありません"; "room_details_fail_to_update_room_communities" = "関連するコミュニティーの更新に失敗"; // Group Details @@ -495,15 +495,15 @@ "group_details_people" = "連絡先"; "group_details_rooms" = "ルーム"; // Group Home -"group_home_one_member_format" = "1名のメンバー"; -"group_home_multi_members_format" = "%tu名のメンバー"; -"group_home_one_room_format" = "1つのルーム"; -"group_home_multi_rooms_format" = "%tuつのルーム"; +"group_home_one_member_format" = "1人のメンバー"; +"group_home_multi_members_format" = "%tu人のメンバー"; +"group_home_one_room_format" = "1個のルーム"; +"group_home_multi_rooms_format" = "%tu個のルーム"; "group_invitation_format" = "%@がこのコミュニティーにあなたを招待しました"; // Group participants "group_participants_add_participant" = "参加者を追加"; -"group_participants_leave_prompt_title" = "グループを退出"; -"group_participants_leave_prompt_msg" = "グループを退出してよろしいですか?"; +"group_participants_leave_prompt_title" = "グループから退出"; +"group_participants_leave_prompt_msg" = "グループから退出してよろしいですか?"; "group_participants_remove_prompt_title" = "確認"; "group_participants_remove_prompt_msg" = "このグループから%@を削除してよろしいですか?"; "group_participants_invite_prompt_title" = "確認"; @@ -514,36 +514,36 @@ "group_participants_invite_malformed_id" = "不正なID。'@localpart:domain'のようなMatrix IDでなければなりません"; "group_participants_invited_section" = "招待中"; // Group rooms -"group_rooms_filter_rooms" = "コミュニティールームを絞り込む"; +"group_rooms_filter_rooms" = "コミュニティーのルームを絞り込む"; "event_formatter_rerequest_keys_part1_link" = "暗号鍵を再要求"; "event_formatter_rerequest_keys_part2" = " あなたの他のセッションに。"; "homeserver_connection_lost" = "ホームサーバーに接続できませんでした。"; -"widget_sticker_picker_no_stickerpacks_alert" = "現在、ステッカーパックを有効にしていません。"; +"widget_sticker_picker_no_stickerpacks_alert" = "現在、ステッカーパックが有効になっていません。"; "widget_sticker_picker_no_stickerpacks_alert_add_now" = "今すぐ追加しますか?"; // Room key request dialog "e2e_room_key_request_title" = "暗号鍵の要求"; -"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しい端末 '%@' を追加しました。"; -"e2e_room_key_request_message" = "認証されていない端末 '%@' が暗号鍵を要求しています。"; -"e2e_room_key_request_start_verification" = "認証を始めます…"; -"e2e_room_key_request_share_without_verifying" = "認証せずに共有"; +"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; +"e2e_room_key_request_message" = "未認証のセッション'%@'が暗号鍵を要求しています。"; +"e2e_room_key_request_start_verification" = "認証を開始…"; +"e2e_room_key_request_share_without_verifying" = "認証せず共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR -"gdpr_consent_not_given_alert_message" = "%@ホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; +"gdpr_consent_not_given_alert_message" = "%@のホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; "gdpr_consent_not_given_alert_review_now_action" = "確認"; -"deactivate_account_title" = "無効なアカウント"; -"deactivate_account_informations_part1" = "これにより、アカウントは永久に使用できなくなります。ログインすることはできず、誰も同じユーザーIDを再登録することはできません。これにより、あなたのアカウントは参加している全てのルームから退去し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; -"deactivate_account_informations_part2_emphasize" = "この動作は元に戻せません。"; -"deactivate_account_informations_part3" = "\n\nアカウントの無効化 "; -"deactivate_account_informations_part4_emphasize" = "デフォルトではあなたが送信したメッセージを忘れることはありません。 "; -"deactivate_account_informations_part5" = "メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; -"deactivate_account_forget_messages_information_part1" = "アカウントが無効になったときに送信した全てのメッセージを忘れてください ("; +"deactivate_account_title" = "アカウントの無効化"; +"deactivate_account_informations_part1" = "この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; +"deactivate_account_informations_part2_emphasize" = "この操作は取り消せません。"; +"deactivate_account_informations_part3" = "\n\nアカウントを無効化しても、 "; +"deactivate_account_informations_part4_emphasize" = "デフォルトではあなたが送信したメッセージの履歴は消去されません。 "; +"deactivate_account_informations_part5" = "メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; +"deactivate_account_forget_messages_information_part1" = "アカウントを無効化する際、全ての送信済のメッセージを消去("; "deactivate_account_forget_messages_information_part2_emphasize" = "警告"; -"deactivate_account_forget_messages_information_part3" = ":これは将来のユーザーに会話の不完全なビューが表示される)"; -"deactivate_account_validate_action" = "無効なアカウント"; -"deactivate_account_password_alert_title" = "無効なアカウント"; -"deactivate_account_password_alert_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; +"deactivate_account_forget_messages_information_part3" = ":今後のユーザーには、不完全な会話が表示されます)"; +"deactivate_account_validate_action" = "アカウントを無効化"; +"deactivate_account_password_alert_title" = "アカウントの無効化"; +"deactivate_account_password_alert_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; // Re-request confirmation dialog -"rerequest_keys_alert_title" = "要求が送信されました"; +"rerequest_keys_alert_title" = "要求を送信しました"; "rerequest_keys_alert_message" = "鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で%@を起動してください。"; "room_event_action_ban_prompt_reason" = "このユーザーをブロックする理由"; "room_resource_limit_exceeded_message_contact_1" = " お願い "; @@ -554,85 +554,85 @@ "close" = "閉じる"; // Accessibility "accessibility_checkbox_label" = "チェックボックス"; -"auth_login_single_sign_on" = "シングルサインオン(SSO)でサインイン"; +"auth_login_single_sign_on" = "サインイン"; "auth_softlogout_clear_data_sign_out" = "サインアウト"; -"room_message_unable_open_link_error_message" = "リンクを開くことができません。"; -"user_verification_session_details_verify_action_other_user" = "手動で確認"; +"room_message_unable_open_link_error_message" = "リンクを開けません。"; +"user_verification_session_details_verify_action_other_user" = "手動で認証"; "room_info_list_section_other" = "その他"; "room_info_list_several_members" = "%@人のメンバー"; // MARK: - Room Info -"room_info_list_one_member" = "1名のメンバー"; +"room_info_list_one_member" = "1人のメンバー"; "create_room_placeholder_address" = "#testroom:matrix.org"; "create_room_section_header_address" = "アドレス"; "create_room_show_in_directory" = "ルーム一覧に掲載"; "create_room_section_footer_type" = "非公開のルームは、ルームに招待された人のみ参加できます。"; -"create_room_type_public" = "公開ルーム (誰でも参加可能)"; -"create_room_type_private" = "非公開ルーム (招待者のみ参加可能)"; +"create_room_type_public" = "公開ルーム(誰でも参加可能)"; +"create_room_type_private" = "非公開のルーム(招待者のみ参加可能)"; "create_room_section_header_type" = "アクセスできる人"; "create_room_section_footer_encryption" = "暗号化はあとから無効にすることはできません。"; -"create_room_section_header_encryption" = "ルームの暗号化"; -"create_room_placeholder_topic" = "トピック"; -"create_room_section_header_topic" = "ルームのトピック(任意)"; +"create_room_section_header_encryption" = "暗号化"; +"create_room_placeholder_topic" = "ルームのトピックを入力してください"; +"create_room_section_header_topic" = "トピック(任意)"; "create_room_placeholder_name" = "名前"; -"create_room_section_header_name" = "ルーム名"; +"create_room_section_header_name" = "名前"; // MARK: - Create Room "create_room_title" = "新しいルーム"; "create_room_enable_encryption" = "暗号化を有効にする"; "room_details_room_name_for_dm" = "名前"; -"room_participants_security_information_room_encrypted_for_dm" = "ここで送受信されるメッセージはエンドツーエンド暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; -"room_participants_security_information_room_not_encrypted_for_dm" = "ここでのメッセージはエンドツーエンド暗号化されていません。"; +"room_participants_security_information_room_encrypted_for_dm" = "ここでのメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; +"room_participants_security_information_room_not_encrypted_for_dm" = "ここでのメッセージはエンドツーエンドで暗号化されていません。"; // Mark: - Room creation introduction cell -"room_intro_cell_add_participants_action" = "参加者を追加"; -"room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; -"room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されていません。"; +"room_intro_cell_add_participants_action" = "連絡先を追加"; +"room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; +"room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されていません。"; "room_intro_cell_information_dm_sentence1_part3" = "とのダイレクトメッセージの始まりです。 "; "callbar_active_and_single_paused" = "1つのアクティブな通話(%@)· 1つの一時停止された通話"; // Call Bar "callbar_only_single_active" = "タップして通話(%@)に戻る"; "settings_add_3pid_password_title_msidsn" = "電話番号を追加"; -"device_verification_emoji_scissors" = "ハサミ"; -"device_verification_emoji_paperclip" = "ペーパークリップ"; +"device_verification_emoji_scissors" = "はさみ"; +"device_verification_emoji_paperclip" = "クリップ"; "device_verification_emoji_pencil" = "鉛筆"; "device_verification_emoji_book" = "本"; "device_verification_emoji_light bulb" = "電球"; "device_verification_emoji_gift" = "ギフト"; "device_verification_emoji_clock" = "時計"; -"device_verification_emoji_hourglass" = "スバ時計"; -"device_verification_emoji_umbrella" = "雨"; -"device_verification_emoji_thumbs up" = "親指を立てる"; +"device_verification_emoji_hourglass" = "砂時計"; +"device_verification_emoji_umbrella" = "傘"; +"device_verification_emoji_thumbs up" = "いいね"; "device_verification_emoji_spanner" = "スパナ"; "device_verification_emoji_santa" = "サンタ"; -"device_verification_emoji_glasses" = "メガネ"; -"device_verification_emoji_hat" = "ハット"; +"device_verification_emoji_glasses" = "めがね"; +"device_verification_emoji_hat" = "帽子"; "device_verification_emoji_robot" = "ロボット"; -"device_verification_emoji_smiley" = "笑顔"; +"device_verification_emoji_smiley" = "スマイル"; "device_verification_emoji_heart" = "ハート"; "device_verification_emoji_cake" = "ケーキ"; "device_verification_emoji_pizza" = "ピザ"; // Room widget permissions "room_widget_permission_title" = "ウィジェットを読み込む"; -"widget_picker_manage_integrations" = "インテグレーションを管理する…"; +"widget_picker_manage_integrations" = "インテグレーションを管理…"; // Widget Picker -"widget_picker_title" = "インテグレーションマネージャー"; +"widget_picker_title" = "インテグレーション(統合)"; "widget_integration_manager_disabled" = "設定でインテグレーションマネージャーを有効にする必要があります"; -"widget_menu_remove" = "全て取り除く"; +"widget_menu_remove" = "全員から削除"; "widget_menu_revoke_permission" = "アクセスを取り消す"; "widget_menu_open_outside" = "ブラウザーで開く"; -"widget_menu_refresh" = "リフレッシュ"; -"widget_integrations_server_failed_to_connect" = "インテグレーションサーバーへの接続が失敗しました"; +"widget_menu_refresh" = "再読み込み"; +"widget_integrations_server_failed_to_connect" = "インテグレーションサーバーへの接続に失敗しました"; // Widget "widget_no_integrations_server_configured" = "インテグレーションサーバーが設定されていません"; -"bug_report_background_mode" = "バックグラウンドで継続"; +"bug_report_background_mode" = "バックグラウンドで続行"; "e2e_key_backup_wrong_version_button_wasme" = "これはわたしです"; "e2e_key_backup_wrong_version_button_settings" = "設定"; "e2e_key_backup_wrong_version" = "メッセージの鍵の新しい安全なバックアップが検出されました。\n\nこれがあなたによるものではない場合は、設定から新しいパスフレーズを設定してください。"; @@ -640,93 +640,93 @@ // Key backup wrong version "e2e_key_backup_wrong_version_title" = "新しい鍵のバックアップ"; "call_no_stun_server_error_use_fallback_button" = "%@を使ってみてください"; -"call_actions_unhold" = "やり直す"; -"call_no_stun_server_error_message_2" = "代わりに、%@のパブリックサーバーを使用することもできますが、これは信頼性が低くあなたのIPアドレスがそのサーバーと共有されてしまいます。これは、設定から管理することができます"; -"call_no_stun_server_error_message_1" = "通話を確実に機能させるためには、ホームサーバー%@の管理者にTURNサーバーの設定を依頼してください。"; -"call_no_stun_server_error_title" = "サーバーの設定が間違っているため通話に失敗しました"; +"call_actions_unhold" = "再開"; +"call_no_stun_server_error_message_2" = "公開サーバー %@ を使用することもできますが、信頼性は低く、また、サーバーとIPアドレスが共有されます。これは設定画面からも管理できます"; +"call_no_stun_server_error_message_1" = "安定した通話のために、ホームサーバー %@ の管理者にTURNサーバーの設定を依頼してください。"; +"call_no_stun_server_error_title" = "サーバーの不正な設定のため通話に失敗しました"; "room_does_not_exist" = "%@は存在しません"; -"photo_library_access_not_granted" = "%@はフォトライブラリにアクセスする権限がありません"; -"camera_unavailable" = "お使いの端末ではカメラを利用できません"; +"photo_library_access_not_granted" = "%@にはフォトライブラリーにアクセスする権限がありません。プライバシー設定を変更してください"; +"camera_unavailable" = "この端末ではカメラを使用できません"; "event_formatter_widget_removed_by_you" = "ウィジェットを削除しました:%@"; -"event_formatter_jitsi_widget_removed_by_you" = "VoIPカンファレンスを削除しました"; -"event_formatter_jitsi_widget_added_by_you" = "VoIPカンファレンスを追加しました"; +"event_formatter_jitsi_widget_removed_by_you" = "VoIP会議を削除しました"; +"event_formatter_jitsi_widget_added_by_you" = "VoIP会議を追加しました"; // Events formatter with you "event_formatter_widget_added_by_you" = "ウィジェットを追加しました:%@"; "event_formatter_call_back" = "かけ直す"; "event_formatter_call_you_declined" = "通話を拒否しました"; "event_formatter_call_you_currently_in" = "通話中です"; -"event_formatter_call_has_ended" = "通話は有効です"; +"event_formatter_call_has_ended" = "通話が終了しました"; "event_formatter_call_video" = "ビデオ通話"; "event_formatter_call_voice" = "音声通話"; "event_formatter_message_edited_mention" = "(編集済)"; -"image_picker_action_library" = "ライブラリを選ぶ"; +"image_picker_action_library" = "ライブラリーから選択"; // Image picker -"image_picker_action_camera" = "写真を撮る"; +"image_picker_action_camera" = "写真を撮影"; // Media picker -"media_picker_title" = "メディアライブラリ"; -"room_details_advanced_e2e_encryption_disabled_for_dm" = "ここは暗号化が有効ではありません。"; -"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここは暗号化が有効です"; -"room_details_advanced_room_id_for_dm" = "ID:"; +"media_picker_title" = "メディアライブラリー"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "ここでは暗号化が有効ではありません。"; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効です"; +"room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; "room_details_access_section_anyone_apart_from_guest_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_for_dm" = "これにアクセスできる人は?"; -"room_details_photo_for_dm" = "写真"; -"room_details_integrations" = "インテグレーション"; +"room_details_photo_for_dm" = "画像"; +"room_details_integrations" = "インテグレーション(統合)"; "room_details_search" = "ルーム内検索"; "room_details_title_for_dm" = "詳細"; "identity_server_settings_alert_error_invalid_identity_server" = "%@は有効なIDサーバーではありません。"; -"identity_server_settings_alert_error_terms_not_accepted" = "IDサーバーとして設定するには%@の条件を受け入れる必要があります。"; -"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して切断"; -"identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー%@で個人データを共有しています。\n\n切断する前にメールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; -"identity_server_settings_alert_disconnect_button" = "接続を解除"; -"identity_server_settings_alert_disconnect" = "IDサーバー%@を接続解除しますか?"; -"identity_server_settings_alert_disconnect_title" = "IDサーバーを接続解除"; -"identity_server_settings_alert_change" = "IDサーバー%1$@を切断し、代わりに%2$@に接続しますか?"; +"identity_server_settings_alert_error_terms_not_accepted" = "IDサーバーに設定するには、%@の利用規約を承諾する必要があります。"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して接続解除"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー %@ で個人データを共有しています。\n\n接続を解除する前に、メールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; +"identity_server_settings_alert_disconnect_button" = "切断"; +"identity_server_settings_alert_disconnect" = "IDサーバー %@ から切断しますか?"; +"identity_server_settings_alert_disconnect_title" = "IDサーバーから切断"; +"identity_server_settings_alert_change" = "IDサーバー %1$@ から切断して %2$@ に接続しますか?"; "identity_server_settings_alert_change_title" = "IDサーバーを変更"; "identity_server_settings_alert_no_terms" = "選択したIDサーバーには利用規約がありません。そのサーバーの所有者を信頼できる場合にのみ続行してください。"; "identity_server_settings_alert_no_terms_title" = "IDサーバーには利用規約がありません"; -"identity_server_settings_disconnect" = "接続を解除"; -"identity_server_settings_disconnect_info" = "IDサーバーとの接続を解除すると、他のユーザーから発見されなくなり、メールや電話で他のユーザーを招待することができるようになります。"; +"identity_server_settings_disconnect" = "切断"; +"identity_server_settings_disconnect_info" = "IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。"; "identity_server_settings_change" = "変更"; "identity_server_settings_add" = "追加"; "identity_server_settings_place_holder" = "IDサーバーを入力"; -"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以上でIDサーバーを追加してください。"; -"identity_server_settings_description" = "あなたは%@を使って、あなたの知り合いを発見し、また向こうから発見できるようにしています。"; +"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; +"identity_server_settings_description" = "現在 %@ を使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。"; "security_settings_complete_security_alert_title" = "セキュリティーを確認"; "security_settings_crosssigning_complete_security" = "セキュリティーを確認"; "security_settings_crosssigning_bootstrap" = "設定"; -"settings_devices_description" = "セッションの公開名は、あなたとやり取りする人々に対して表示されます"; -"settings_key_backup_delete_confirmation_prompt_title" = "バックアップの削除"; +"settings_devices_description" = "セッションの公開名は、あなたとやり取りする連絡先に対して表示されます"; +"settings_key_backup_delete_confirmation_prompt_title" = "バックアップを削除"; "settings_key_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "settings_key_backup_info_algorithm" = "アルゴリズム:%@"; "settings_key_backup_info_version" = "鍵のバックアップのバージョン:%@"; -"settings_key_backup_info_none" = "あなたの鍵は、このセッションからバックアップされていません。"; +"settings_key_backup_info_none" = "鍵はこのセッションからバックアップされていません。"; "settings_key_backup_info_checking" = "確認しています…"; -"settings_add_3pid_password_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; -"settings_add_3pid_invalid_password_message" = "無効な認証情報"; +"settings_add_3pid_password_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; +"settings_add_3pid_invalid_password_message" = "認証情報が正しくありません"; "settings_add_3pid_password_title_email" = "メールアドレスを追加"; -"settings_integrations_allow_description" = "インテグレーションマネージャー(%@)を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。\n\n設定データを受け取り、お客様に代わってウィジェットの変更、ルーム招待の送信、権限の設定を行うことができます。"; +"settings_integrations_allow_description" = "インテグレーションマネージャー %@ を使用すると、ボット、ブリッジ、ウィジェット、ステッカーパックを管理できます。\n\n設定データを受信し、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。"; "settings_integrations_allow_button" = "インテグレーションを管理"; -"settings_calls_stun_server_fallback_button" = "フォールバックコールアシストサーバーを許可"; +"settings_calls_stun_server_fallback_button" = "フォールバック用の通話アシストサーバーを許可"; "settings_key_backup" = "鍵のバックアップ"; "settings_integrations" = "インテグレーション"; "settings_discovery_settings" = "ディスカバリー"; -"room_multiple_typing_notification" = "%@とその他のユーザーが入力中です"; +"room_multiple_typing_notification" = "%@とその他のメンバー"; "external_link_confirmation_message" = "リンク %@ は別のサイトに移動します:%@\n\n続行してよろしいですか?"; -"room_event_action_delete_confirmation_title" = "未送信メッセージを削除"; -"room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; -"room_unsent_messages_cancel_title" = "未送信メッセージを削除"; -"room_message_replying_to" = "%@に返信中"; +"room_event_action_delete_confirmation_title" = "未送信のメッセージを削除"; +"room_unsent_messages_cancel_message" = "このルームの全ての未送信のメッセージを削除してよろしいですか?"; +"room_unsent_messages_cancel_title" = "未送信のメッセージを削除"; +"room_message_replying_to" = "%@に返信しています"; "room_message_editing" = "編集中"; "room_accessiblity_scroll_to_bottom" = "いちばん下までスクロール"; -"room_member_power_level_short_custom" = "カスタム"; +"room_member_power_level_short_custom" = "ユーザー定義"; "room_member_power_level_short_moderator" = "モデレーター"; -"room_member_power_level_custom_in" = "カスタム (%@) in %@"; +"room_member_power_level_custom_in" = "ユーザー定義(%@):%@"; "room_member_power_level_short_admin" = "管理者"; "room_member_power_level_moderator_in" = "%@のモデレーター"; "room_member_power_level_admin_in" = "%@の管理者"; @@ -735,58 +735,58 @@ "room_participants_action_security_status_warning" = "警告"; "room_participants_action_security_status_complete_security" = "セキュリティーを確認"; "room_participants_action_security_status_verify" = "認証"; -"room_participants_action_security_status_verified" = "検証済み"; +"room_participants_action_security_status_verified" = "認証済"; "room_participants_action_section_security" = "セキュリティー"; -"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使って連絡先とチャットを開始することができません。"; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使用して連絡先とチャットを開始することはできません。"; "room_participants_filter_room_members_for_dm" = "メンバーを検索"; "room_participants_remove_third_party_invite_prompt_msg" = "招待を取り消してよろしいですか?"; "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; "room_participants_leave_prompt_title_for_dm" = "退出"; "contacts_address_book_no_identity_server" = "IDサーバーが設定されていません"; -"rooms_empty_view_information" = "ルームは非公開でも公開でも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ったりすることができます。"; +"rooms_empty_view_information" = "ルームは非公開でも公開でも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ったりすることができます。"; "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると連絡先を追加できます。"; "people_empty_view_title" = "連絡先"; -"room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールで参加者を追加することができません。"; +"room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールでは参加者を追加できません。"; // Errors -"error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようですね。サインアウトしますか?"; +"error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようです。サインアウトしますか?"; "social_login_button_title_sign_up" = "%@でサインアップ"; "social_login_button_title_sign_in" = "%@でサインイン"; -"social_login_button_title_continue" = "続きはこちら%@"; -"social_login_list_title_sign_up" = "もしくは"; -"social_login_list_title_sign_in" = "もしくは"; +"social_login_button_title_continue" = "%@で続行"; +"social_login_list_title_sign_up" = "または"; +"social_login_list_title_sign_in" = "または"; // Social login -"social_login_list_title_continue" = "続きはこちら"; -"auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?再びサインインするとアカウントデータやメッセージにアクセスできます。"; -"auth_softlogout_clear_data_sign_out_title" = "続行してよろしいですか?"; -"auth_softlogout_clear_data_button" = "全てのデータをクリア"; -"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; -"auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; -"callbar_return" = "かけ直す"; -"callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; -"callbar_only_multiple_paused" = "一時停止した%@の通話"; -"callbar_only_single_paused" = "通話の一時停止"; -"store_promotional_text" = "オープンネットワーク上でプライバシーを保護したチャットアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、サードパーティによるアクセスはありません。"; +"social_login_list_title_continue" = "次で続行"; +"auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?アカウントのデータやメッセージにアクセスするには、再びサインインしてください。"; +"auth_softlogout_clear_data_sign_out_title" = "よろしいですか?"; +"auth_softlogout_clear_data_button" = "全てのデータを消去"; +"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する、または別のアカウントにサインインする場合は、個人データを消去してください。"; +"auth_softlogout_clear_data_message_1" = "警告:あなたの個人データ(暗号鍵を含む)が、この端末にまだ保存されています。"; +"callbar_return" = "折り返す"; +"callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; +"callbar_only_multiple_paused" = "一時停止した%@件の通話"; +"callbar_only_single_paused" = "一時停止した通話"; +"store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者が勝手にデータにアクセスすることはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; -"auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; -"auth_softlogout_reason" = "ホームサーバー(%1$@)の管理者が%2$@(%3$@)からサインアウトさせました。"; +"auth_softlogout_recover_encryption_keys" = "暗号鍵はこの端末にのみ保存されています。保護されたメッセージをどの端末でも読むには、その暗号鍵が必要になります。サインインして暗号鍵を復元してください。"; +"auth_softlogout_reason" = "あなたのホームサーバー(%1$@)の管理者が、あなたをアカウント %2$@ (%3$@)からサインアウトさせました。"; "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; -"auth_autodiscover_invalid_response" = "無効なホームサーバー発見レスポンス"; -"auth_accept_policies" = "このホームサーバーのポリシーを確認して同意してください:"; -"auth_reset_password_error_is_required" = "IDサーバーが設定されていません:パスワードをリセットするためにサーバーオプションに追加してください。"; -"auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードをリセットするためにIDサーバーを追加してください。"; -"auth_phone_is_required" = "IDサーバーが設定されていないので、パスワードをリセットするために電話番号を追加することはできません。"; -"auth_email_is_required" = "IDサーバーが設定されていないので、パスワードをリセットするためにメールアドレスを追加することはできません。"; +"auth_autodiscover_invalid_response" = "ホームサーバーのディスカバリー(発見)に関する不正な応答です"; +"auth_accept_policies" = "このホームサーバーの運営方針を確認し、同意してください:"; +"auth_reset_password_error_is_required" = "IDサーバーが設定されていません:Matrixのアカウントのパスワードを再設定するためにサーバーオプションに追加してください。"; +"auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードを再設定するためにIDサーバーを追加してください。"; +"auth_phone_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用する電話番号を追加することができません。"; +"auth_email_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードを再設定する際に使用するメールアドレスを追加することができません。"; "auth_add_email_phone_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションでメールアドレスや電話番号を使用して知人に見つけてもらえるようにできます。"; "auth_add_phone_message_2" = "電話番号を設定します。後からオプションで知人に見つけてもらえるようにできます。"; "auth_add_email_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。"; "less" = "たたむ"; "more" = "もっと"; -"switch" = "切り替え"; +"switch" = "切り替える"; "joined" = "参加済"; "skip" = "スキップ"; @@ -795,7 +795,7 @@ // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "このアプリは、ホームサーバーの認証機構をサポートしていません。"; -"manage_session_sign_out" = "セッションからサインアウト"; +"manage_session_sign_out" = "このセッションからサインアウト"; "manage_session_not_trusted" = "信頼されていません"; "manage_session_trusted" = "信頼済"; "manage_session_name" = "セッション名"; @@ -803,75 +803,75 @@ // Manage session "manage_session_title" = "セッションを管理"; -"security_settings_user_password_description" = "アカウントのパスワードを入力して本人確認を行ってください"; +"security_settings_user_password_description" = "Matrixのアカウントのパスワードを入力して本人確認を行ってください"; "security_settings_complete_security_alert_message" = "現在のセッションのセキュリティーを完了させる必要があります。"; -"security_settings_blacklist_unverified_devices_description" = "全てのセッションを認証して、信頼できるものとしてマークしメッセージを送信します。"; +"security_settings_blacklist_unverified_devices_description" = "全てのセッションを認証し、信頼済としてマークしてメッセージを送信します。"; "security_settings_blacklist_unverified_devices" = "信頼していないセッションにはメッセージを送信しない"; -"security_settings_advanced" = "上級者向け"; +"security_settings_advanced" = "高度な設定"; "security_settings_export_keys_manually" = "手動で鍵をエクスポート"; -"security_settings_cryptography" = "暗号技術"; +"security_settings_cryptography" = "暗号化"; "security_settings_crosssigning_reset" = "クロス署名をリセット"; -"security_settings_crosssigning_info_ok" = "クロス署名が有効です。"; +"security_settings_crosssigning_info_ok" = "クロス署名を利用できます。"; "security_settings_crosssigning_info_trusted" = "クロス署名が有効になっています。クロス署名に基づいて他のユーザーや自分の他のセッションを信頼することはできますが、このセッションにはクロス署名用の秘密鍵がないため、このセッションからクロス署名を行うことはできません。このセッションのセキュリティーを完了してください。"; "security_settings_crosssigning_info_exists" = "アカウントにはクロス署名IDがありますが、このセッションはまだ信頼されていません。このセッションのセキュリティーを完了してください。"; -"security_settings_crosssigning_info_not_bootstrapped" = "クロス署名がまだ行われていません。"; +"security_settings_crosssigning_info_not_bootstrapped" = "クロス署名がまだ設定されていません。"; "security_settings_crosssigning" = "クロス署名"; "security_settings_backup" = "メッセージのバックアップ"; "security_settings_secure_backup_delete" = "バックアップの削除"; "security_settings_secure_backup_synchronise" = "同期"; "security_settings_secure_backup_setup" = "設定"; -"security_settings_secure_backup_description" = "セッションにアクセスできなくなる場合に備えて、アカウントデータと暗号鍵をバックアップします。鍵は一意のセキュリティーキーで保護されます。"; +"security_settings_secure_backup_description" = "セッションにアクセスできなくなる場合に備えて、アカウントデータと暗号鍵をバックアップしましょう。鍵は一意のセキュリティーキーで保護されます。"; "security_settings_secure_backup" = "安全なバックアップ"; -"security_settings_crypto_sessions_description_2" = "見覚えのないログインがある場合は、Matrixアカウントのパスワードを変更し、バックアップをリセットしてください。"; +"security_settings_crypto_sessions_description_2" = "見覚えのないログインがある場合は、Matrixのアカウントのパスワードを変更し、バックアップをリセットしてください。"; "security_settings_crypto_sessions_loading" = "セッションを読み込んでいます…"; "security_settings_crypto_sessions" = "セッション"; // Security settings "security_settings_title" = "セキュリティー"; "settings_show_NSFW_public_rooms" = "NSFWパブリックルームを表示"; -"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以上でIDサーバーを追加してください。"; +"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; "settings_identity_server_no_is" = "IDサーバーが設定されていません"; -"settings_identity_server_description" = "上記で設定したIDサーバーを使って、自分の知り合いを発見したり、発見されたりすることができます。"; +"settings_identity_server_description" = "上記で設定したIDサーバーを使うと、自分の連絡先を見つけたり、連絡先から見つけてもらったりすることができます。"; "settings_discovery_three_pid_details_enter_sms_code_action" = "SMSアクティベーションコードを入力"; "settings_discovery_three_pid_details_cancel_email_validation_action" = "メールの認証をキャンセル"; -"settings_discovery_three_pid_details_revoke_action" = "取り消し"; +"settings_discovery_three_pid_details_revoke_action" = "取り消す"; "settings_discovery_three_pid_details_share_action" = "共有"; "settings_discovery_three_pid_details_title_email" = "メールアドレスを管理"; "settings_discovery_three_pid_details_title_phone_number" = "電話番号を管理"; -"settings_discovery_three_pid_details_information_phone_number" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用できる電話番号の設定を管理します。アカウントへ電話番号の追加や削除ができます。"; -"settings_discovery_three_pid_details_information_email" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用できるメールアドレスの設定を管理します。アカウントへメールアドレスの追加や削除ができます。"; +"settings_discovery_three_pid_details_information_phone_number" = "他のユーザーがあなたを発見したり、ルームに招待したりする際に使用できる電話番号の設定を管理します。アカウント画面で電話番号を追加、削除できます。"; +"settings_discovery_three_pid_details_information_email" = "他のユーザーがあなたを発見したり、ルームに招待したりする際に使用できるメールアドレスの設定を管理します。アカウント画面でメールアドレスを追加、削除できます。"; "settings_discovery_error_message" = "エラーが発生しました。再試行してください。"; "settings_discovery_three_pids_management_information_part3" = "。"; "settings_discovery_three_pids_management_information_part2" = "ユーザー設定"; -"settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストにメールアドレスや電話番号を追加したり、削除したりすることができます。 "; -"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー(%@)の利用規約への同意が必要です。"; -"settings_discovery_no_identity_server" = "現在、IDサーバーを使用していません。あなたの知っている連絡先から発見されるようにするには、IDサーバーを追加してください。"; -"settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを失うことがあります。"; +"settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストに、メールアドレスや電話番号を追加したり、削除したりすることができます。 "; +"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを検出可能にするには、IDサーバー %@ の利用規約への同意が必要です。"; +"settings_discovery_no_identity_server" = "現在、IDサーバーを使用していません。連絡先から見つけてもらうようにするには、IDサーバーを追加してください。"; +"settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを読み取れなくなってしまいます。"; "settings_key_backup_button_connect" = "このセッションを鍵のバックアップに接続"; -"settings_key_backup_button_delete" = "バックアップの削除"; +"settings_key_backup_button_delete" = "バックアップを削除"; "settings_key_backup_button_restore" = "バックアップから復元"; "settings_key_backup_button_create" = "鍵のバックアップを使用開始"; "settings_key_backup_info_trust_signature_invalid_device_unverified" = "バックアップには%@による無効な署名があります"; "settings_key_backup_info_trust_signature_invalid_device_verified" = "バックアップには%@による無効な署名があります"; "settings_key_backup_info_trust_signature_valid_device_unverified" = "バックアップには%@による署名があります"; "settings_key_backup_info_trust_signature_valid_device_verified" = "バックアップには%@による有効な署名があります"; -"settings_key_backup_info_trust_signature_valid" = "バックアップにはこのセッションの有効な署名があります"; -"settings_key_backup_info_trust_signature_unknown" = "バックアップにはID:%@によるセッションの署名があります"; -"settings_key_backup_info_progress_done" = "全ての鍵がバックアップされています"; +"settings_key_backup_info_trust_signature_valid" = "バックアップにはこのセッションによる有効な署名があります"; +"settings_key_backup_info_trust_signature_unknown" = "バックアップには、ID:%@によるセッションの署名があります"; +"settings_key_backup_info_progress_done" = "全ての鍵をバックアップしました"; "settings_key_backup_info_progress" = "%@の鍵をバックアップしています…"; -"settings_key_backup_info_not_valid" = "このセッションでは鍵をバックアップしていませんが、復元に使用したり、今後鍵を追加したりできるバックアップを持っています。"; +"settings_key_backup_info_not_valid" = "このセッションでは鍵をバックアップしていませんが、復元に使用したり、鍵を今後追加したりできるバックアップを持っています。"; "settings_key_backup_info_signout_warning" = "鍵を失くさないよう、サインアウトする前にバックアップしてください。"; "settings_key_backup_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。"; "settings_labs_message_reaction" = "絵文字でメッセージに反応"; "settings_security" = "セキュリティー"; -"settings_three_pids_management_information_part3" = ""; -"settings_three_pids_management_information_part2" = "ディスカバリー"; -"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrixーーオープンな分散型通信の標準規格ーーで動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ(https://app.element.io)でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; +"settings_three_pids_management_information_part3" = "で設定しましょう。"; +"settings_three_pids_management_information_part2" = "ディスカバリー(発見)"; +"store_full_description" = "Elementは画期的なメッセンジャーアプリです。\n\n1. あなた自身が、プライバシーをコントロールできます。\n2. Matrixネットワークにいる誰とでもコミュニケーションできます。Slackなどのアプリと連携すれば、他のネットワークのユーザーともコミュニケーションを行うことができます。\n3. 広告やデータマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化と、クロス署名による認証で、コミュニケーションの安全性を確保します。\n\nElementは分散型(非中央集権型)でオープンソースのメッセンジャーアプリです。他のメッセンジャーアプリとは全く性質が異なります。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータや会話に関するプライバシーや、誰があなたのデータを所有するかは、あなた自身で定められます。さらに、Elementがアクセスするネットワークは、誰でも参加できるオープンなネットワークなので、Elementのユーザー以外ともコミュニケーションを行うことができます。しかもきわめて安全です。\n\nこれら全ては、ElementがMatrix――オープンな分散型通信の標準規格――で動作するために可能になっています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得。\n2. あなた自身がサーバーを運営し、アカウントを管理。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作成。\n\nElementを選ぶべき理由:\n\n自分のデータを、自分で所有:データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり、第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングと、コラボレーション:Matrixネットワーク上の誰とでも、メッセージのやり取りを行うことができます。Elementや他のMatrixアプリだけでなく、Slack、IRC、XMPPのような他のメッセージングシステムのユーザーとも、チャットをすることができます。\n\n非常に安全:本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを読み取ることができます)を備えています。また、クロス署名を行えば、会話に参加しているユーザーの端末が、本当にそのユーザーのものであるかを認証することができます。\n\n包括的なコミュニケーション:メッセージのやり取り、音声・ビデオ通話、ファイル共有、画面共有、その他多くの機能、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、タスクをスムーズに成し遂げましょう。\n\nいつでも、どこにいても:アプリをインストールしている全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; -"user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; -"user_verification_session_details_information_untrusted_current_user" = "このセッションを認証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; -"user_verification_session_details_information_trusted_other_user_part2" = " 検証済み:"; -"user_verification_session_details_information_trusted_other_user_part1" = "このセッションは安全なものとして信頼されています。なぜなら "; +"user_verification_session_details_information_untrusted_other_user" = " が新しいセッションを使ってサインインしました:"; +"user_verification_session_details_information_untrusted_current_user" = "このセッションを認証して信頼済としてマークし、暗号化されたメッセージへのアクセスを許可。"; +"user_verification_session_details_information_trusted_other_user_part2" = " が検証しました:"; +"user_verification_session_details_information_trusted_other_user_part1" = "このセッションは安全なものとして信頼されています。 "; "user_verification_session_details_information_trusted_current_user" = "このセッションは、認証されたため安全なものとして信頼されています。"; "user_verification_session_details_untrusted_title" = "信頼されていません"; @@ -880,17 +880,17 @@ "user_verification_session_details_trusted_title" = "信頼済"; "user_verification_sessions_list_session_untrusted" = "信頼されていません"; "user_verification_sessions_list_session_trusted" = "信頼済"; -"user_verification_sessions_list_table_title" = "セッション一覧"; -"user_verification_sessions_list_information" = "このルームにいるこのユーザーとのメッセージはエンドツーエンドで暗号化されており第三者が読み取ることはできません。"; -"user_verification_sessions_list_user_trust_level_unknown_title" = "未知"; +"user_verification_sessions_list_table_title" = "セッション"; +"user_verification_sessions_list_information" = "このルームにいるこのユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "不明"; "user_verification_sessions_list_user_trust_level_warning_title" = "警告"; // Sessions list "user_verification_sessions_list_user_trust_level_trusted_title" = "信頼済"; -"user_verification_start_additional_information" = "安心してご利用いただくために、直接お会いするか、別の方法でご連絡ください。"; -"user_verification_start_waiting_partner" = "%@を待っています…"; -"user_verification_start_information_part2" = " 両方の端末でワンタイムコードを確認します。"; +"user_verification_start_additional_information" = "セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。"; +"user_verification_start_waiting_partner" = "%@を待機しています…"; +"user_verification_start_information_part2" = " 両方の端末でワンタイムコードを確認し、認証してください。"; "user_verification_start_information_part1" = "セキュリティーを高めるために "; // MARK: - User verification @@ -902,23 +902,23 @@ "key_verification_scan_confirmation_scanned_user_information" = "%@は同じシールドを表示していますか?"; // Scanned -"key_verification_scan_confirmation_scanned_title" = "まもなくです!"; -"key_verification_scan_confirmation_scanning_device_waiting_other" = "他の端末を待っています…"; +"key_verification_scan_confirmation_scanned_title" = "もう少しです!"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "他の端末を待機しています…"; // MARK: Scan confirmation // Scanning -"key_verification_scan_confirmation_scanning_title" = "もう少しです。確認を待っています…"; -"key_verification_scan_confirmation_scanning_user_waiting_other" = "%@を待っています…"; -"key_verification_verify_qr_code_scan_other_code_success_message" = "QRコードの認証に成功しました。"; -"key_verification_verify_qr_code_scan_other_code_success_title" = "コードが有効になりました!"; -"key_verification_verify_qr_code_other_scan_my_code_title" = "相手がQRコードを読み取ってくれましたか?"; -"key_verification_verify_qr_code_start_emoji_action" = "絵文字による認証"; +"key_verification_scan_confirmation_scanning_title" = "もう少しです!確認を待機しています…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "%@を待機しています…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QRコードを正常に検証しました。"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "コードを検証しました!"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "相手がQRコードを正常に読み取りましたか?"; +"key_verification_verify_qr_code_start_emoji_action" = "絵文字で認証"; "key_verification_verify_qr_code_cannot_scan_action" = "スキャンできませんか?"; -"key_verification_verify_qr_code_scan_code_action" = "コードをスキャン"; +"key_verification_verify_qr_code_scan_code_action" = "コードをスキャンしてください"; "key_verification_verify_qr_code_emoji_information" = "絵文字の並びを比較して認証。"; -"key_verification_verify_qr_code_information_other_device" = "以下のコードをスキャンして確認してください:"; -"key_verification_verify_qr_code_information" = "コードをスキャンして、お互いをしっかりと確認します。"; +"key_verification_verify_qr_code_information_other_device" = "以下のコードをスキャンして認証してください:"; +"key_verification_verify_qr_code_information" = "コードをスキャンして、お互いを安全に認証しましょう。"; // MARK: QR code @@ -928,16 +928,16 @@ "key_verification_incoming_request_incoming_alert_message" = "%@は認証を要求しています"; "key_verification_tile_conclusion_warning_title" = "信頼されていないサインイン"; -"key_verification_tile_conclusion_done_title" = "検証済み"; -"key_verification_tile_request_incoming_approval_decline" = "却下"; -"key_verification_tile_request_incoming_approval_accept" = "承認"; -"key_verification_tile_request_status_accepted" = "あなたは承認しました"; +"key_verification_tile_conclusion_done_title" = "認証済"; +"key_verification_tile_request_incoming_approval_decline" = "拒否"; +"key_verification_tile_request_incoming_approval_accept" = "同意"; +"key_verification_tile_request_status_accepted" = "承認しました"; "key_verification_tile_request_status_cancelled" = "%@はキャンセルしました"; -"key_verification_tile_request_status_cancelled_by_me" = "あなたはキャンセルしました"; +"key_verification_tile_request_status_cancelled_by_me" = "キャンセルしました"; "key_verification_tile_request_status_expired" = "期限切れ"; -"key_verification_tile_request_status_waiting" = "お待ちください…"; -"key_verification_tile_request_status_data_loading" = "日時を読み込み…"; -"key_verification_tile_request_outgoing_title" = "認証を送信済"; +"key_verification_tile_request_status_waiting" = "待機しています…"; +"key_verification_tile_request_status_data_loading" = "日時を読み込んでいます…"; +"key_verification_tile_request_outgoing_title" = "認証を送信しました"; // Tiles @@ -951,28 +951,28 @@ // Generic errors -"error_invite_3pid_with_no_identity_server" = "メールで招待するために設定からIDサーバーを追加します。"; +"error_invite_3pid_with_no_identity_server" = "メールで招待するには、設定でIDサーバーを追加してください。"; // MARK: Reaction history "reaction_history_title" = "リアクションの履歴"; -"emoji_picker_places_category" = "旅と場所"; -"emoji_picker_flags_category" = "国旗"; +"emoji_picker_places_category" = "旅行と場所"; +"emoji_picker_flags_category" = "旗"; "emoji_picker_symbols_category" = "シンボル"; -"emoji_picker_objects_category" = "オブジェクト"; +"emoji_picker_objects_category" = "物体"; "emoji_picker_foods_category" = "食べ物と飲み物"; "emoji_picker_nature_category" = "動物と自然"; -"emoji_picker_people_category" = "笑顔とみんな"; +"emoji_picker_people_category" = "表情と人々"; // MARK: Emoji picker -"emoji_picker_title" = "ピッカー"; +"emoji_picker_title" = "リアクション"; // MARK: File upload "file_upload_error_title" = "ファイルのアップロードエラー"; -"file_upload_error_unsupported_file_type_message" = "ファイルのタイプがサポートされていません。"; +"file_upload_error_unsupported_file_type_message" = "ファイルの種類がサポートされていません。"; "device_verification_emoji_pin" = "ピン"; "device_verification_emoji_folder" = "フォルダー"; -"device_verification_emoji_headphones" = "ヘッドフォン"; -"device_verification_emoji_anchor" = "アンカー"; +"device_verification_emoji_headphones" = "ヘッドホン"; +"device_verification_emoji_anchor" = "いかり"; "device_verification_emoji_bell" = "ベル"; "device_verification_emoji_trumpet" = "トランペット"; "device_verification_emoji_guitar" = "ギター"; @@ -982,55 +982,55 @@ "device_verification_emoji_aeroplane" = "飛行機"; "device_verification_emoji_bicycle" = "自転車"; "device_verification_emoji_train" = "電車"; -"device_verification_emoji_flag" = "フラグ"; -"device_verification_emoji_telephone" = "テレフォン"; -"device_verification_emoji_hammer" = "ハンマー"; +"device_verification_emoji_flag" = "旗"; +"device_verification_emoji_telephone" = "電話機"; +"device_verification_emoji_hammer" = "金槌"; "device_verification_emoji_key" = "鍵"; -"device_verification_emoji_lock" = "錠"; -"settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理します。誰があなたのことを発見できるかを管理する "; +"device_verification_emoji_lock" = "錠前"; +"settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理。あなたを見つけられる人を "; "settings_identity_server_settings" = "IDサーバー"; "external_link_confirmation_title" = "このリンクを再確認してください"; -"media_type_accessibility_sticker" = "スティッカー"; +"media_type_accessibility_sticker" = "ステッカー"; "media_type_accessibility_file" = "ファイル"; "media_type_accessibility_location" = "位置情報"; "media_type_accessibility_video" = "動画"; "media_type_accessibility_audio" = "音声"; "media_type_accessibility_image" = "画像"; "room_open_dialpad" = "ダイヤルパッド"; -"room_place_voice_call" = "ビデオ通話"; -"room_accessibility_hangup" = "通話を切る"; -"room_event_action_delete_confirmation_message" = "この未送信メッセージを削除してもよろしいですか?"; +"room_place_voice_call" = "音声通話"; +"room_accessibility_hangup" = "電話を切る"; +"room_event_action_delete_confirmation_message" = "この未送信のメッセージを削除してよろしいですか?"; "room_accessibility_video_call" = "ビデオ通話"; "room_accessibility_call" = "通話"; -"room_accessibility_integrations" = "統合"; +"room_accessibility_integrations" = "インテグレーション(統合)"; "room_accessibility_search" = "検索"; "room_accessibility_upload" = "アップロード"; -"room_message_edits_history_title" = "メッセージを編集"; +"room_message_edits_history_title" = "メッセージの編集履歴"; "room_action_reply" = "返信"; -"room_action_send_file" = "ファイルを送る"; -"room_action_camera" = "写真やビデオの撮影"; -"room_event_action_reaction_history" = "反応の履歴"; +"room_action_send_file" = "ファイルを送信"; +"room_action_camera" = "写真または動画を撮影"; +"room_event_action_reaction_history" = "リアクションの履歴"; "room_event_action_reaction_show_less" = "表示しない"; "room_event_action_reaction_show_all" = "全てを見る"; "room_event_action_edit" = "編集"; "room_event_action_reply" = "返信"; -"device_verification_security_advice_emoji" = "絵文字の順番はもう一方のログインと一致しますか?"; +"device_verification_security_advice_emoji" = "絵文字を比較して、同じ順番で現れていることを確認してください。"; "key_verification_verify_sas_validate_action" = "一致しています"; -"key_verification_verify_sas_cancel_action" = "一致しません"; +"key_verification_verify_sas_cancel_action" = "一致していません"; // MARK: Verify -"key_verification_verify_sas_title_emoji" = "絵文字の比較"; +"key_verification_verify_sas_title_emoji" = "絵文字を比較"; "device_verification_self_verify_alert_validate_action" = "認証"; -"device_verification_self_verify_alert_message" = "ログインを認証してください:%@"; +"device_verification_self_verify_alert_message" = "新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%@"; // MARK: Self verification start // New login -"device_verification_self_verify_alert_title" = "ログインしましたか?"; +"device_verification_self_verify_alert_title" = "新しいログインです。ログインしましたか?"; "room_recents_suggested_rooms_section" = "おすすめのルーム"; "settings_show_url_previews_description" = "プレビューは暗号化されていないルームでのみ表示されます。"; -"settings_show_url_previews" = "ウェブサイトプレビューを表示"; +"settings_show_url_previews" = "ウェブサイトのプレビューを表示"; "biometrics_setup_enable_button_title_x" = "%@を有効にする"; "biometrics_setup_title_x" = "%@を有効にする"; "biometrics_settings_enable_x" = "%@を有効にする"; @@ -1039,12 +1039,12 @@ // MARK: - Biometrics Protection "biometrics_mode_touch_id" = "Touch ID"; -"pin_protection_settings_enable_pin" = "PINを有効にする"; -"pin_protection_settings_section_header_with_biometrics" = "PINと%@"; -"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_enable_pin" = "PINコードを有効にする"; +"pin_protection_settings_section_header_with_biometrics" = "PINコードと%@"; +"pin_protection_settings_section_header" = "PINコード"; "settings_mentions_and_keywords_encryption_notice" = "携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。"; "settings_new_keyword" = "キーワードを追加"; -"settings_your_keywords" = "以下でキーワードを指定できます"; +"settings_your_keywords" = "キーワード"; "settings_mentions_and_keywords" = "メンションとキーワード"; "settings_messages_containing_keywords" = "キーワード"; "settings_messages_containing_at_room" = "@room"; @@ -1054,7 +1054,7 @@ "settings_group_messages" = "グループメッセージ"; "settings_encrypted_direct_messages" = "暗号化されたダイレクトメッセージ"; "settings_direct_messages" = "ダイレクトメッセージ"; -"settings_notify_me_for" = "以下がメッセージに含まれる場合に通知"; +"settings_notify_me_for" = "以下の場合に通知"; "settings_phone_contacts" = "端末の連絡先"; "settings_notifications" = "通知"; "settings_links" = "リンク"; @@ -1105,13 +1105,13 @@ "secrets_setup_recovery_passphrase_validate_action" = "完了"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "バックアップ"; "room_event_action_forward" = "転送"; -"room_event_action_view_in_room" = "ルームに表示"; +"room_event_action_view_in_room" = "ルーム内で表示"; "room_notifs_settings_encrypted_room_notice" = "暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。"; "room_notifs_settings_mentions_and_keywords" = "メンションとキーワードのみ"; "security_settings_secure_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "key_backup_setup_intro_setup_action_without_existing_backup" = "鍵のバックアップを使用開始"; "space_participants_action_ban" = "このスペースからブロック"; -"space_participants_action_remove" = "このスペースから削除"; +"space_participants_action_remove" = "このスペースから追放"; "accessibility_button_label" = "ボタン"; "ok" = "OK"; "spaces_empty_space_detail" = "非公開で招待が必要なルームは表示されていません。"; @@ -1126,8 +1126,8 @@ "home_empty_view_title" = "%@へようこそ、\n%@"; "threads_empty_tip" = "ヒント:メッセージをタップして「スレッド」を選択し、開始。"; -"threads_empty_info_all" = "スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。"; -"threads_empty_title" = "スレッドでディスカッションを整理して管理"; +"threads_empty_info_all" = "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。"; +"threads_empty_title" = "スレッド機能を使って、会話をまとめましょう"; "secure_key_backup_setup_intro_use_security_key_title" = "セキュリティーキーを使用"; // MARK: Secure backup setup @@ -1135,7 +1135,7 @@ // Intro "secure_key_backup_setup_intro_title" = "セキュアバックアップ"; -"spaces_explore_rooms" = "ルームを探索"; +"spaces_explore_rooms" = "ルームを探す"; "secure_key_backup_setup_intro_use_security_key_info" = "セキュリティーキーを生成します。パスワードマネージャーもしくは金庫のような安全な場所で保管してください。"; "secure_key_backup_setup_intro_info" = "サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。"; "secure_backup_setup_banner_subtitle" = "暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう"; @@ -1146,11 +1146,11 @@ "matrix" = "Matrix"; // Login Screen -"login_create_account" = "アカウント作成:"; +"login_create_account" = "アカウントを作成:"; "login_server_url_placeholder" = "URL (例 https://matrix.org)"; -"login_home_server_title" = "接続先サーバーURL:"; -"login_home_server_info" = "あなたの接続先サーバーは、あなたの全ての会話とアカウント情報を保存します"; -"login_identity_server_title" = "認証サーバーURL:"; +"login_home_server_title" = "ホームサーバーのURL:"; +"login_home_server_info" = "あなたのホームサーバーは、あなたの全ての会話とアカウント情報を保存します"; +"login_identity_server_title" = "IDサーバーのURL:"; "login_password_placeholder" = "パスワード"; "login_email_placeholder" = "メールアドレス"; // Action @@ -1160,40 +1160,40 @@ "resend_message" = "メッセージを再送信"; "select_all" = "全て選択"; "show_details" = "詳細を表示"; -"login_identity_server_info" = "Matrixは、どの電子メールなどがどのMatrix IDに属しているかを追跡するアイデンティティサーバーを提供します。 現在 https://matrix.org のみが存在します。"; +"login_identity_server_info" = "Matrixは、電子メールなどからMatrix IDを検索するIDサーバーを提供します。現在は https://matrix.org のみが存在します。"; "login_user_id_placeholder" = "Matrix ID(例 @bob:matrix.org または bob)"; -"login_optional_field" = "オプション"; -"login_display_name_placeholder" = "表示名 (例 Bob Obson)"; -"login_email_info" = "メールアドレスを指定すると、他のユーザーがあなたをMatrixで簡単に見つけることができ、今後パスワードをリセットすることができます。"; -"login_prompt_email_token" = "メールの認証トークンを入力してください:"; +"login_optional_field" = "任意"; +"login_display_name_placeholder" = "表示名(例 Bob Obson)"; +"login_email_info" = "メールアドレスを指定すると、他のユーザーがあなたをMatrixでより簡単に見つけられます。また、電子メールでパスワードをリセットすることも可能となります。"; +"login_prompt_email_token" = "電子メールの認証トークンを入力してください:"; "login_error_title" = "ログインに失敗しました"; "login_error_no_login_flow" = "このホームサーバーから認証情報を取得できませんでした"; "login_error_do_not_support_login_flows" = "現在、このホームサーバーによって定義されたログインフローの一部または全てをサポートしていません"; "login_error_registration_is_not_supported" = "登録は現在サポートされていません"; -"login_error_forbidden" = "無効なユーザー名/パスワード"; +"login_error_forbidden" = "ユーザー名かパスワードが正しくありません"; "login_error_unknown_token" = "指定されたアクセストークンが認識されませんでした"; "login_error_bad_json" = "不正な形式のJSON"; "login_error_not_json" = "有効なJSONを含んでいませんでした"; -"login_error_limit_exceeded" = "あまりにも多くのリクエストが送られました"; +"login_error_limit_exceeded" = "ログイン要求が多すぎます"; "login_error_user_in_use" = "このユーザー名は既に使用されています"; -"login_error_login_email_not_yet" = "まだクリックされていないメールリンク"; -"login_use_fallback" = "フォールバックページを使用"; +"login_error_login_email_not_yet" = "まだクリックされていない電子メールのリンク"; +"login_use_fallback" = "フォールバック用のページを使用"; "login_leave_fallback" = "キャンセル"; "login_invalid_param" = "無効なパラメーター"; "register_error_title" = "登録に失敗しました"; -"login_error_forgot_password_is_not_supported" = "Forgot passwordは現在サポートされていません"; -"login_mobile_device" = "携帯"; +"login_error_forgot_password_is_not_supported" = "「パスワードを忘れた場合」は現在サポートされていません"; +"login_mobile_device" = "携帯端末"; "login_tablet_device" = "タブレット"; "login_desktop_device" = "デスクトップ"; "login_error_resource_limit_exceeded_title" = "リソース制限を超えました"; -"login_error_resource_limit_exceeded_message_default" = "このホームサーバーは、リソース制限の1つを超えています。"; -"login_error_resource_limit_exceeded_message_monthly_active_user" = "このホームサーバーは、月間アクティブユーザー制限を超えています。"; -"login_error_resource_limit_exceeded_message_contact" = "\n\nこのサービスを続行するには、サービス管理者に連絡してください。"; +"login_error_resource_limit_exceeded_message_default" = "このホームサーバーはリソースの上限に達しました。"; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数の上限に達しました 。"; +"login_error_resource_limit_exceeded_message_contact" = "\n\nこのサービスを引き続き使用するには、サービス管理者にお問い合わせください。"; "login_error_resource_limit_exceeded_contact_button" = "管理者に連絡"; "abort" = "中断"; "discard" = "破棄"; "dismiss" = "却下"; -"submit" = "提出"; +"submit" = "送信"; "submit_code" = "コードを送信"; "set_default_power_level" = "権限レベルをリセット"; "set_moderator" = "モデレーターを設定"; @@ -1203,31 +1203,31 @@ "start_video_call" = "ビデオ通話を開始"; "mention" = "メンション"; "select_account" = "アカウントを選択"; -"attach_media" = "ライブラリからメディアを添付"; -"capture_media" = "写真/ビデオを撮る"; +"attach_media" = "ライブラリーからメディアを添付"; +"capture_media" = "写真/動画を撮る"; "invite_user" = "Matrixユーザーを招待"; -"reset_to_default" = "デフォルトにリセット"; +"reset_to_default" = "既定にリセット"; "cancel_upload" = "アップロードをキャンセル"; "cancel_download" = "ダウンロードをキャンセル"; "answer_call" = "通話に応答"; "reject_call" = "通話を拒否"; -"end_call" = "通話終了"; +"end_call" = "通話を終了"; "ignore" = "無視"; // Events formatter "notice_avatar_changed_too" = "(アバターも変更されました)"; "notice_room_name_removed" = "%@がルーム名を削除しました"; "notice_room_topic_removed" = "%@がトピックを削除しました"; -"notice_event_redacted" = "<編集された%@>"; +"notice_event_redacted" = "<%@が編集されました>"; "notice_event_redacted_by" = " %@により"; "notice_event_redacted_reason" = " [理由: %@]"; -"notice_profile_change_redacted" = "%@が彼らのプロフィール %@を更新しました"; -"notice_room_created" = "%@がルームを作成しました"; -"notice_room_join_rule" = "結合ルールは次のとおり: %@"; -"notice_room_power_level_intro" = "ルームメンバーの権限レベル:"; -"notice_room_power_level_acting_requirement" = "アクション前にユーザーの必要な最小権限レベル:"; -"notice_room_power_level_event_requirement" = "イベントに関連する最小権限レベル:"; -"notice_room_aliases" = "ルームエイリアス: %@"; -"notice_room_related_groups" = "このルームに関連付けられたグループ: %@"; +"notice_profile_change_redacted" = "%@がプロフィール%@を更新しました"; +"notice_room_created" = "%@がルームを作成し設定しました。"; +"notice_room_join_rule" = "参加ルール:%@"; +"notice_room_power_level_intro" = "ルームメンバーの権限レベル:"; +"notice_room_power_level_acting_requirement" = "アクションに必要なユーザーの最小権限レベル:"; +"notice_room_power_level_event_requirement" = "イベントに関連する最小権限レベル:"; +"notice_room_aliases" = "ルームのエイリアス:%@"; +"notice_room_related_groups" = "このルームに関連付けられたグループ:%@"; "notice_encrypted_message" = "暗号化されたメッセージ"; "notice_image_attachment" = "画像添付"; "notice_audio_attachment" = "音声添付"; @@ -1235,17 +1235,17 @@ "notice_location_attachment" = "位置情報添付"; "notice_file_attachment" = "ファイル添付"; "notice_invalid_attachment" = "無効な添付"; -"notice_unsupported_attachment" = "サポートされていない添付: %@"; -"notice_feedback" = "フィードバックイベント (id: %@): %@"; -"notice_redaction" = "%@はイベントを編集しました (id: %@)"; +"notice_unsupported_attachment" = "サポートされていない添付ファイル:%@"; +"notice_feedback" = "フィードバックイベント(id:%@):%@"; +"notice_redaction" = "%@はイベントを編集しました(id:%@)"; "notice_error_unsupported_event" = "サポートされていないイベント"; "notice_error_unexpected_event" = "予期しないイベント"; -"notice_error_unknown_event_type" = "不明なイベントタイプ"; -"notice_room_history_visible_to_anyone" = "%@が今後のルーム履歴を「誰でも」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members" = "%@が今後のルーム履歴を「メンバーのみ」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_from_invited_point" = "%@が今後のルーム履歴を「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_from_joined_point" = "%@が今後のルーム履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; -"notice_crypto_unable_to_decrypt" = "** 復号化できません: %@ **"; +"notice_error_unknown_event_type" = "イベントの種類が不明です"; +"notice_room_history_visible_to_anyone" = "%@が今後のルームの履歴を「誰でも」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members" = "%@が今後のルームの履歴を「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point" = "%@が今後のルームの履歴を「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point" = "%@が今後のルームの履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; +"notice_crypto_unable_to_decrypt" = "** 復号化できません:%@ **"; "notice_crypto_error_unknown_inbound_session_id" = "送信者のセッションからこのメッセージ用の鍵が送信されていません。"; "notice_sticker" = "ステッカー"; "notice_in_reply_to" = "返信先"; @@ -1255,15 +1255,15 @@ "settings" = "設定"; "settings_enable_inapp_notifications" = "アプリ内通知を有効にする"; "settings_enable_push_notifications" = "プッシュ通知を有効にする"; -"settings_enter_validation_token_for" = "%@の認証トークンを入力:"; -"notification_settings_room_rule_title" = "ルーム: '%@'"; +"settings_enter_validation_token_for" = "%@の認証トークンを入力:"; +"notification_settings_room_rule_title" = "ルーム:'%@'"; // Devices -"device_details_title" = "セッション情報\n"; -"device_details_name" = "名前\n"; +"device_details_title" = "セッションの情報\n"; +"device_details_name" = "公開端末名\n"; "device_details_identifier" = "ID\n"; -"device_details_last_seen" = "最終接続日\n"; +"device_details_last_seen" = "直近のオンライン日時\n"; "device_details_last_seen_format" = "%@ @ %@\n"; -"device_details_rename_prompt_message" = "セッションの公開名は、あなたとやり取りする人々に対して表示されます"; +"device_details_rename_prompt_message" = "セッションの公開名は、あなたとやり取りする連絡先に対して表示されます"; "device_details_delete_prompt_title" = "認証"; "device_details_delete_prompt_message" = "この操作には、追加の認証が必要です。\n続行するには、パスワードを入力してください。"; // Encryption information @@ -1277,47 +1277,47 @@ "room_event_encryption_info_event_decryption_error" = "復号化エラー\n"; "room_event_encryption_info_event_unencrypted" = "暗号化されていません"; "room_event_encryption_info_event_none" = "なし"; -"room_event_encryption_info_device" = "\n送信者セッション情報\n"; -"room_event_encryption_info_device_unknown" = "未知のセッション\n"; -"room_event_encryption_info_device_name" = "名前\n"; +"room_event_encryption_info_device" = "\n送信者のセッションの情報\n"; +"room_event_encryption_info_device_unknown" = "不明なセッション\n"; +"room_event_encryption_info_device_name" = "公開端末名\n"; "room_event_encryption_info_device_id" = "ID\n"; "room_event_encryption_info_device_verification" = "認証\n"; -"room_event_encryption_info_device_fingerprint" = "Ed25519 fingerprint\n"; -"room_event_encryption_info_device_verified" = "検証済み"; -"room_event_encryption_info_device_not_verified" = "認証されていない"; -"room_event_encryption_info_device_blocked" = "ブラックリストに載せた"; -"room_event_encryption_info_verify" = "認証しています…"; -"room_event_encryption_info_unverify" = "未認証"; -"room_event_encryption_info_block" = "ブラックリスト"; -"room_event_encryption_info_unblock" = "ブラックでないリスト"; -"room_event_encryption_verify_title" = "セッション認証\n\n"; -"room_event_encryption_verify_message" = "このセッションが信頼できることを確認するには、他の方法(対面や電話など)で所有者に連絡し、セッションのユーザー設定で表示される鍵が以下の鍵と一致するかどうかを訪ねてください。\n\nセッション名: %@\nセッションID: %@\nセッションキー: %@\n\n一致する場合は、下の確認ボタンを押します。 それ以外の人がこのセッションを傍受している場合は、代わりにブラックリストボタンを押してください。\n\n将来この認証プロセスはより洗練されたものになります。"; +"room_event_encryption_info_device_fingerprint" = "Ed25519 フィンガープリント\n"; +"room_event_encryption_info_device_verified" = "認証済"; +"room_event_encryption_info_device_not_verified" = "認証されていません"; +"room_event_encryption_info_device_blocked" = "ブラックリストに追加済"; +"room_event_encryption_info_verify" = "認証…"; +"room_event_encryption_info_unverify" = "認証を取り消す"; +"room_event_encryption_info_block" = "ブラックリストに追加"; +"room_event_encryption_info_unblock" = "ブラックリストから除外"; +"room_event_encryption_verify_title" = "セッションを認証\n\n"; +"room_event_encryption_verify_message" = "このセッションが信頼できることを確認するには、他の方法(対面や電話など)で所有者に連絡し、セッションのユーザー設定で表示される鍵が以下の鍵と一致するかどうかを訪ねてください。\n\nセッション名:%@\nセッションID:%@\nセッションキー:%@\n\n一致する場合は、下の確認ボタンを押します。 それ以外の人がこのセッションを傍受している場合は、代わりにブラックリストボタンを押してください。\n\n将来この認証プロセスはより洗練されたものになります。"; "room_event_encryption_verify_ok" = "認証"; // Account "account_save_changes" = "変更を保存"; -"account_link_email" = "リンクメール"; -"account_linked_emails" = "リンクされたメール"; +"account_link_email" = "電子メールをリンク"; +"account_linked_emails" = "リンクした電子メール"; "account_email_validation_title" = "認証の保留中"; -"account_email_validation_message" = "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。"; -"account_email_validation_error" = "メールアドレスを認証できません。メールを確認して、記載されているリンクをクリックしてください。その後、「続行する」をクリックしてください"; +"account_email_validation_message" = "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。"; +"account_email_validation_error" = "メールアドレスを認証できません。メールを確認して、記載されているリンクをクリックしてください。完了したら「続行する」をクリックしてください"; "account_msisdn_validation_title" = "認証の保留中"; "account_msisdn_validation_message" = "SMSで認証番号を送りました。以下にその番号を入力してください。"; -"account_msisdn_validation_error" = "電話番号を確認できません。"; +"account_msisdn_validation_error" = "電話番号を認証できません。"; "account_error_display_name_change_failed" = "表示名の変更に失敗しました"; "account_error_picture_change_failed" = "画像の変更に失敗しました"; "account_error_matrix_session_is_not_opened" = "Matrixセッションが開かれていません"; -"account_error_email_wrong_title" = "無効な電子メールアドレス"; +"account_error_email_wrong_title" = "無効なメールアドレス"; "account_error_email_wrong_description" = "メールアドレスの形式が正しくありません"; "account_error_msisdn_wrong_title" = "無効な電話番号"; "account_error_msisdn_wrong_description" = "電話番号の形式が正しくありません"; // Room creation -"room_creation_name_title" = "ルーム名:"; -"room_creation_name_placeholder" = "(例 ランチグループ)"; -"room_creation_alias_title" = "ルームの別名:"; -"room_creation_alias_placeholder" = "(例 #foo:example.org)"; -"room_creation_alias_placeholder_with_homeserver" = "(例 #foo%@)"; -"room_creation_participants_title" = "参加者:"; -"room_creation_participants_placeholder" = "(例 @bob:homeserver1; @john:homeserver2…)"; +"room_creation_name_title" = "ルーム名:"; +"room_creation_name_placeholder" = "(例 ランチグループ)"; +"room_creation_alias_title" = "ルームの別名:"; +"room_creation_alias_placeholder" = "(例 #foo:example.org)"; +"room_creation_alias_placeholder_with_homeserver" = "(例 #foo%@)"; +"room_creation_participants_title" = "参加者:"; +"room_creation_participants_placeholder" = "(例 @bob:homeserver1; @john:homeserver2…)"; // Room "room_please_select" = "ルームを選択してください"; "room_error_join_failed_title" = "ルームに参加できませんでした"; @@ -1326,34 +1326,34 @@ "room_error_topic_edition_not_authorized" = "このルームのトピックを編集する権限がありません"; "room_error_cannot_load_timeline" = "タイムラインの読み込みに失敗しました"; "room_error_timeline_event_not_found_title" = "タイムラインの位置を読み込めませんでした"; -"room_error_timeline_event_not_found" = "アプリケーションがこのルームのタイムラインに特定のポイントをロードしようとしましたが、それを見つけることができませんでした"; -"room_left" = "あなたはルームを出ました"; -"room_no_power_to_create_conference_call" = "このルームで会議を開始するために招待する権限が必要です"; -"room_no_conference_call_in_encrypted_rooms" = "暗号化された会議室では会議通話はサポートされません"; +"room_error_timeline_event_not_found" = "このルームのタイムラインの特定の地点を読み込もうとしましたが、見つけられませんでした"; +"room_left" = "ルームから退出しました"; +"room_no_power_to_create_conference_call" = "このルームで会議を開始するには、招待するための権限が必要です"; +"room_no_conference_call_in_encrypted_rooms" = "暗号化されたルームでは、グループ通話はサポートされません"; // Reply to message "message_reply_to_sender_sent_an_image" = "画像を送信しました。"; -"message_reply_to_sender_sent_a_video" = "動画を送りました。"; -"message_reply_to_sender_sent_an_audio_file" = "オーディオファイルを送信しました。"; +"message_reply_to_sender_sent_a_video" = "動画を送信しました。"; +"message_reply_to_sender_sent_an_audio_file" = "音声ファイルを送信しました。"; "message_reply_to_sender_sent_a_file" = "ファイルを送信しました。"; -"message_reply_to_message_to_reply_to_prefix" = "に返信"; +"message_reply_to_message_to_reply_to_prefix" = "返信先"; // Room members "room_member_ignore_prompt" = "このユーザーからの全てのメッセージを非表示にしますか?"; -"room_member_power_level_prompt" = "この変更を元に戻すことはできません。ユーザーが自分と同じレベルの権限を持つように促しますが、よろしいですか?"; +"room_member_power_level_prompt" = "このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。\nよろしいですか?"; // Attachment -"attachment_size_prompt" = "次のように送信しますか:"; -"attachment_original" = "実際のサイズ: %@"; -"attachment_small" = "小: %@"; -"attachment_medium" = "中: %@"; -"attachment_large" = "大: %@"; -"attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; -"attachment_cancel_upload" = "アップロードをキャンセルしますか?"; -"attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; -"attachment_multiselection_original" = "実際のサイズ"; -"attachment_e2e_keys_file_prompt" = "このファイルには、Matrixクライアントからエクスポートされた暗号鍵が含まれています。\nファイルの内容を表示するか、ファイル内の鍵をインポートしますか?"; +"attachment_size_prompt" = "次のように送信しますか:"; +"attachment_original" = "実際のサイズ:%@"; +"attachment_small" = "小:%@"; +"attachment_medium" = "中:%@"; +"attachment_large" = "大:%@"; +"attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; +"attachment_cancel_upload" = "アップロードをキャンセルしますか?"; +"attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; +"attachment_multiselection_original" = "等倍"; +"attachment_e2e_keys_file_prompt" = "このファイルには、Matrixのクライアントからエクスポートされた暗号鍵が含まれています。\nファイルの内容を表示するか、ファイル内の鍵をインポートしますか?"; "attachment_e2e_keys_import" = "インポート…"; // Contacts "contact_mx_users" = "Matrixユーザー"; -"contact_local_contacts" = "ローカルの連絡先"; +"contact_local_contacts" = "端末の連絡先"; // Groups // Search "search_no_results" = "結果がありません"; @@ -1365,19 +1365,19 @@ "format_time_d" = "日"; // E2E import "e2e_import_room_keys" = "ルームの暗号鍵をインポート"; -"e2e_import_prompt" = "このプロセスでは、以前に別のMatrixクライアントからエクスポートした暗号鍵をインポートできます。 これにより、他のクライアントが解読できる全てのメッセージを解読することができます。\nエクスポートした暗号鍵のファイルは、パスフレーズで保護されています。 ファイルを復号化するには、パスフレーズをここに入力する必要があります。"; +"e2e_import_prompt" = "このプロセスでは、以前に別のMatrixのクライアントからエクスポートした暗号鍵をインポートできます。 これにより、他のクライアントが解読できる全てのメッセージを解読することができます。\nエクスポートした暗号鍵のファイルは、パスフレーズで保護されています。 ファイルを復号化するには、パスフレーズをここに入力する必要があります。"; "e2e_import" = "インポート"; "e2e_passphrase_enter" = "パスフレーズを入力"; // E2E export "e2e_export_room_keys" = "ルームの暗号鍵をエクスポート"; -"e2e_export_prompt" = "このプロセスでは、暗号化されたルームで受信したメッセージの鍵をローカルファイルにエクスポートできます。 そのファイルを別のMatrixクライアントにインポートすると、クライアントはこれらのメッセージを復号化することができます。\nエクスポートしたファイルを使えば、誰でも暗号化されたメッセージを復号化できるので、ファイルを安全に保つように注意する必要があります。"; +"e2e_export_prompt" = "このプロセスでは、暗号化されたルームで受信したメッセージの鍵をローカルファイルにエクスポートできます。 そのファイルを別のMatrixのクライアントにインポートすると、クライアントはこれらのメッセージを復号化することができます。\nエクスポートしたファイルを使うと、誰でも暗号化されたメッセージを復号化できるため、ファイルを安全に保つように注意する必要があります。"; "e2e_export" = "エクスポート"; "e2e_passphrase_confirm" = "パスフレーズを確認"; -"e2e_passphrase_empty" = "パスフレーズは空であってはいけません"; -"e2e_passphrase_not_match" = "パスフレーズは一致する必要があります"; +"e2e_passphrase_empty" = "パスフレーズには1文字以上が必要です"; +"e2e_passphrase_not_match" = "パスフレーズが一致していません"; "e2e_passphrase_create" = "パスフレーズの作成"; // Others -"user_id_title" = "ユーザーID:"; +"user_id_title" = "ユーザーID:"; "offline" = "オフライン"; "unsent" = "未送信"; "error" = "エラー"; @@ -1388,38 +1388,38 @@ "public" = "公開"; "power_level" = "権限レベル"; "network_error_not_reachable" = "ネットワーク接続を確認してください"; -"user_id_placeholder" = "例: @bob:homeserver"; -"ssl_homeserver_url" = "ホームサーバーのURL: %@"; +"user_id_placeholder" = "例:@bob:homeserver"; +"ssl_homeserver_url" = "ホームサーバーのURL:%@"; // Permissions -"camera_access_not_granted_for_call" = "ビデオ通話はカメラにアクセスする必要がありますが、%@にはそのカメラを使用する権限がありません"; -"microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@には使用許可がありません"; -"local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはそのアクセス権限がありません"; -"local_contacts_access_discovery_warning_title" = "ユーザーの探索"; -"local_contacts_access_discovery_warning" = "%@は、ユーザーを検索するためにあなたの連絡先から電子メールと電話番号をアップロードしたい"; +"camera_access_not_granted_for_call" = "ビデオ通話にはカメラへのアクセスが必要ですが、%@にはカメラを使用する権限がありません"; +"microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; +"local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはアクセス権限がありません"; +"local_contacts_access_discovery_warning_title" = "ユーザーを探す"; +"local_contacts_access_discovery_warning" = "Matrixを既に使用している連絡先を見つけるため、%@は電話帳にあるメールアドレスと電話番号を、あなたが選択したMatrixのIDサーバーに送信することができます。サポートしている場合、個人データは送信前にハッシュ化されます。詳細はIDサーバーのプライバシーポリシーを確認してください。"; // Country picker "country_picker_title" = "国を選択"; // Language picker "language_picker_title" = "言語を選択"; -"language_picker_default_language" = "既定値 (%@)"; +"language_picker_default_language" = "既定値(%@)"; "notice_room_invite" = "%@が%@を招待しました"; -"notice_room_third_party_invite" = "%@が%@にルームへの招待状を送りました"; +"notice_room_third_party_invite" = "%@が%@にルームへの招待を送りました"; "notice_room_third_party_registered_invite" = "%@が%@の招待を受け入れました"; "notice_room_join" = "%@が参加しました"; "notice_room_leave" = "%@が退出しました"; "notice_room_reject" = "%@が招待を拒否しました"; -"notice_room_kick" = "%@が%@を追い出しました"; -"notice_room_unban" = "%@が%@を追放解除しました"; -"notice_room_ban" = "%@が%@を追放しました"; -"notice_room_withdraw" = "%@が%@の招待を辞退しました"; -"notice_room_reason" = ". 理由: %@"; +"notice_room_kick" = "%@が%@を追放しました"; +"notice_room_unban" = "%@が%@のブロックを解除しました"; +"notice_room_ban" = "%@が%@をブロックしました"; +"notice_room_withdraw" = "%@が%@の招待を取り下げました"; +"notice_room_reason" = "。理由:%@"; "notice_avatar_url_changed" = "%@がアバターを変更しました"; "notice_display_name_set" = "%@が表示名を%@に設定しました"; "notice_display_name_changed_from" = "%@が表示名を%@から%@に変更しました"; "notice_display_name_removed" = "%@が表示名を削除しました"; -"notice_topic_changed" = "%@がトピックを次のように変更しました:%@"; -"notice_room_name_changed" = "%@がルーム名を次のように変更しました:%@"; -"notice_placed_voice_call" = "%@が電話をかけました"; -"notice_placed_video_call" = "%@がビデオ電話をかけました"; +"notice_topic_changed" = "%@がトピックを「%@」に変更しました。"; +"notice_room_name_changed" = "%@がルーム名を%@に変更しました。"; +"notice_placed_voice_call" = "%@が音声通話を発信しました"; +"notice_placed_video_call" = "%@がビデオ通話を発信しました"; "notice_answered_video_call" = "%@が電話に出ました"; "notice_ended_video_call" = "%@が通話を終了しました"; "notice_conference_call_request" = "%@がVoIP会議をリクエストしました"; @@ -1429,21 +1429,21 @@ "send" = "送信"; "copy_button_name" = "コピー"; "resend" = "再送信"; -"redact" = "編集"; +"redact" = "削除"; "share" = "共有"; -"set_power_level" = "権限レベル"; +"set_power_level" = "権限レベルを設定"; "delete" = "削除"; // actions "action_logout" = "ログアウト"; -"create_room" = "ルームを作る"; +"create_room" = "ルームを作成"; "login" = "ログイン"; "create_account" = "アカウントを作成"; -"membership_invite" = "招待しました"; -"membership_leave" = "退出しました"; -"membership_ban" = "ブロックしました"; -"num_members_one" = "%@ ユーザー"; -"num_members_other" = "%@ ユーザー"; -"kick" = "キック"; +"membership_invite" = "招待済"; +"membership_leave" = "退出済"; +"membership_ban" = "ブロック済"; +"num_members_one" = "%@人のユーザー"; +"num_members_other" = "%@人のユーザー"; +"kick" = "会話から追放"; "ban" = "ブロック"; "unban" = "ブロック解除"; "message_unsaved_changes" = "保存されていない変更があります。 退出すると変更は取り消されます。"; @@ -1452,59 +1452,59 @@ "login_error_must_start_http" = "URLは http[s]:// で始まる必要があります"; // room details dialog screen // contacts list screen -"invitation_message" = "私はmatrixであなたとチャットしたい。 詳細はウェブサイトhttp://matrix.orgをお尋ねください。"; +"invitation_message" = "matrixでチャットしましょう。 詳細はウェブサイト http://matrix.org で確認してください。"; // Settings screen -"settings_title_config" = "構成"; +"settings_title_config" = "設定"; "settings_title_notifications" = "通知"; // Notification settings screen "notification_settings_disable_all" = "全ての通知を無効にする"; "notification_settings_enable_notifications" = "通知を有効にする"; "notification_settings_enable_notifications_warning" = "現在、全ての端末で全ての通知が無効になっています。"; -"notification_settings_global_info" = "通知設定はユーザーアカウントに保存され、デスクトップ通知を含む全てのクライアント間で共有されます。\n\nルールは順番に適用されます。 一致する最初のルールは、メッセージの結果を定義します。\nだから:単語ごとの通知は、送信者ごとの通知よりも重要なルームごとの通知よりも重要です。\n同じ種類の複数のルールの場合、一致するリストの最初のルールが優先されます。"; +"notification_settings_global_info" = "通知設定はユーザーアカウントに保存され、デスクトップ通知を含む全てのクライアント間で共有されます。\n\nルールは順番に適用されます。 一致する最初のルールは、メッセージの結果を定義します。\nしたがって、単語単位の通知はルーム単位の通知よりも優先され、ルーム単位の通知は、送信者単位の通知よりも優先されます。\n同じ種類の複数のルールに関しては、一致するリストの最初のルールが優先されます。"; "notification_settings_per_word_notifications" = "単語単位の通知"; -"notification_settings_per_word_info" = "単語は大文字と小文字を区別せずに一致させ、*ワイルドカードを含めることができます。 従って:\nfooは、区切り文字で囲まれた文字列foo(例 句読点や空白、行の開始/終了)と一致します。\nfoo*は、fooで始まる単語に一致します。\n*foo*は、3文字のfooを含む単語に一致します。"; +"notification_settings_per_word_info" = "単語は大文字と小文字を区別せずに一致させ、*ワイルドカードを含めることができます。 よって、\nfooは、区切り文字で囲まれた文字列foo(例 句読点や空白、行の開始/終了)と一致します。\nfoo*は、fooで始まる単語に一致します。\n*foo*は、3文字のfooを含む単語に一致します。"; "notification_settings_always_notify" = "常に通知"; -"notification_settings_never_notify" = "決して通知しない"; +"notification_settings_never_notify" = "通知しない"; "notification_settings_word_to_match" = "一致する単語"; "notification_settings_highlight" = "ハイライト"; -"notification_settings_custom_sound" = "カスタムサウンド"; -"notification_settings_per_room_notifications" = "1ルームあたりの通知"; -"notification_settings_per_sender_notifications" = "送信者ごとの通知"; +"notification_settings_custom_sound" = "カスタム音"; +"notification_settings_per_room_notifications" = "ルーム単位の通知"; +"notification_settings_per_sender_notifications" = "送信者単位の通知"; "notification_settings_sender_hint" = "@user:domain.com"; "notification_settings_select_room" = "ルームを選択"; "notification_settings_other_alerts" = "その他のアラート"; -"notification_settings_contain_my_user_name" = "私のユーザー名を含むメッセージについて音で私に通知してください"; -"notification_settings_contain_my_display_name" = "私の表示名が含まれているメッセージが届いた際に音で通知"; -"notification_settings_just_sent_to_me" = "私に送られたメッセージについての音で私に知らせる"; -"notification_settings_invite_to_a_new_room" = "私が新しいルームに招待されたときに知らせる"; +"notification_settings_contain_my_user_name" = "私のユーザー名を含むメッセージについて音で通知"; +"notification_settings_contain_my_display_name" = "私の表示名を含むメッセージについて音で通知"; +"notification_settings_just_sent_to_me" = "私にのみ送信されたメッセージについて音で通知"; +"notification_settings_invite_to_a_new_room" = "新しいルームに招待されたときに通知"; "notification_settings_people_join_leave_rooms" = "誰かがルームに参加もしくは退出したときに通知"; "notification_settings_receive_a_call" = "通話を受信したときに通知"; "notification_settings_suppress_from_bots" = "ボットからの通知を抑制"; "notification_settings_by_default" = "既定値では…"; -"notification_settings_notify_all_other" = "他の全てのメッセージ/ルームについて通知"; +"notification_settings_notify_all_other" = "他の全てのメッセージまたはルームについて通知"; // gcm section // call string "call_waiting" = "待機中..."; -"call_connecting" = "通話接続中…"; -"call_ended" = "通話終了"; +"call_connecting" = "接続しています…"; +"call_ended" = "通話が終了しました"; "call_ring" = "呼び出し中..."; -"incoming_video_call" = "着信ビデオ通話"; -"incoming_voice_call" = "着信音声通話"; -"call_invite_expired" = "期限切れの招待コール"; +"incoming_video_call" = "ビデオ通話の着信中"; +"incoming_voice_call" = "音声通話の着信中"; +"call_invite_expired" = "通話の招待の期限が切れました"; // unrecognized SSL certificate "ssl_trust" = "信頼"; "ssl_logout_account" = "ログアウト"; "ssl_remain_offline" = "無視"; -"ssl_fingerprint_hash" = "指紋 (%@):"; -"ssl_could_not_verify" = "リモートサーバーのIDを確認できませんでした。"; -"ssl_cert_not_trust" = "これは、誰かがあなたのトラフィックを悪意を持って傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味します。"; -"ssl_cert_new_account_expl" = "サーバー管理者がこれが予期されると述べた場合は、以下の指紋が提供された指紋と一致することを確認してください。"; -"ssl_unexpected_existing_expl" = "証明書は、お使いの携帯電話にて信頼されたものから変更されました。 これは非常に珍しいことです。 この新しい証明書に同意しないことをお勧めします。"; -"ssl_expected_existing_expl" = "証明書が以前に信頼されたものから信頼されていないものに変更されました。 サーバーが証明書を更新した可能性があります。 予想される指紋については、サーバー管理者にお問い合わせください。"; -"ssl_only_accept" = "サーバー管理者が上記のものと一致する指紋を発行した場合にのみ、証明書を受け入れてください。"; -"unignore" = "無視しない"; -"notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化をオンにしました。"; -"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化をオンにしました(不明なアルゴリズム %2$@)。"; +"ssl_fingerprint_hash" = "フィンガープリント(%@):"; +"ssl_could_not_verify" = "リモートサーバーのIDを認証できませんでした。"; +"ssl_cert_not_trust" = "これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。"; +"ssl_cert_new_account_expl" = "サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。"; +"ssl_unexpected_existing_expl" = "証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。"; +"ssl_expected_existing_expl" = "証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。"; +"ssl_only_accept" = "サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。"; +"unignore" = "無視を解除"; +"notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化を有効にしました。"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %2$@)を有効にしました。"; "device_details_rename_prompt_title" = "セッション名"; "account_error_push_not_allowed" = "通知は許可されていません"; "notice_room_third_party_revoked_invite" = "%@が%@のルームへの招待を取り消しました"; @@ -1512,27 +1512,27 @@ "notice_room_invite_by_you" = "%@を招待しました"; "notice_room_invite_you" = "%@があなたを招待しました"; "notice_room_join_by_you" = "参加しました"; -"notice_room_leave_by_you" = "あなたが退出しました"; -"notice_room_kick_by_you" = "%@をキックしました"; +"notice_room_leave_by_you" = "退出しました"; +"notice_room_kick_by_you" = "%@を追放しました"; "notice_room_unban_by_you" = "%@のブロックを解除しました"; "notice_room_ban_by_you" = "%@をブロックしました"; "notice_avatar_url_changed_by_you" = "アバターを変更しました"; "notice_display_name_set_by_you" = "表示名を%@に変更しました"; "notice_display_name_changed_from_by_you" = "表示名を%@から%@に変更しました"; "notice_display_name_removed_by_you" = "表示名を削除しました"; -"notice_topic_changed_by_you" = "トピックを変更しました: %@"; -"notice_room_name_changed_by_you" = "ルームの名前を変更しました: %@"; -"notice_placed_voice_call_by_you" = "音声通話を開始しました"; -"notice_placed_video_call_by_you" = "ビデオ通話を開始しました"; +"notice_topic_changed_by_you" = "トピックを「%@」に変更しました。"; +"notice_room_name_changed_by_you" = "ルーム名を%@に変更しました。"; +"notice_placed_voice_call_by_you" = "音声通話を発信しました"; +"notice_placed_video_call_by_you" = "ビデオ通話を発信しました"; "notice_answered_video_call_by_you" = "電話に出ました"; "notice_ended_video_call_by_you" = "通話を終了しました"; "notice_conference_call_request_by_you" = "VoIP会議をリクエストしました"; "notice_room_name_removed_by_you" = "ルーム名を削除しました"; "notice_room_topic_removed_by_you" = "トピックを削除しました"; "notice_profile_change_redacted_by_you" = "プロフィール %@を更新しました"; -"notice_room_created_by_you" = "ルームを作成しました"; -"notice_encryption_enabled_ok_by_you" = "あなたはエンドツーエンド暗号化をオンにしました。"; -"notice_redaction_by_you" = "イベントを編集しました (id: %@)"; +"notice_room_created_by_you" = "ルームを作成し設定しました。"; +"notice_encryption_enabled_ok_by_you" = "エンドツーエンド暗号化を有効にしました。"; +"notice_redaction_by_you" = "イベントを編集しました(id:%@)"; "resume_call" = "再開"; "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@が今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@が今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; @@ -1553,10 +1553,10 @@ "location_sharing_title" = "位置情報"; "poll_timeline_not_closed_subtitle" = "もう一度やり直してください"; "poll_timeline_not_closed_title" = "アンケートの終了に失敗しました"; -"poll_timeline_total_no_votes" = "まだ誰も投票していません"; +"poll_timeline_total_no_votes" = "投票がありません"; "poll_timeline_votes_count" = "%lu票"; "poll_timeline_one_vote" = "1票"; -"poll_edit_form_poll_type_closed_description" = "結果はアンケートを終了した後でのみ明らかにされます"; +"poll_edit_form_poll_type_closed_description" = "結果はアンケートが終了した後で表示されます"; "poll_edit_form_poll_type_closed" = "アンケートの終了後に結果を公開"; "poll_edit_form_poll_type_open_description" = "投票した人には、投票の際に即座に結果が表示されます"; "poll_edit_form_poll_type_open" = "投票の際に結果を公開"; @@ -1575,39 +1575,39 @@ // Mark: - Polls "poll_edit_form_create_poll" = "アンケートを作成"; -"poll_timeline_vote_not_registered_subtitle" = "申し訳ありませんが投票が登録されていません、再度お試しください"; -"poll_timeline_vote_not_registered_title" = "投票が登録されていません"; -"poll_timeline_total_final_results" = "合計%lu票の投票に基づく最終結果"; +"poll_timeline_vote_not_registered_subtitle" = "投票できませんでした。もう一度やり直してください"; +"poll_timeline_vote_not_registered_title" = "投票できませんでした"; +"poll_timeline_total_final_results" = "合計%lu票に基づく最終結果"; "poll_timeline_total_final_results_one_vote" = "合計1票の投票に基づく最終結果"; -"poll_timeline_total_votes_not_voted" = "合計%lu票、投票すると結果を確認できます"; -"poll_timeline_total_one_vote_not_voted" = "合計1票、投票すると結果を確認できます"; +"poll_timeline_total_votes_not_voted" = "合計%lu票。投票すると結果を確認できます"; +"poll_timeline_total_one_vote_not_voted" = "合計1票。投票すると結果を確認できます"; "poll_timeline_total_votes" = "合計%lu票"; "poll_timeline_total_one_vote" = "合計1票"; "biometrics_cant_unlocked_alert_message_retry" = "再試行"; "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; -"invite_friends_share_text" = "%@ での連絡先: %@"; +"invite_friends_share_text" = "%@でお話ししましょう:%@"; "side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; -"onboarding_splash_login_button_title" = "既にアカウントを持っています"; +"onboarding_splash_login_button_title" = "既にアカウントがあります"; // Onboarding "onboarding_splash_register_button_title" = "アカウントを作成"; -"notice_room_created_by_you_for_dm" = "参加しました"; -"notice_room_created_for_dm" = "%@が参加しました"; +"notice_room_created_by_you_for_dm" = "参加しました。"; +"notice_room_created_for_dm" = "%@が参加しました。"; "onboarding_use_case_existing_server_button" = "サーバーに接続"; "callbar_only_single_active_group" = "タップしてグループ通話に参加 (%@)"; -"settings_confirm_media_size" = "送信時のサイズ確認"; -"settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面が表示されます。"; -"settings_contacts_enable_sync_description" = "IDサーバーを使用して連絡先を探すと同時に、連絡先があなたを探せるようにします。"; -"home_syncing" = "同期中"; +"settings_confirm_media_size" = "送信時にサイズを確認"; +"settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面を表示します。"; +"settings_contacts_enable_sync_description" = "IDサーバーを使用すると、連絡先を探したり、相手があなたを探したりできるようになります。"; +"home_syncing" = "同期しています"; "search_filter_placeholder" = "絞り込む"; // MARK: - Share invite link "share_invite_link_action" = "招待リンクを共有"; -"room_intro_cell_information_room_with_topic_sentence2" = "トピック: %@"; +"room_intro_cell_information_room_with_topic_sentence2" = "トピック:%@"; "room_intro_cell_information_room_sentence1_part3" = "の始まりです。 "; "room_intro_cell_information_room_sentence1_part1" = "ここが "; "room_intro_cell_information_dm_sentence1_part1" = "ここが "; @@ -1616,23 +1616,23 @@ "spaces_add_space_title" = "スペースを作成"; "spaces_creation_address" = "アドレス"; "spaces_creation_visibility_message" = "既存のスペースに参加するには、招待が必要です。"; -"spaces_creation_footer" = "この設定は後から変更できます"; -"onboarding_display_name_hint" = "この設定は後から変更できます"; +"spaces_creation_footer" = "これは後から変更できます"; +"onboarding_display_name_hint" = "これは後から変更できます"; "spaces_creation_visibility_title" = "作成するスペースの種類を選択してください"; -"space_public_join_rule_detail" = "誰でも参加可能、コミュニティー向け"; -"space_private_join_rule_detail" = "招待者のみ参加可能、個人やチーム向け"; -"onboarding_use_case_title" = "誰と話すことが一番多いですか?"; +"space_public_join_rule_detail" = "誰でも参加可能。コミュニティー向け"; +"space_private_join_rule_detail" = "招待者のみ参加可能。個人やチーム向け"; +"onboarding_use_case_title" = "誰と最もよく会話しますか?"; "onboarding_splash_page_4_message" = "Elementは職場利用にも最適です。世界で最も安全な組織によって信頼されています。"; -"onboarding_splash_page_4_title_no_pun" = "チームのためのメッセージング。"; -"onboarding_splash_page_3_message" = "E2Eで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; -"onboarding_splash_page_3_title" = "安全なメッセージ。"; -"onboarding_splash_page_2_message" = "データがどこに保存されるかを自分で選び、主導権と独立を手に入れよう。Matrixで接続。"; -"onboarding_splash_page_2_title" = "主導権はあなたにある。"; +"onboarding_splash_page_4_title_no_pun" = "あなたのチームのメッセージングに。"; +"onboarding_splash_page_3_message" = "エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; +"onboarding_splash_page_3_title" = "安全なメッセージのやりとり。"; +"onboarding_splash_page_2_message" = "会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。"; +"onboarding_splash_page_2_title" = "主導権を握るのは、あなたです。"; "onboarding_splash_page_1_message" = "オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。"; -"saving" = "保存中"; +"saving" = "保存しています"; // Activities -"loading" = "ロード中"; +"loading" = "読み込んでいます"; "confirm" = "確認"; "edit" = "編集"; "suggest" = "サジェスト"; @@ -1640,23 +1640,23 @@ "existing" = "既存"; "new_word" = "新規"; "stop" = "停止"; -"spaces_creation_post_process_creating_space_task" = "%@を作成中"; +"spaces_creation_post_process_creating_space_task" = "%@を作成しています"; "side_menu_coach_message" = "右にスワイプまたはタップで全てのルームが表示されます"; -"spaces_creation_post_process_creating_space" = "スペースを作成中"; -"spaces_creation_add_rooms_message" = "このスペースはあなた専用のため、他の人に通知されることはありません。この設定は後から変更できます。"; -"spaces_creation_add_rooms_title" = "どれを追加しますか?"; -"spaces_creation_sharing_type_me_and_teammates_detail" = "あなたとチームメイトの非公開のスペース"; +"spaces_creation_post_process_creating_space" = "スペースを作成しています"; +"spaces_creation_add_rooms_message" = "これはあなた専用のスペースで、他の人からは見えません。後からルームや会話を追加することもできます。"; +"spaces_creation_add_rooms_title" = "何を追加しますか?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "自分とチームメイトの非公開のスペース"; "spaces_creation_sharing_type_me_and_teammates_title" = "自分とチームメイト"; "spaces_creation_sharing_type_just_me_detail" = "ルームを整理するための非公開のスペース"; "spaces_creation_sharing_type_just_me_title" = "自分専用"; -"spaces_creation_sharing_type_message" = "参加者を選択してください%@。この設定は後から変更できます。"; -"spaces_creation_settings_message" = "詳細を入力してください。この設定は後から変更できます。"; -"spaces_creation_address_default_message" = "スペースは以下のように表記されます\n%@"; -"space_settings_current_address_message" = "スペースは以下のように表記されます\n%@"; -"space_topic" = "説明文"; -"spaces_creation_cancel_message" = "進捗状況は失われます。"; -"spaces_creation_cancel_title" = "スペースの作成を停止しますか?"; -"create_room_section_footer_type_private" = "招待した人のみが検索・参加できます。"; +"spaces_creation_sharing_type_message" = "適切な人が %@ アクセスできることを確認してください。この設定は後から変更できます。"; +"spaces_creation_settings_message" = "詳細を入力してください。これはいつでも変更できます。"; +"spaces_creation_address_default_message" = "スペースは以下で閲覧可能になります\n%@"; +"space_settings_current_address_message" = "スペースは以下で閲覧できます\n%@"; +"space_topic" = "詳細"; +"spaces_creation_cancel_message" = "これまでの設定は失われます。"; +"spaces_creation_cancel_title" = "スペースの作成を中止しますか?"; +"create_room_section_footer_type_private" = "招待した人のみが検索し、参加できます。"; // MARK: - Searchable Directory View Controller @@ -1668,8 +1668,8 @@ // MARK: Sign out warning "sign_out_existing_key_backup_alert_title" = "サインアウトしてよろしいですか?"; -"find_your_contacts_message" = "%@ であなたの連絡先を表示し、知人とのチャットを素早く始めます。"; -"find_your_contacts_footer" = "この設定はいつでも無効にできます"; +"find_your_contacts_message" = "%@であなたの連絡先を表示し、知人との会話をすぐ始められるようにしましょう。"; +"find_your_contacts_footer" = "この設定はいつでも無効にできます。"; "find_your_contacts_button_title" = "連絡先を検索"; "find_your_contacts_title" = "連絡先をリストアップ"; @@ -1679,7 +1679,7 @@ // MARK: - Invite friends -"invite_friends_action" = "友だちを %@ に招待"; +"invite_friends_action" = "友達を%@に招待"; "call_transfer_error_title" = "エラー"; "home_context_menu_mark_as_read" = "既読にする"; "home_context_menu_normal_priority" = "通常優先度"; @@ -1689,38 +1689,38 @@ // MARK: - Call Transfer "call_transfer_title" = "転送"; -"room_info_back_button_title" = "ルーム情報"; +"room_info_back_button_title" = "ルームの情報"; "create_room_processing" = "ルームを作成しています"; "call_transfer_users" = "ユーザー"; "home_context_menu_notifications" = "通知"; "home_context_menu_make_dm" = "連絡先に移動"; "home_context_menu_make_room" = "ルームに移動"; -"leave_space_title" = "%@ を退出"; -"room_participants_leave_success" = "ルームを退出しました"; +"leave_space_title" = "%@から退出"; +"room_participants_leave_success" = "ルームから退出しました"; "room_participants_leave_processing" = "退出しています"; "event_formatter_group_call_leave" = "退出"; "home_context_menu_leave" = "退出"; // Mark: Leave space -"leave_space_action" = "スペースを退出"; +"leave_space_action" = "スペースから退出"; "leave_space_selection_title" = "ルームを選択"; -"create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; -"create_room_suggest_room" = "スペースメンバーにおすすめ"; -"create_room_show_in_directory_footer" = "他の人が検索・参加できるようになります。"; -"create_room_promotion_header" = "PR"; +"create_room_section_footer_type_restricted" = "誰でもスペース名で検索し、参加できます。"; +"create_room_suggest_room" = "スペースのメンバーへのおすすめ"; +"create_room_show_in_directory_footer" = "他の人が検索し、参加できるようになります。"; +"create_room_promotion_header" = "プロモート"; "searchable_directory_search_placeholder" = "名前または ID"; "room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; -"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースメンバーに参加を推奨するものとして PR されます。"; +"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; // Room suggestion Settings "room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; -"room_details_promote_room_suggest_title" = "スペースメンバーへのおすすめ"; -"settings_default" = "デフォルトの通知"; +"room_details_promote_room_suggest_title" = "スペースのメンバーへのおすすめ"; +"settings_default" = "通知のデフォルト"; "pin_protection_reset_alert_action_reset" = "リセット"; "authentication_recaptcha_title" = "あなたは人間ですか?"; "authentication_verify_msisdn_waiting_button" = "コードを再送信"; -"authentication_choose_password_submit_button" = "パスワードをリセット"; +"authentication_choose_password_submit_button" = "パスワードを再設定"; "authentication_choose_password_signout_all_devices" = "全ての端末からサインアウト"; "authentication_choose_password_text_field_placeholder" = "新しいパスワード"; "authentication_terms_title" = "プライバシーポリシー"; @@ -1730,24 +1730,24 @@ "password_validation_error_contain_uppercase_letter" = "大文字を含める"; "password_validation_error_contain_lowercase_letter" = "小文字を含める"; /* The placeholder will show a number */ -"password_validation_error_max_length" = "%d 文字以下"; +"password_validation_error_max_length" = "%d文字以下"; /* The placeholder will show a number */ -"password_validation_error_min_length" = "%d 文字以上"; +"password_validation_error_min_length" = "%d文字以上"; // MARK: Password Validation -"password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; +"password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; "space_selector_empty_view_title" = "まだスペースがありません"; -"all_chats_empty_list_placeholder_title" = "未読はありません"; +"all_chats_empty_list_placeholder_title" = "未読はありません。"; "all_chats_empty_unreads_placeholder_message" = "未読のメッセージがある場合は、ここに表示されます。"; -"room_notifs_settings_account_settings" = "アカウント設定"; +"room_notifs_settings_account_settings" = "アカウントの設定"; "room_access_settings_screen_upgrade_alert_upgrading" = "ルームをアップグレードしています"; "room_access_settings_screen_upgrade_alert_upgrade_button" = "アップグレード"; "room_access_settings_screen_edit_spaces" = "スペースを編集"; "room_access_settings_screen_upgrade_required" = "アップグレードが必要"; "room_access_settings_screen_upgrade_alert_title" = "ルームをアップグレード"; -"room_access_settings_screen_public_message" = "誰でも検索・参加できます。"; -"room_access_settings_screen_private_message" = "招待された人だけが検索・参加できます。"; -"room_access_settings_screen_message" = "誰が %@ を検索・参加できるか選択してください。"; +"room_access_settings_screen_public_message" = "誰でも検索し、参加できます。"; +"room_access_settings_screen_private_message" = "招待された人のみ検索し、参加できます。"; +"room_access_settings_screen_message" = "誰が%@を検索し、参加できるか選択してください。"; "space_settings_access_section" = "このスペースにアクセスできる人は?"; "room_access_settings_screen_title" = "このルームにアクセスできる人は?"; "room_notifs_settings_none" = "なし"; @@ -1758,24 +1758,24 @@ "room_details_notifs" = "通知"; "location_sharing_invalid_power_level_title" = "位置情報(ライブ)の共有に必要な権限がありません"; "settings_labs_enable_live_location_sharing" = "位置情報(ライブ)の共有 - 現在の位置情報を共有(開発中の機能。位置情報が一時的にルームの履歴に残ります)"; -"event_formatter_message_deleted" = "削除済みのメッセージ"; +"event_formatter_message_deleted" = "メッセージが削除されました"; "home_context_menu_unfavourite" = "お気に入りから削除"; "home_context_menu_favourite" = "お気に入り"; "all_chats_user_menu_settings" = "ユーザー設定"; "all_chats_edit_layout_show_filters" = "フィルターを表示"; -"all_chats_edit_layout_show_recents" = "最近使用したものを表示"; +"all_chats_edit_layout_show_recents" = "最近の履歴を表示"; "all_chats_edit_layout_alphabetical_order" = "アルファベット順で並び替え"; "all_chats_edit_layout_activity_order" = "アクティビティで並び替え"; "space_selector_create_space" = "スペースを作成"; -"space_selector_empty_view_information" = "スペースは、ルームや連絡先をグループ化する方法です。以下からスペースを作成できます。"; +"space_selector_empty_view_information" = "スペースは、ルームと連絡先をまとめる方法です。はじめに、スペースを作成しましょう。"; "all_chats_all_filter" = "全て"; -"all_chats_edit_layout_recents" = "最近"; -"all_chats_edit_layout_unreads" = "未読"; -"all_chats_section_title" = "チャット"; +"all_chats_edit_layout_recents" = "履歴"; +"all_chats_edit_layout_unreads" = "未読あり"; +"all_chats_section_title" = "会話"; // Mark: - All Chats -"all_chats_title" = "全てのチャット"; +"all_chats_title" = "全ての会話"; "location_sharing_live_loading" = "位置情報(ライブ)を読み込んでいます…"; "location_sharing_live_list_item_stop_sharing_action" = "停止"; "location_sharing_live_list_item_current_user_display_name" = "あなた"; @@ -1789,7 +1789,7 @@ "location_sharing_live_share_title" = "位置情報(ライブ)を共有"; "service_terms_modal_decline_button" = "拒否"; "service_terms_modal_accept_button" = "同意"; -"service_terms_modal_description_identity_server" = "この操作により、端末の連絡先にあなたの電話番号や電子メールを保存している人があなたを検索できるようになります。"; +"service_terms_modal_description_identity_server" = "これにより、端末の連絡先にあなたの電話番号や電子メールを保存している人が、あなたを検索できるようになります。"; // Service terms "service_terms_modal_title_message" = "続行するには、以下の利用規約に同意してください"; @@ -1797,5 +1797,1031 @@ // Alert explaining what an identity server / integration manager is. "service_terms_modal_information_title_identity_server" = "IDサーバー"; -"location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; +"location_sharing_invalid_power_level_message" = "このルームでの位置情報(ライブ)の共有には適切な権限が必要です。"; "location_sharing_live_error" = "位置情報(ライブ)のエラー"; +"all_chats_edit_layout" = "レイアウトの設定"; + +// Crypto +"e2e_enabling_on_app_update" = "%@はエンドツーエンドの暗号化に対応しましたが、有効にするには再度ログインする必要があります。\n\nアプリケーションの設定から今すぐ、もしくは後で行うことができます。"; +"analytics_prompt_stop" = "共有を停止"; +"analytics_prompt_not_now" = "後で"; +"analytics_prompt_point_3" = "これはいつでも設定から無効にできます"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "私たちは、情報を第三者と共有することはありません"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "私たちは、アカウントのいかなるデータも記録したり分析したりすることはありません"; +"analytics_prompt_terms_link_upgrade" = "ここ"; +"call_jitsi_unable_to_start" = "グループ通話を開始できません"; +"network_offline_message" = "オフラインです。接続を確認してください。"; +"network_offline_title" = "オフラインです"; +"event_formatter_group_call_join" = "参加"; +"event_formatter_group_call" = "グループ通話"; +"event_formatter_call_end_call" = "通話を終了"; +"event_formatter_call_retry" = "再試行"; +"event_formatter_call_decline" = "拒否"; +"event_formatter_call_connection_failed" = "接続に失敗しました"; +"event_formatter_call_ringing" = "呼び出しています…"; +"event_formatter_call_connecting" = "接続しています…"; +"call_ringing" = "呼び出しています…"; +"room_notifs_settings_manage_notifications" = "通知は%@で管理できます"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "メンバーを新しいルームに自動的に招待"; +"room_access_settings_screen_upgrade_alert_note" = "アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。"; +"room_access_settings_screen_upgrade_alert_message_no_param" = "上位のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; +"room_access_settings_screen_upgrade_alert_message" = "%@に属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "ルームへのアクセス"; +"room_details_polls" = "アンケートの履歴"; +// User sessions management +"user_sessions_settings" = "セッションを管理"; +"manage_session_sign_out_other_sessions" = "他の全てのセッションからサインアウト"; +"manage_session_rename" = "セッション名を変更"; +"manage_session_name_info_link" = "詳細を表示"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "セッション名は連絡先にも表示されます。%@"; +"manage_session_name_hint" = "セッション名を設定すると、端末をより簡単に認識できるようになります。"; +"security_settings_coming_soon" = "申し訳ありません。このアクションは%@ iOSではまだ利用できません。他のMatrixのクライアントを使って設定してください。将来的には%@ iOSでも実装される予定です。"; +"security_settings_secure_backup_reset" = "再設定"; +"security_settings_secure_backup_info_checking" = "確認しています…"; +"settings_presence_offline_mode_description" = "有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。"; +"settings_presence_offline_mode" = "オフラインモード"; +"settings_enable_room_message_bubbles" = "吹き出しでメッセージを表示"; +"settings_discovery_accept_terms" = "IDサーバーの利用規約を承諾"; +"settings_labs_confirm_crypto_sdk" = "この機能は実験段階のため、予期したとおりに機能せず、意図しない結果を引き起こす可能性があります。この機能を無効にするには、ログアウトして再度ログインしてください。自分自身の判断で慎重に使ってください。"; +"settings_labs_enable_voice_broadcast" = "音声配信"; +"settings_labs_enable_new_app_layout" = "アプリケーションの新しいレイアウト"; +"settings_labs_enable_new_client_info_feature" = "クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定"; +"settings_labs_enable_new_session_manager" = "新しいセッションマネージャー"; +"settings_labs_use_only_latest_user_avatar_and_name" = "ユーザーの最新のアバターと名前をメッセージの履歴に表示"; +"settings_labs_enable_threads" = "メッセージのスレッド機能"; +"settings_labs_enabled_polls" = "アンケート"; +"settings_ui_show_redactions_in_room_history" = "削除されたメッセージに関する通知を表示"; +"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は %@ を許可(IPアドレスが通話中に共有されます)。"; +"settings_callkit_info" = "ロック画面に着信を表示。%@の着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; +"settings_notifications_disabled_alert_title" = "通知が無効です"; +"threads_discourage_information_1" = "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。 "; +"threads_beta_cancel" = "後で"; +"threads_beta_enable" = "試してみる"; +"threads_beta_information_link" = "詳細を表示"; +"threads_beta_title" = "スレッド"; +"threads_notice_done" = "了解"; +"threads_notice_title" = "スレッド機能は正式版になりました🎉"; +"message_from_a_thread" = "スレッドから"; +"room_accessibility_record_voice_message" = "音声メッセージを録音"; +"room_event_copy_link_info" = "リンクをクリップボードにコピーしました。"; +"room_event_action_end_poll" = "アンケートを終了"; +"room_event_action_remove_poll" = "アンケートを削除"; +"room_participants_invite_prompt_to_msg" = "%@を%@に招待してよろしいですか?"; +"find_your_contacts_identity_service_error" = "IDサーバーに接続できません。"; +"contacts_address_book_permission_denied" = "端末の電話帳を%@が読み取ることは許可されていません"; +/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. Note the > at the start indicates "more than 20 results". */ +"directory_search_results_more_than" = ">%2$@の検索結果%1$tu件"; +/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. */ +"directory_search_results" = "%2$@の検索結果%1$tu件"; +"room_recents_unknown_room_error_message" = "このルームを発見できません。存在することを確認してください"; +"room_creation_dm_error" = "ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。"; +"password_policy_pwd_in_dict_error" = "パスワードが辞書で見つかりました。許可できません。"; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "パスワードが短すぎます"; +"password_validation_error_header" = "指定したパスワードは以下の要件を満たしていません:"; +"authentication_qr_login_failure_retry" = "もう一度試す"; +"authentication_qr_login_failure_request_denied" = "リクエストはもう一方の端末で拒否されました。"; +"authentication_qr_login_failure_invalid_qr" = "QRコードが不正です。"; +"authentication_qr_login_loading_waiting_signin" = "端末のサインインを待機しています。"; +"authentication_qr_login_loading_connecting_device" = "端末に接続しています"; +"authentication_qr_login_confirm_subtitle" = "以下のコードが他の端末と一致していることを確認してください:"; +"authentication_qr_login_confirm_title" = "安全な接続を確立しました"; +"authentication_qr_login_scan_title" = "QRコードをスキャン"; +"authentication_qr_login_display_subtitle" = "サインアウトした端末で以下のQRコードをスキャンしてください。"; +"authentication_qr_login_start_title" = "QRコードをスキャン"; +"authentication_terms_policy_url_error" = "選択した運営方針が見つかりませんでした。後でもう一度やり直してください。"; +/* The placeholder will show the homeserver's domain */ +"authentication_terms_message" = "%@の利用規約と運営方針を確認してください"; +"authentication_verify_msisdn_invalid_phone_number" = "電話番号が不正です"; +/* The placeholder will show the phone number that was entered. */ +"authentication_verify_msisdn_waiting_message" = "コードが%@に送信されました"; +"authentication_verify_msisdn_waiting_title" = "電話番号を認証してください"; +"authentication_verify_msisdn_otp_text_field_placeholder" = "確認コード"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_msisdn_input_message" = "%@はアカウントの認証が必要です"; +"authentication_verify_msisdn_input_title" = "電話番号を入力してください"; +"authentication_choose_password_not_verified_message" = "メールボックスを確認してください"; +"authentication_choose_password_input_message" = "パスワードは8文字以上に設定してください"; +"authentication_choose_password_input_title" = "パスワードを選択"; +"authentication_forgot_password_waiting_button" = "電子メールを再送信"; +/* The placeholder will show the email address that was entered. */ +"authentication_forgot_password_waiting_message" = "%@に送信された手順に従ってください。"; +"authentication_forgot_password_waiting_title" = "電子メールを確認してください。"; +"authentication_forgot_password_text_field_placeholder" = "メールアドレス"; +/* The placeholder will show the homeserver's domain */ +"authentication_forgot_password_input_message" = "%@は認証リンクを送信します"; +"authentication_forgot_password_input_title" = "電子メールを入力してください"; +"authentication_verify_email_waiting_button" = "電子メールを再送信"; +"authentication_verify_email_waiting_hint" = "電子メールが届いていませんか?"; +/* The placeholder will show the email address that was entered. */ +"authentication_verify_email_waiting_message" = "%@に送信された手順に従ってください。"; +"authentication_verify_email_waiting_title" = "メールアドレスを認証してください。"; +"authentication_verify_email_text_field_placeholder" = "メールアドレス"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_email_input_message" = "%@はアカウントの認証が必要です"; +"authentication_verify_email_input_title" = "電子メールを入力してください"; +"authentication_cancel_flow_confirmation_message" = "アカウントがまだ作成されていません。登録を中止しますか?"; +"authentication_server_selection_server_url" = "ホームサーバーのURL"; +"authentication_login_with_qr" = "QRコードでサインイン"; +"authentication_login_username" = "ユーザー名 / メールアドレス / 電話番号"; +"authentication_login_title" = "おかえりなさい!"; +"authentication_registration_password_footer" = "8文字以上にしてください"; +"authentication_registration_username_footer" = "これは後から変更できません"; +"authentication_registration_username" = "ユーザー名"; + +// MARK: Authentication +"authentication_registration_title" = "アカウントを作成"; +"onboarding_celebration_button" = "進みましょう"; +"onboarding_celebration_message" = "プロフィールは設定画面からいつでも更新できます"; +"onboarding_celebration_title" = "問題ありません!"; +"onboarding_avatar_accessibility_label" = "プロフィール画像"; +"onboarding_avatar_title" = "プロフィール画像を追加"; +"onboarding_display_name_max_length" = "表示名は256字以下にしてください"; +"onboarding_display_name_placeholder" = "表示名"; +"onboarding_display_name_message" = "メッセージを送信する際に表示されます。"; +"onboarding_display_name_title" = "表示名を選択"; +"onboarding_personalization_skip" = "このステップをスキップ"; +"onboarding_personalization_save" = "保存して続行"; +"onboarding_congratulations_home_button" = "ホームに移動"; +"onboarding_congratulations_personalize_button" = "プロフィールを設定"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "あなたのアカウント %@ が作成されました"; +"onboarding_congratulations_title" = "おめでとうございます!"; +"onboarding_use_case_existing_server_message" = "既存のサーバーに参加しますか?"; +"onboarding_use_case_skip_button" = "この質問をスキップ"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "迷っていますか?%@"; +"onboarding_use_case_community_messaging" = "コミュニティー"; +"onboarding_use_case_work_messaging" = "チーム"; +"onboarding_use_case_personal_messaging" = "友達と家族"; +"onboarding_use_case_message" = "みんなと繋がる手助けをいたします"; +"onboarding_splash_page_1_title" = "自分の会話は、自分のもの。"; +"accessibility_selected" = "選択済"; +"invite_to" = "%@に招待"; +"joining" = "参加しています"; +"key_backup_setup_passphrase_passphrase_placeholder" = "パスフレーズを入力"; +"key_backup_setup_passphrase_passphrase_title" = "入力"; +"key_backup_setup_passphrase_info" = "鍵のコピーを暗号化してサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。\n\n最大限のセキュリティーを確保するために、Matrixのアカウントのパスワードと異なるものに設定することが大切です。"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "バックアップをセキュリティーフレーズで保護"; +"key_backup_setup_intro_manual_export_action" = "手動で鍵をエクスポート"; +"key_backup_setup_intro_manual_export_info" = "(高度)"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "この端末を鍵のバックアップに接続"; +"key_backup_setup_intro_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。\n\n鍵を失くさないよう、鍵を安全にバックアップしてください。"; + +// Intro + +"key_backup_setup_intro_title" = "暗号化されたメッセージを決して失わないために"; +"key_backup_setup_skip_alert_skip_action" = "スキップ"; +"key_backup_setup_skip_alert_message" = "ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。"; +"key_backup_setup_skip_alert_title" = "よろしいですか?"; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "鍵のバックアップ"; + +// Banner + +"secure_backup_setup_banner_title" = "セキュアバックアップ"; +"secure_key_backup_setup_cancel_alert_message" = "いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。\n\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。"; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "よろしいですか?"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "削除"; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "ロックを解除"; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "セキュリティーフレーズを使用"; +"service_terms_modal_table_header_integration_manager" = "インテグレーションマネージャーの利用規約"; +"service_terms_modal_table_header_identity_server" = "IDサーバーの利用規約"; +"service_terms_modal_footer" = "この設定はいつでも無効にできます。"; +"share_extension_send_now" = "送信"; +"room_widget_permission_room_id_permission" = "ルームID"; +"room_widget_permission_widget_id_permission" = "ウィジェットID"; +"room_widget_permission_theme_permission" = "あなたのテーマ"; +"room_widget_permission_user_id_permission" = "あなたのユーザーID"; +"room_widget_permission_avatar_url_permission" = "あなたのアバターのURL"; +"room_widget_permission_display_name_permission" = "あなたの表示名"; +"room_widget_permission_information_title" = "これを使用するとデータが%@と共有される可能性があります:\n"; +"room_widget_permission_webview_information_title" = "これを使用すると、クッキーが設定され、データが%@と共有される可能性があります:\n"; +"room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; +"key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存してください。"; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(高度)セキュリティーキーで設定"; + +// Success + +"key_backup_setup_success_title" = "成功しました!"; +"key_backup_recover_invalid_passphrase_title" = "セキュリティーフレーズが正しくありません"; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "鍵をバックアップしています。"; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "セキュリティーキーを保存"; + +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; +"key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした。正しいセキュリティーフレーズを入力したことを確認してください。"; +"key_backup_recover_invalid_recovery_key_title" = "セキュリティーキーが一致しません"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "バックアップを復元しています…"; +"key_backup_recover_invalid_recovery_key" = "この鍵ではバックアップを復号化できませんでした。正しいセキュリティーキーを入力したことを確認してください。"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、暗号化されたメッセージの履歴のロックを解除できます"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "セキュリティーフレーズが分かりませんか?そんなときは "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; +"key_backup_recover_from_passphrase_recover_action" = "履歴のロックを解除"; +"location_sharing_live_timer_selector_title" = "位置情報を共有する期間を選択してください。"; +"location_sharing_live_timer_selector_short" = "15分"; +"location_sharing_live_timer_selector_medium" = "1時間"; +"location_sharing_live_timer_selector_long" = "8時間"; +"location_sharing_live_no_user_locations_error_title" = "ユーザーの位置情報はありません"; +"location_sharing_live_stop_sharing_error" = "位置情報の共有の停止に失敗しました"; +"location_sharing_live_stop_sharing_progress" = "位置情報の共有を停止"; +"location_sharing_live_lab_promotion_text" = "注意:これは一時的な実装による試験機能です。あなたの位置情報の履歴はルームのメンバーに対して永続的に閲覧可能となります。"; +"location_sharing_live_lab_promotion_title" = "位置情報(ライブ)の共有"; +"location_sharing_live_lab_promotion_activation" = "位置情報(ライブ)の共有を有効にする"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"user_sessions_overview_title" = "セッション"; +"user_sessions_overview_security_recommendations_section_title" = "セキュリティーに関する勧告"; +"user_sessions_overview_security_recommendations_section_info" = "以下の勧告に従い、アカウントのセキュリティーを改善しましょう。"; +"user_sessions_overview_security_recommendations_unverified_title" = "未認証のセッション"; +"user_sessions_overview_security_recommendations_inactive_title" = "非アクティブなセッション"; +"user_sessions_overview_security_recommendations_inactive_info" = "使用していない古いセッション(90日以上使用されていません)からのサインアウトを検討してください。"; +"user_sessions_overview_other_sessions_section_title" = "その他のセッション"; +"user_sessions_overview_other_sessions_section_info" = "セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや使用していないセッションからサインアウトしてください。"; +"user_sessions_show_location_info" = "IPアドレスを表示"; +"user_sessions_hide_location_info" = "IPアドレスを表示しない"; +"user_sessions_overview_current_session_section_title" = "現在のセッション"; +"user_sessions_view_all_action" = "全て表示(%d)"; +"user_session_verified" = "認証済のセッション"; +"user_session_unverified" = "未認証のセッション"; +"user_session_verification_unknown" = "認証の状態が不明です"; +"user_session_verified_short" = "認証済"; +"user_session_unverified_short" = "未認証"; +"user_session_verification_unknown_short" = "不明"; +"user_session_verify_action" = "セッションを認証"; +"user_session_view_details" = "詳細を表示"; +"major_update_learn_more_action" = "詳細を表示"; +"user_session_learn_more" = "詳細を表示"; +"user_session_verified_additional_info" = "現在のセッションは安全なメッセージのやりとりに対応しています。"; +"user_session_unverified_additional_info" = "より安全なメッセージのやりとりのために、現在のセッションを認証しましょう。"; +"user_other_session_unverified_additional_info" = "セキュリティーと安定性の観点から、このセッションを認証するかサインアウトしてください。"; +"user_other_session_permanently_unverified_additional_info" = "このセッションは暗号化をサポートしていないため、認証できません。"; +"user_other_session_verified_additional_info" = "このセッションは安全なメッセージのやりとりの準備ができています。"; +"user_session_push_notifications" = "プッシュ通知"; +"user_session_got_it" = "了解"; +"user_session_verified_session_title" = "認証済のセッション"; +"user_session_unverified_session_title" = "未認証のセッション"; +"user_session_inactive_session_title" = "非アクティブなセッション"; +"user_session_rename_session_title" = "セッション名の変更"; +"user_other_session_security_recommendation_title" = "その他のセッション"; +"user_other_session_unverified_sessions_header_subtitle" = "セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。"; +"user_other_session_current_session_details" = "現在のセッション"; +"user_other_session_verified_sessions_header_subtitle" = "セキュリティーを最大限に高めるには、不明なセッションや使用していないセッションからサインアウトしてください。"; +"user_other_session_filter" = "絞り込む"; +"user_other_session_filter_menu_all" = "全てのセッション"; +"user_other_session_filter_menu_verified" = "認証済"; +"user_other_session_filter_menu_unverified" = "未認証"; +"user_other_session_filter_menu_inactive" = "非アクティブ"; +"user_other_session_no_inactive_sessions" = "使用していないセッションはありません。"; +"user_other_session_no_verified_sessions" = "認証済のセッションはありません。"; +"user_other_session_no_unverified_sessions" = "未認証のセッションはありません。"; +"user_other_session_clear_filter" = "絞り込みを解除"; +"user_other_session_menu_select_sessions" = "セッションを選択"; +"user_other_session_menu_sign_out_sessions" = "%@件のセッションからサインアウト"; +"device_name_desktop" = "%@デスクトップ"; +"device_name_unknown" = "不明なクライアント"; +"device_type_name_desktop" = "デスクトップ"; +"device_type_name_web" = "ウェブ"; +"device_type_name_mobile" = "携帯端末"; +"device_type_name_unknown" = "不明"; +"user_session_details_title" = "セッションの詳細"; +"user_session_details_session_section_header" = "セッション"; +"user_session_details_application_section_header" = "アプリケーション"; +"user_session_details_device_section_header" = "端末"; +"user_session_details_session_name" = "セッション名"; +"user_session_details_session_id" = "セッションID"; +"user_session_details_last_activity" = "直近のアクティビティー"; +"user_session_details_device_ip_address" = "IPアドレス"; +"user_session_details_device_browser" = "ブラウザー"; +"user_session_details_device_os" = "オペレーティングシステム"; +"user_session_details_application_name" = "名前"; +"user_session_details_application_version" = "バージョン"; +"user_session_details_application_url" = "URL"; +"user_session_overview_current_session_title" = "現在のセッション"; +"user_session_overview_session_title" = "セッション"; +"user_session_overview_session_details_button_title" = "セッションの詳細"; +"wysiwyg_composer_start_action_stickers" = "ステッカー"; +"wysiwyg_composer_start_action_attachments" = "添付ファイル"; +"wysiwyg_composer_start_action_polls" = "アンケート"; +"wysiwyg_composer_start_action_location" = "位置情報"; +"wysiwyg_composer_start_action_camera" = "カメラ"; +"wysiwyg_composer_start_action_voice_broadcast" = "音声配信"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "太字にする"; +"wysiwyg_composer_format_action_italic" = "斜字体にする"; + + + +// Links +"wysiwyg_composer_link_action_text" = "テキスト"; +"wysiwyg_composer_link_action_link" = "リンク"; +"wysiwyg_composer_link_action_create_title" = "リンクを作成"; +"wysiwyg_composer_link_action_edit_title" = "リンクを編集"; +"deselect_all" = "全ての選択を解除"; +"ignore_user" = "ユーザーを無視"; +"notice_room_name_removed_for_dm" = "%@がルーム名を削除しました"; +// New +"notice_room_join_rule_invite" = "%@がこのルームを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_for_dm" = "%@がこれを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_by_you" = "このルームを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_by_you_for_dm" = "これを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_public" = "%@がルームを公開しました。"; +"notice_room_join_rule_public_for_dm" = "%@が公開しました。"; +"notice_room_join_rule_public_by_you" = "ルームを公開しました。"; +"notice_room_join_rule_public_by_you_for_dm" = "公開しました。"; +"notice_room_power_level_intro_for_dm" = "メンバーの権限レベル:"; +"notice_room_aliases_for_dm" = "エイリアス:%@"; +"notice_voice_broadcast_live" = "ライブ配信"; +"notice_voice_broadcast_ended" = "%@が音声配信を終了しました。"; +"notice_voice_broadcast_ended_by_you" = "音声配信を終了しました。"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "この暗号化されたメッセージの真正性はこの端末では保証できません。"; +"room_left_for_dm" = "退出しました"; +"message_reply_to_sender_sent_their_live_location" = "位置情報(ライブ)。"; +"attachment_unsupported_preview_title" = "プレビューできません"; +"attachment_unsupported_preview_message" = "このファイルの種類はサポートしていません。"; +"microphone_access_not_granted_for_voice_message" = "音声メッセージにはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; +"notice_room_third_party_invite_for_dm" = "%@が%@を招待しました"; +"notice_room_name_changed_for_dm" = "%@が名前を%@に変更しました。"; +"notice_room_third_party_invite_by_you" = "%@にルームへの招待を送りました"; +"notice_room_third_party_invite_by_you_for_dm" = "%@を招待しました"; +"notice_room_third_party_registered_invite_by_you" = "%@の招待を受け入れました"; +"notice_room_reject_by_you" = "招待を拒否しました"; +"notice_room_withdraw_by_you" = "%@の招待を取り下げました"; +"notice_declined_video_call_by_you" = "通話を拒否しました"; +"notice_room_history_visible_to_anyone_by_you" = "今後のルームの履歴を「誰でも」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_by_you" = "今後のルームの履歴を「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_by_you_for_dm" = "今後のメッセージを「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "今後のメッセージを「全員 (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "今後のルームの履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; +"call_more_actions_audio_use_device" = "端末のスピーカー"; +"call_more_actions_transfer" = "転送"; +"call_voice_with_user" = "%@との音声通話"; +"call_transfer_to_user" = "%@に転送"; +"pin_protection_confirm_pin" = "PINコードを確認してください"; +"pin_protection_choose_pin" = "PINコードを設定してください"; +"pin_protection_choose_pin_welcome_after_register" = "ようこそ。"; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "おかえりなさい。"; +"major_update_done_action" = "了解"; +"cross_signing_setup_banner_subtitle" = "他の端末をより簡単に認証"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "暗号化の設定"; +"secrets_reset_reset_action" = "リセット"; +"secrets_reset_warning_message" = "履歴とメッセージが消去され、信頼済の端末、信頼済のユーザーが取り消されます。"; +"secrets_reset_warning_title" = "全てをリセットすると"; +"secrets_reset_information" = "この端末を認証できる他の端末が全くない場合にのみ、続行してください。"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "全てリセット"; + + +"secrets_setup_recovery_passphrase_summary_title" = "セキュリティーフレーズを保存"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "確認"; +"secrets_setup_recovery_passphrase_confirm_information" = "確認のため、セキュリティーフレーズを再入力してください。"; +"secrets_setup_recovery_passphrase_additional_information" = "Matrixのアカウントパスワードと違うものにしてください。"; +"secrets_setup_recovery_passphrase_information" = "あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。"; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "セキュリティーフレーズを設定"; +"secrets_setup_recovery_key_storage_alert_title" = "大切に保護しましょう"; +"secrets_setup_recovery_key_export_action" = "保存"; +"secrets_setup_recovery_key_loading" = "読み込んでいます…"; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "セキュリティーキーを保存"; +"secrets_recovery_with_key_invalid_recovery_key_title" = "機密ストレージにアクセスできません"; +"secrets_recovery_with_key_recover_action" = "鍵を使用"; +"secrets_recovery_with_key_recovery_key_placeholder" = "セキュリティーキーを入力"; +"secrets_recovery_with_key_recovery_key_title" = "入力"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "続行するにはセキュリティーキーを入力してください。"; + +// Recover with key + +"secrets_recovery_with_key_title" = "セキュリティーキー"; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "機密ストレージにアクセスできません"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "。"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "セキュリティーフレーズが分かりませんか?そんなときは "; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "セキュリティーフレーズを入力"; +"secrets_recovery_with_passphrase_passphrase_title" = "入力"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "セキュリティーフレーズ"; +"secrets_recovery_reset_action_part_2" = "全てリセット"; +"user_verification_session_details_verify_action_current_user_manually" = "テキストを使って手動で認証"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "この端末でスキャン"; +"emoji_picker_activity_category" = "アクティビティー"; +"device_verification_emoji_corn" = "とうもろこし"; +"device_verification_emoji_strawberry" = "いちご"; +"device_verification_emoji_apple" = "リンゴ"; +"device_verification_emoji_banana" = "バナナ"; +"device_verification_emoji_fire" = "炎"; +"device_verification_emoji_cloud" = "雲"; +"device_verification_emoji_moon" = "月"; +"device_verification_emoji_globe" = "地球"; +"device_verification_emoji_mushroom" = "きのこ"; +"device_verification_emoji_cactus" = "サボテン"; +"device_verification_emoji_tree" = "木"; +"device_verification_emoji_flower" = "花"; +"device_verification_emoji_butterfly" = "ちょうちょ"; +"device_verification_emoji_octopus" = "たこ"; +"device_verification_emoji_fish" = "魚"; +"device_verification_emoji_turtle" = "亀"; +"device_verification_emoji_penguin" = "ペンギン"; +"device_verification_emoji_rooster" = "ニワトリ"; +"device_verification_emoji_panda" = "パンダ"; +"device_verification_emoji_rabbit" = "うさぎ"; +"device_verification_emoji_elephant" = "ゾウ"; +"device_verification_emoji_pig" = "ブタ"; +"device_verification_emoji_unicorn" = "ユニコーン"; +"device_verification_emoji_horse" = "馬"; +"device_verification_emoji_lion" = "ライオン"; +"device_verification_emoji_cat" = "猫"; + +// MARK: Emoji +"device_verification_emoji_dog" = "犬"; + +// User + +"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; +"key_verification_verified_new_session_title" = "新しいセッションを認証しました!"; +"device_verification_verified_got_it_button" = "了解"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "認証しました!"; + +// Device + +"device_verification_verify_wait_partner" = "相手の承認を待機しています…"; +"key_verification_manually_verify_device_validate_action" = "認証"; +"key_verification_manually_verify_device_additional_information" = "一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。"; +"key_verification_manually_verify_device_key_title" = "セッションキー"; +"key_verification_manually_verify_device_id_title" = "セッションID"; +"key_verification_manually_verify_device_name_title" = "セッション名"; +"key_verification_manually_verify_device_instruction" = "他のセッションのユーザー設定で、以下を比較して承認してください:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "テキストを使って手動で認証"; +"key_verification_verify_sas_additional_information" = "セキュリティーを最大限に高めるには、対面で行うか、他の信頼できる通信手段を使用してください。"; +"key_verification_verify_sas_title_number" = "番号を比較"; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "既存のセッションにアクセスできない場合"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "セキュリティーフレーズまたはセキュリティーキーを使用"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "セキュリティーキーを使用"; +"device_verification_self_verify_wait_new_sign_in_title" = "このログインを認証"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "確認"; +"key_verification_alert_body" = "アカウントが安全かどうか確認してください。"; + +// Unverified sessions +"key_verification_alert_title" = "未認証のセッションがあります"; +"key_verification_self_verify_current_session_alert_validate_action" = "認証"; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "このセッションを認証"; +"device_verification_self_verify_start_waiting" = "待機しています…"; +"device_verification_self_verify_start_information" = "新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。"; +"device_verification_self_verify_start_verify_action" = "認証を開始"; +"device_verification_start_use_legacy_action" = "レガシー認証を使用"; +"device_verification_start_verify_button" = "認証を開始"; + +// MARK: Start +"device_verification_start_title" = "短い文字列を比較して認証"; +"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済として表示し、あなたのセッションも相手に信頼済として表示されます。"; +"device_verification_incoming_description_1" = "このセッションを認証すると、信頼済として表示します。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; + +// MARK: Incoming +"device_verification_incoming_title" = "認証のリクエストが届いています"; +"device_verification_error_cannot_load_device" = "セッションの情報を読み込めません。"; +"device_verification_cancelled_by_me" = "認証がキャンセルされました。理由:%@"; +"device_verification_cancelled" = "相手が認証をキャンセルしました。"; +"device_verification_security_advice_number" = "数字を比較して、同じ順番で現れていることを確認してください。"; +"key_verification_this_session_title" = "このセッションを認証"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "セッションを認証"; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "暗号化されたメッセージは不要です"; +"sign_out_key_backup_in_progress_alert_title" = "鍵をバックアップしています。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "暗号化されたメッセージは不要です"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "セキュアバックアップを使用開始"; +"sign_out_non_existing_key_backup_alert_title" = "今サインアウトすると、あなたの暗号化されたメッセージにアクセスできなくなります"; +"sign_out_confirmation_message" = "サインアウトしてよろしいですか?"; + +// MARK: Sign out warning + +"sign_out" = "サインアウト"; + +// Success + +"key_backup_recover_success_info" = "バックアップを復元しました!"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "セキュリティーキーを無くしましたか? 設定で新しいセキュリティーキーを設定できます。"; +"key_backup_recover_from_recovery_key_recover_action" = "履歴のロックを解除"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "セキュリティーキーを入力"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "入力"; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "セキュリティーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます"; +"call_video_with_user" = "%@とのビデオ通話"; +"call_more_actions_hold" = "保留"; +"notice_encryption_enabled_unknown_algorithm_by_you" = "エンドツーエンド暗号化(認識されていないアルゴリズム %@)を有効にしました。"; +"notice_room_name_removed_by_you_for_dm" = "ルーム名を削除しました"; +"notice_room_third_party_revoked_invite_by_you" = "%@のルームへの招待を取り消しました"; +"notice_declined_video_call" = "%@が通話を拒否しました"; +"attachment_size_prompt_message" = "これは設定から無効にできます。"; +"message_reply_to_sender_sent_their_location" = "位置情報を共有しました。"; +"message_reply_to_sender_sent_a_voice_message" = "音声メッセージを送信しました。"; +"wysiwyg_composer_start_action_text_formatting" = "テキストの装飾"; +"user_session_details_device_model" = "形式"; +"user_inactive_session_item_with_date" = "90日以上使用されていません(%@)"; +"user_inactive_session_item" = "90日以上使用されていません"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +"user_other_session_selected_count" = "%@件選択済"; +"user_session_inactive_session_description" = "非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。\n\n使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。"; +"user_session_permanently_unverified_session_description" = "このセッションは暗号化をサポートしていないため、認証できません。\n\nこのセッションでは、暗号化が有効になっているルームに参加することができません。\n\nセキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。"; +"user_sessions_overview_security_recommendations_unverified_info" = "未認証のセッションを認証するか、サインアウトしてください。"; +"location_sharing_live_list_item_time_left" = "残り%@"; +"location_sharing_live_viewer_title" = "位置情報"; +"location_sharing_live_map_callout_title" = "位置情報を共有"; +"location_sharing_pin_drop_share_title" = "この位置情報を送信"; +"location_sharing_static_share_title" = "現在の位置情報を送信"; +"location_sharing_map_loading_error" = "地図を読み込めません\nこのホームサーバーは地図を読み込むよう設定されていません"; +"location_sharing_allow_background_location_cancel_action" = "後で"; +"location_sharing_allow_background_location_validate_action" = "設定"; +"location_sharing_allow_background_location_title" = "アクセスを許可"; +"location_sharing_settings_header" = "位置情報の共有"; +"location_sharing_open_open_street_maps" = "OpenStreetMapで開く"; +"location_sharing_open_apple_maps" = "Appleマップで開く"; +"location_sharing_invalid_authorization_not_now" = "後で"; +"location_sharing_locating_user_error_title" = "%@は位置情報にアクセスできませんでした。後でもう一度やり直してください。"; +"location_sharing_post_failure_subtitle" = "%@は位置情報を送信できませんでした。後でもう一度やり直してください。"; +"location_sharing_post_failure_title" = "位置情報を送信できませんでした"; +"location_sharing_close_action" = "閉じる"; +"poll_timeline_ended_text" = "アンケートを終了しました"; +"poll_timeline_decryption_error" = "復号エラーにより、いくつかの投票はカウントできません"; +"poll_history_fetching_error" = "アンケートの取得中にエラーが発生しました。"; +"poll_history_no_past_poll_text" = "このルームに過去のアンケートはありません"; +"poll_history_no_active_poll_text" = "このルームに実施中のアンケートはありません"; +"poll_history_past_segment_title" = "過去のアンケート"; +"poll_history_active_segment_title" = "実施中のアンケート"; +"poll_history_loading_text" = "アンケートを表示しています"; + +// MARK: - Polls history + +"poll_history_title" = "アンケートの履歴"; +"space_detail_nav_title" = "スペースの詳細"; + +// MARK: - Room invites + +"room_invites_empty_view_title" = "新着はありません。"; +"all_chats_edit_menu_space_settings" = "スペースの設定"; +"all_chats_edit_menu_leave_space" = "%@から退出"; +"all_chats_user_menu_accessibility_label" = "ユーザーメニュー"; +"room_recents_recently_viewed_section" = "最近表示したルーム"; +"all_chats_empty_space_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。"; +"all_chats_edit_layout_sorting_options_title" = "メッセージを並び替える"; +"all_chats_edit_layout_add_filters_title" = "メッセージを絞り込む"; +"version_check_modal_action_title_supported" = "了解"; +"voice_broadcast_recorder_connection_error" = "接続エラー - 録音を停止しました"; +"voice_broadcast_connection_error_message" = "録音を開始できません。後でもう一度やり直してください。"; +"voice_broadcast_connection_error_title" = "接続エラー"; +"voice_broadcast_voip_cannot_start_description" = "ライブ配信を録音しているため、通話を開始できません。通話を開始するには、ライブ配信を終了してください。"; +"voice_broadcast_voip_cannot_start_title" = "通話を開始できません"; +"voice_broadcast_stop_alert_agree_button" = "はい、停止"; +"voice_broadcast_stop_alert_description" = "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。"; +"voice_broadcast_stop_alert_title" = "ライブ配信を停止しますか?"; +"voice_broadcast_buffering" = "バッファリングしています…"; +"voice_broadcast_time_left" = "残り%@"; +"voice_broadcast_tile" = "音声配信"; +"voice_broadcast_live" = "ライブ"; +"voice_broadcast_playback_lock_screen_placeholder" = "音声配信"; +"voice_broadcast_playback_loading_error" = "この音声配信を再生できません。"; +"voice_broadcast_already_in_progress_message" = "既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。"; +"voice_broadcast_blocked_by_someone_else_message" = "他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。"; +"voice_broadcast_permission_denied_message" = "このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。"; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "新しい音声配信を開始できません"; +"voice_message_broadcast_in_progress_message" = "ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください"; +"voice_message_broadcast_in_progress_title" = "音声メッセージを開始できません"; +"voice_message_lock_screen_placeholder" = "音声メッセージ"; +"voice_message_remaining_recording_time" = "残り%@"; + +// MARK: - Voice Messages + +"voice_message_release_to_send" = "押し続けて録音し、離すと送信"; +"side_menu_app_version" = "バージョン %@"; +"user_avatar_view_accessibility_hint" = "ユーザーのアバターを変更"; + +// MARK: - User avatar view + +"user_avatar_view_accessibility_label" = "アバター"; +"space_avatar_view_accessibility_hint" = "スペースのアバターを変更"; + +// MARK: Avatar + +"space_avatar_view_accessibility_label" = "アバター"; +"leave_space_selection_no_rooms" = "ルームを選択しない"; +"spaces_creation_post_process_creating_room" = "%@を作成しています"; +"spaces_creation_post_process_uploading_avatar" = "アバターをアップロードしています"; +"spaces_creation_invite_by_username_title" = "チームを招待"; +"spaces_creation_invite_by_username" = "ユーザー名で招待"; +"spaces_creation_sharing_type_title" = "誰と使いますか?"; +"spaces_creation_email_invites_email_title" = "電子メール"; +"spaces_creation_email_invites_title" = "チームを招待"; +"spaces_creation_new_rooms_support" = "サポート"; +"spaces_creation_new_rooms_random" = "ランダム"; +"spaces_creation_new_rooms_general" = "一般"; +"spaces_creation_new_rooms_room_name_title" = "ルーム名"; +"spaces_creation_private_space_title" = "あなたの非公開のスペース"; +"spaces_creation_public_space_title" = "あなたの公開スペース"; +"spaces_subspace_creation_visibility_title" = "作成するサブスペースの種類を選択してください"; + +// MARK: - Space Creation + +"spaces_creation_hint" = "スペースは、ルームや連絡先をまとめる新しい方法です。"; +"spaces_add_space" = "スペースを追加"; +"spaces_add_room" = "ルームを追加"; +"spaces_invite_people" = "連絡先を招待"; +"space_public_join_rule" = "公開スペース"; +"space_private_join_rule" = "非公開のスペース"; +"spaces_no_result_found_title" = "検索結果がありません"; +"space_tag" = "スペース"; +"spaces_explore_rooms_one_room" = "1個のルーム"; +"spaces_explore_rooms_room_number" = "%@個のルーム"; +"leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; +"leave_space_only_action" = "どのルームからも退出しない"; +"threads_discourage_information_2" = "\n\nスレッド機能を有効にしますか?"; +"room_no_privileges_to_create_group_call" = "通話を開始するには管理者あるいはモデレーターである必要があります。"; +"contacts_address_book_permission_denied_alert_message" = "連絡先を有効にするには、端末の設定画面を開いてください。"; +"contacts_address_book_permission_denied_alert_title" = "連絡先が無効です"; +"password_policy_weak_pwd_error" = "パスワードが弱すぎます。8文字以上で、大文字、小文字、数字、特殊文字をそれぞれ1つずつ含めてください。"; +"authentication_qr_login_loading_signed_in" = "他の端末でサインインしました。"; +"authentication_qr_login_display_step1" = "他の端末でElementを開いてください"; +"authentication_qr_login_start_display_qr" = "この端末でQRコードを表示"; +"authentication_qr_login_start_need_alternative" = "別の方法が必要ですか?"; +"authentication_qr_login_start_step1" = "他の端末でElementを開いてください"; +"authentication_qr_login_start_subtitle" = "この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください:"; +"authentication_choose_password_not_verified_title" = "電子メールは認証されていません"; +"authentication_server_selection_generic_error" = "このURLでサーバーを発見できません。URLを確認してください。"; +"authentication_server_selection_register_title" = "あなたのホームサーバーを選択してください"; +"authentication_server_selection_login_message" = "ホームサーバーのアドレスを入力してください"; +"authentication_server_selection_login_title" = "ホームサーバーに接続"; +"authentication_login_forgot_password" = "パスワードを忘れた場合"; +"event_formatter_call_answer" = "出る"; +"event_formatter_call_incoming_video" = "着信中のビデオ通話"; +"event_formatter_call_incoming_voice" = "着信中の音声通話"; +"event_formatter_call_has_ended_with_time" = "通話を終了しました・%@"; +"room_access_space_chooser_other_spaces_section" = "その他のスペースまたはルーム"; +"room_access_settings_screen_setting_room_access" = "ルームのアクセスの設定"; +"settings_labs_enable_wysiwyg_composer" = "リッチテキストエディターを試してみる"; +"settings_labs_enable_ringing_for_group_calls" = "グループ通話で呼び出す"; +"settings_notifications_disabled_alert_message" = "通知を有効にするには、端末の設定画面を開いてください。"; +"room_accessibility_record_voice_message_hint" = "2回続けてタップし長押しすると録音。"; +"room_preview_decline_invitation_options" = "招待を拒否するか、このユーザーを無視しますか?"; +"threads_beta_information" = "スレッド機能を使って、会話をまとめましょう。\n\nスレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 "; +"threads_notice_information" = "実験期間中に作成されたスレッドは通常の返信として表示されます

スレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。"; +"threads_empty_info_my" = "既存のスレッドに返信するか、メッセージをタップし「スレッド」から新しいスレッドを開始。"; +"room_accessibility_thread_more" = "その他"; +"room_first_message_placeholder" = "最初のメッセージを送信…"; + +// MARK: - Chat + +"room_slide_to_end_group_call" = "スライドすると全員の通話を終了"; +"authentication_qr_login_failure_request_timed_out" = "時間内にリンクが完了しませんでした。"; +"authentication_qr_login_failure_title" = "リンクに失敗しました"; +"authentication_qr_login_start_step2" = "設定から「セキュリティーとプライバシー」を開いてください"; +"authentication_qr_login_confirm_alert" = "このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに無制限にアクセスできるようになります。"; +"authentication_qr_login_scan_subtitle" = "QRコードを以下の四角形に合わせてください"; +"authentication_qr_login_display_step2" = "「QRコードでサインイン」を選択してください"; +"authentication_qr_login_start_step4" = "「この端末でQRコードを表示」を選択してください"; +"authentication_qr_login_start_step3" = "「端末をリンク」を選択してください"; +"authentication_qr_login_display_title" = "端末をリンク"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "他の人は %@ であなたを見つけることができます"; +"authentication_server_selection_register_message" = "あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます"; +"authentication_server_info_title_login" = "アカウントにサインインするサーバー"; +"authentication_server_info_title" = "アカウントを作成するサーバー"; +"onboarding_avatar_message" = "表示名にプロフィール画像を追加しましょう"; +"all_chats_edit_layout_add_filters_message" = "あなたが選択したカテゴリーにメッセージを自動的にフィルタリング"; +"all_chats_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。"; +"home_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。以下の+ボタンを押すと、連絡先とルームを追加できます。"; +"all_chats_empty_view_title" = "%@\nは空です。"; +"all_chats_nothing_found_placeholder_message" = "検索を調整してみてください。"; +"all_chats_nothing_found_placeholder_title" = "何も見つかりませんでした。"; +"all_chats_edit_layout_pin_spaces_title" = "スペースをピン止め"; +"all_chats_edit_layout_add_section_message" = "セクションをホームにピン止めすると簡単にアクセスできます"; +"all_chats_edit_layout_add_section_title" = "セクションをホームに追加"; +"version_check_banner_subtitle_deprecated" = "%@のサポートはiOS %@で終了しました。%@の使用を継続する場合は、iOSのバージョンをアップグレードしてください。"; +"version_check_banner_subtitle_supported" = "%@のサポートはiOS %@で近日中に終了します。%@の使用を継続する場合は、iOSのバージョンをアップグレードしてください。"; +"version_check_modal_action_title_deprecated" = "方法を確認"; +"version_check_modal_title_supported" = "iOS %@のサポートは近日中に終了します"; + +// MARK: - Version check + +"version_check_banner_title_supported" = "iOS %@のサポートは近日中に終了します"; +"version_check_banner_title_deprecated" = "iOS %@のサポートは終了しました"; +"version_check_modal_title_deprecated" = "iOS %@のサポートは終了しました"; +"attachment_size_prompt_title" = "送信するサイズを確認"; +"attachment_small_with_resolution" = "小:%@(~%@)"; +"attachment_medium_with_resolution" = "中:%@(~%@)"; +"attachment_large_with_resolution" = "大:%@(~%@)"; +"e2e_passphrase_too_short" = "パスフレーズが短すぎます(%d文字以上にしてください)"; +"notice_room_third_party_revoked_invite_for_dm" = "%@が%@の招待を取り消しました"; +"notice_room_third_party_revoked_invite_by_you_for_dm" = "%@の招待を取り消しました"; +"notice_room_name_changed_by_you_for_dm" = "名前を%@に変更しました。"; +"call_remote_holded" = "%@が通話を保留しました"; +"call_holded" = "通話を保留しました"; +"call_more_actions_unhold" = "再開"; +"user_session_rename_session_description" = "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。\n\nセッションの一覧から、相手はあなたとやり取りしていることを確かめることができます。なお、あなたがここに入力するセッション名は相手に対して表示されます。"; +"user_session_unverified_session_description" = "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。"; +"user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。"; +"user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。"; +"launch_loading_server_syncing" = "サーバーと同期しています"; +"launch_loading_processing_response" = "データを処理しています\n%@ %%"; +"wysiwyg_composer_format_action_link" = "リンクの装飾を適用"; +"wysiwyg_composer_format_action_inline_code" = "インラインコードの装飾を適用"; +"wysiwyg_composer_format_action_unordered_list" = "箇条書きリストの表示を切り替える"; +"wysiwyg_composer_format_action_ordered_list" = "番号付きリストの表示を切り替える"; +"wysiwyg_composer_format_action_code_block" = "コードブロックの表示を切り替える"; +"wysiwyg_composer_format_action_quote" = "引用の表示を切り替える"; +"poll_timeline_reply_ended_poll" = "終了したアンケート"; +"settings_labs_enable_crypto_sdk" = "Rust エンドツーエンド暗号化"; +"settings_labs_disable_crypto_sdk" = "Rust エンドツーエンド暗号化(無効にするにはログアウトしてください)"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "データを移行しています\n%@ %%"; +"poll_history_load_more" = "他のアンケートを読み込む"; +"key_backup_recover_from_private_key_progress" = "%@%%完了"; +"voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; +"home_context_menu_mark_as_unread" = "未読にする"; +"key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; +"key_backup_setup_passphrase_passphrase_invalid" = "語を追加してみる"; +"biometrics_cant_unlocked_alert_title" = "アプリのロックを解除できません"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; +"room_avatar_view_accessibility_hint" = "ルームのアバターを変更"; +"room_intro_cell_information_dm_sentence2" = "この会話はお二人だけで、他の人は参加できません。"; +"favourites_empty_view_information" = "お気に入り登録にはいくつかの方法がありますが、一番手っ取り早いのは、長押しすることです。星マークをタップすると、自動的にここに表示され、保管されます。"; +"room_intro_cell_information_multiple_dm_sentence2" = "誰かを招待しない限り、この会話に参加しているのはあなただけです。"; +"analytics_prompt_message_new_user" = "%@の改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。"; + +// Analytics +"analytics_prompt_title" = "%@の改善を手伝う"; +"event_formatter_call_active_video" = "実施中のビデオ通話"; +"event_formatter_call_active_voice" = "実施中の音声通話"; +"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)"; +"create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; +"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索し、参加できます。"; +"searchable_directory_x_network" = "%@ネットワーク"; +"pin_protection_explanatory" = "PINコードを設定すると、メッセージや連絡先などのデータを保護できます。アプリの開始時にPINコードを入力するよう要求します。"; +"secrets_recovery_with_key_information_default" = "セキュリティーキーを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; +"secrets_recovery_with_passphrase_information_default" = "セキュリティーフレーズを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; +"user_verification_session_details_verify_action_current_user" = "インタラクティブ認証"; +"share_extension_low_quality_video_message" = "%@をより高い品質で送信、あるいは、より低い品質で送信。"; +"room_access_space_chooser_other_spaces_section_info" = "これらは、%@の他の管理者がいるスペースまたはルームです。"; +"room_access_space_chooser_known_spaces_section" = "%@を含む、あなたが知っているスペース"; + +// MARK: - Side menu + +"side_menu_reveal_action_accessibility_label" = "左のパネル"; +"leave_space_selection_all_rooms" = "全てのルームを選択"; +"spaces_add_room_missing_permission_message" = "このスペースにルームを追加する権限がありません。"; +"spaces_creation_invite_by_username_message" = "後から招待することもできます。"; +"spaces_creation_email_invites_message" = "後から招待することもできます。"; +"spaces_creation_address_invalid_characters" = "%@\nには不正な文字があります"; +"spaces_creation_address_already_exists" = "%@\nは既に存在します"; +"spaces_creation_empty_room_name_error" = "名称が必要です"; +"space_settings_update_failed_message" = "スペースの設定の更新に失敗しました。再試行しますか?"; +"spaces_coming_soon_title" = "近日公開"; +"spaces_explore_rooms_format" = "%@を探す"; +"spaces_create_subspace_title" = "サブスペースを作成"; +"space_beta_announce_title" = "スペースは近日公開"; +"space_beta_announce_badge" = "ベータ版"; + +// MARK: - Spaces + +"space_feature_unavailable_title" = "スペースはまだありません"; +"room_invite_to_room_option_title" = "このルームのみ"; +"share_invite_link_room_text" = "こんにちは。%@ からこのルームに参加してください。"; +"share_invite_link_space_text" = "こんにちは。%@ からこのスペースに参加してください。"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "メッセージを保護"; +"secure_key_backup_setup_existing_backup_error_info" = "ロックを解除してセキュアバックアップで再利用するか、削除してセキュアバックアップでメッセージの新しいバックアップを作成。"; +"room_access_settings_screen_restricted_message" = "スペースを誰でも検索し、参加できるようにする。\n対象のスペースを確認してください。"; +"room_details_promote_room_title" = "ルームをプロモート"; +"settings_about" = "概要"; +"call_transfer_error_message" = "通話の転送に失敗しました"; +"call_transfer_contacts_all" = "全て"; +"call_transfer_contacts_recent" = "履歴"; + +// MARK: - Dial Pad +"dialpad_title" = "ダイヤルパッド"; +"create_room_type_restricted" = "スペースのメンバー"; +"biometrics_cant_unlocked_alert_message_login" = "再ログイン"; +"biometrics_cant_unlocked_alert_message_x" = "ロックを解除するには、%@を使用するか、再ログインして%@を有効にしてください"; +"biometrics_setup_subtitle" = "時間を節約"; +"biometrics_desetup_disable_button_title_x" = "%@を無効にする"; +"biometrics_desetup_title_x" = "%@を無効にする"; +"pin_protection_kick_user_alert_message" = "多数のエラーが発生したため、ログアウトしました"; +"pin_protection_not_allowed_pin" = "セキュリティー上の理由で、このPINコードは使用できません。他のPINコードを試してください"; +"pin_protection_settings_change_pin" = "PINコードを変更"; +"pin_protection_settings_enabled_forced" = "PINコードが有効です"; +"pin_protection_settings_section_footer" = "PINコードを再設定するには、再ログインして新しいコードを作成してください。"; +"pin_protection_mismatch_too_many_times_error_message" = "PINコードを覚えていない場合は「PINコードを忘れました」のボタンをタップしてください。"; +"pin_protection_mismatch_error_message" = "もう一度やり直してください"; +"pin_protection_mismatch_error_title" = "PINコードが一致しません"; +"pin_protection_reset_alert_message" = "PINコードを再設定するには、再ログインして新しいコードを作成してください"; +"pin_protection_reset_alert_title" = "PINコードを再設定"; +"pin_protection_forgot_pin" = "PINコードを忘れました"; +"pin_protection_enter_pin" = "PINコードを入力してください"; +"pin_protection_confirm_pin_to_change" = "PINコードを変更するには、PINコードを確認してください"; +"pin_protection_confirm_pin_to_disable" = "PINコードを無効にするには、PINコードを確認してください"; +"major_update_information" = "アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。"; + +// MARK: - Major update + +"major_update_title" = "Riotは%@になりました"; +"secrets_reset_authentication_message" = "承認するにはMatrixのアカウントのパスワードを入力してください"; +"secrets_setup_recovery_passphrase_summary_information" = "セキュリティーフレーズを記録してください。セキュリティーフレーズを使うと、暗号化したメッセージやデータのロックを解除することができます。"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ 印刷して安全な場所で保管\n✓ USBキーやバックアップ用ドライブに保存\n✓ 個人用のクラウドストレージにコピー"; +"secrets_setup_recovery_key_information" = "セキュリティーキーは安全な場所で保管してください。セキュリティーキーを使うと、暗号化したメッセージやデータのロックを解除することができます。"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "正しいセキュリティーキーを入力したことを確認してください。"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "続行するにはセキュリティーフレーズを入力してください。"; +"secrets_recovery_with_key_information_verify_device" = "セキュリティーキーを使用して、この端末を認証してください。"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "正しいセキュリティーフレーズを入力したことを確認してください。"; +"secrets_recovery_with_passphrase_recover_action" = "セキュリティーフレーズを使用"; +"secrets_recovery_with_passphrase_information_verify_device" = "セキュリティーフレーズを使用して、この端末を認証してください。"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "全ての復旧用の手段を忘れたか、無くしましたか? "; +"user_verification_session_details_additional_information_untrusted_current_user" = "このセッションにサインインしなかった場合、あなたのアカウントの安全性が損なわれている可能性があります。"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "他の認証方法を確認しています…"; +"device_verification_self_verify_wait_additional_information" = "これは%@と、クロス署名に対応した他のMatrixのクライアントで機能します。"; +"device_verification_self_verify_wait_information" = "暗号化されたメッセージにアクセスするには、あなたの他のセッションからこのセッションを認証する必要があります。\n\n他の端末で最新の%@を使用してください:"; +"key_verification_self_verify_current_session_alert_message" = "他のユーザーは信頼しないかもしれません。"; +"device_verification_start_use_legacy" = "何も表示されませんか?まだ全てのクライアントはインタラクティブな認証をサポートしていません。レガシー認証を使用してください。"; +"device_verification_start_wait_partner" = "相手の承諾を待機しています…"; +"key_verification_user_title" = "認証"; +"key_verification_new_session_title" = "新しいセッションを認証"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "待機します"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "暗号化されたメッセージを失います"; +"secure_key_backup_setup_existing_backup_error_title" = "メッセージのバックアップは既に存在します"; +"service_terms_modal_policy_checkbox_accessibility_hint" = "チェックして%@を承諾してください"; +"service_terms_modal_information_description_integration_manager" = "インテグレーションマネージャーを使うと、第三者による機能を追加することができます。"; +"service_terms_modal_information_description_identity_server" = "IDサーバーを使うと、電話番号やメールアドレスを検索して、連絡先が既にアカウントをもっているかどうか確認することができます。"; +"service_terms_modal_description_integration_manager" = "ボット、ブリッジ、ウィジェット、ステッカーパックの使用を許可します。"; +"share_extension_low_quality_video_title" = "動画を低品質で送信"; +"analytics_prompt_yes" = "はい、大丈夫です"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "規約を%@で確認してください。よろしいですか?"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "規約は%@で確認できます。"; +"analytics_prompt_message_upgrade" = "あなたは以前、利用状況に関する匿名データの共有に同意しました。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。"; +"spaces_creation_in_one_space" = "1個のスペースに"; +"spaces_creation_in_many_spaces" = "%@個のスペースに"; +"spaces_creation_in_spacename_plus_many" = "%@と%@個のスペースに"; +"spaces_creation_in_spacename_plus_one" = "%@と1個のスペースに"; +"spaces_creation_in_spacename" = "%@に"; +"event_formatter_group_call_incoming" = "%@(%@にて)"; + +// MARK: Reactions + +"room_event_action_reaction_more" = "他%@件"; +"notice_event_redacted_by_you" = " あなたにより"; +"room_displayname_all_other_members_left" = "%@(退出済)"; +"user_session_item_details_last_activity" = "直近のオンライン日時 %@"; +"version_check_modal_subtitle_deprecated" = "私たちは%@の高速化と改善に取り組んできました。残念ながら現在のiOSのバージョンはそれらの修正に対応していないため、サポートを終了しました。\nオペレーティングシステムをアップデートして、%@を最大限に活用しましょう。"; +"version_check_modal_subtitle_supported" = "私たちは%@の高速化と改善に取り組んできました。残念ながら現在のiOSのバージョンはそれらの修正に対応していないため、近日中にサポート外となります。\nオペレーティングシステムをアップデートして、%@を最大限に活用しましょう。"; +"key_verification_verified_this_session_information" = "保護されたメッセージをこの端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; +"key_verification_verified_new_session_information" = "保護されたメッセージを新しい端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; +"key_verification_verified_other_session_information" = "保護されたメッセージを他のセッションで読むことができます。また、他のユーザーもこのセッションを信頼することができます。"; +"call_consulting_with_user" = "%@と相談しています"; +"room_displayname_more_than_two_members" = "%@とその他%@人"; +"notice_error_unformattable_event" = "** メッセージを描画できません。不具合を報告してください"; +"wysiwyg_composer_format_action_un_indent" = "インデントを減らす"; +"wysiwyg_composer_format_action_indent" = "インデントを増やす"; +"wysiwyg_composer_format_action_strikethrough" = "下線で装飾"; +"wysiwyg_composer_format_action_underline" = "打ち消し線で装飾"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "フォトライブラリー"; +"user_session_details_device_ip_location" = "IP位置情報"; +"user_session_details_session_section_footer" = "タップして押し続けるとデータをコピーします。"; +"device_name_mobile" = "%@モバイル"; +"device_name_web" = "%@ウェブ"; +// First item is client name and second item is session display name +"user_session_name" = "%@:%@"; +"user_session_verification_unknown_additional_info" = "現在のセッションを認証すると、このセッションの認証の状態を確認できます。"; +"user_sessions_overview_link_device" = "端末をリンク"; +"location_sharing_live_timer_incoming" = "%@まで共有(ライブ)"; +"location_sharing_live_list_item_last_update_invalid" = "最後の更新は不明です"; +"location_sharing_live_list_item_last_update" = "%@前に更新済"; +"location_sharing_live_list_item_sharing_expired" = "共有の期限が切れました"; +"location_sharing_map_credits_title" = "© Copyright"; +"location_sharing_allow_background_location_message" = "位置情報(ライブ)を共有したい場合、Elementはバックグラウンドで位置情報にアクセスできる必要があります。アクセスを許可するには、「設定」の「位置情報」にある「常に」をタップしてください。"; +"location_sharing_invalid_authorization_error_title" = "%@には位置情報にアクセスする権限がありません。「設定」の「位置情報」からアクセスを有効にできます。"; +"location_sharing_loading_map_error_title" = "%@は地図を読み込めませんでした。後でもう一度やり直してください。"; +"poll_history_no_past_poll_period_text" = "過去%@日に実施されたアンケートはありません。さらにアンケートを読み込み、前の月のアンケートを表示"; +"poll_history_no_active_poll_period_text" = "過去%@日に実施中のアンケートはありません。さらにアンケートを読み込み、前の月のアンケートを表示"; +"poll_history_detail_view_in_timeline" = "アンケートをタイムラインに表示"; +"space_invite_nav_title" = "スペースに招待"; + +// MARK: - Space Selector + +"space_selector_title" = "スペース"; +"room_invites_empty_view_information" = "ここに招待が表示されます。"; +"voice_message_stop_locked_mode_recording" = "録音をタップして停止または再生"; +"leave_space_and_more_rooms" = "スペースと%@個のルームから退出"; +"leave_space_and_one_room" = "スペースと1個のルームから退出"; +"spaces_creation_post_process_inviting_users" = "%@人のユーザーを招待しています"; +"spaces_creation_post_process_adding_rooms" = "%@個のルームを追加しています"; +"spaces_creation_new_rooms_message" = "それぞれにルームを作ります。"; +"spaces_creation_new_rooms_title" = "どのような議論を行いますか?"; +"spaces_subspace_creation_visibility_message" = "作成したスペースは%@に追加されます。"; +"spaces_feature_not_available" = "この機能はまだ利用できません。当面は、コンピューターで%@によりこれを行うことができます。"; +"spaces_no_member_found_detail" = "%@のメンバー以外の人を探していますか?当面は、ウェブ版またはデスクトップ版で招待できます。"; +"spaces_coming_soon_detail" = "この機能はまだ実装されていません。当面は、コンピューターで%@によりこれを行うことができます。"; +"spaces_invites_coming_soon_title" = "招待は近日公開"; +"spaces_add_rooms_coming_soon_title" = "ルームの追加は近日公開"; +"spaces_no_room_found_detail" = "非公開で招待が必要なものは表示されていません。"; +"leave_space_message_admin_warning" = "あなたはこのスペースの管理者です。退出する前に、管理者の権限を別のメンバーに移譲してください。"; +"leave_space_message" = "%@から退出してよろしいですか?このスペースの全てのルームとスペースからも退出しますか?"; +"spaces_add_subspace_title" = "%@内にスペースを作成"; +"space_feature_unavailable_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。\n\n近日公開予定です。別のプラットフォームでスペースに参加すると、ここで参加するどのルームにもアクセスすることができます。"; +"space_beta_announce_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。iOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます。"; +"space_feature_unavailable_subtitle" = "スペースはiOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます"; +"space_beta_announce_subtitle" = "コミュニティー機能の新しいバージョン"; +"space_invite_not_enough_permission" = "このスペースにユーザーを招待する権限がありません"; +"room_invite_not_enough_permission" = "このルームにユーザーを招待する権限がありません"; +"room_invite_to_room_option_detail" = "%@のメンバーにはなりません。"; +"room_invite_to_space_option_detail" = "%@を探すことはできますが、%@のメンバーにはなりません。"; + +// MARK: - Room invite + +"room_invite_to_space_option_title" = "%@に"; +"event_formatter_call_missed_video" = "不在着信(ビデオ)"; +"event_formatter_call_missed_voice" = "不在着信(音声)"; +"settings_push_rules_error" = "通知の設定をアップデートする際にエラーが発生しました。もう一度オプションを切り替えてみてください。"; +"settings_presence" = "プレゼンス(ステータス表示)"; +"authentication_qr_login_failure_device_not_supported" = "この端末とのリンクはサポートしていません。"; diff --git a/Riot/Assets/lv.lproj/Localizable.strings b/Riot/Assets/lv.lproj/Localizable.strings index d1f7880f6..abd6219ad 100644 --- a/Riot/Assets/lv.lproj/Localizable.strings +++ b/Riot/Assets/lv.lproj/Localizable.strings @@ -44,3 +44,6 @@ "VOICE_CONF_NAMED_FROM_USER" = "Grupas zvans no %@: '%@'"; /* Incoming named video conference invite from a specific person */ "VIDEO_CONF_NAMED_FROM_USER" = "Grupas video zvans no %@: '%@'"; +/** General **/ + +"Notification" = "Paziņojums"; diff --git a/Riot/Assets/new_features.html b/Riot/Assets/new_features.html index aaf1b3d8e..3a1c330dd 100644 --- a/Riot/Assets/new_features.html +++ b/Riot/Assets/new_features.html @@ -26,6 +26,61 @@ +
+

+ Version 2.5.0 +

+ +

+ Neue Funktionen +

+

+ +

+ Verbesserungen +

+

+ +

+ Behobene Bugs +

+

+
+
+

+ Version 2.4.0 +

+ +

+ Neue Funktionen +

+

+ +

+ Verbesserungen +

+

+ +

+ Behobene Bugs +

+

+

Version 2.3.0 diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index ab294231c..a814c281b 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -2605,14 +2605,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Niets nieuws."; -"all_chats_onboarding_try_it" = "Probeer het uit"; -"all_chats_onboarding_title" = "Wat is nieuw"; -"all_chats_onboarding_page_message3" = "Tik op je profiel om ons te laten weten wat je ervan vindt."; -"all_chats_onboarding_page_title3" = "Geef feedback"; -"all_chats_onboarding_page_message2" = "Krijg sneller en gemakkelijker toegang tot je Spaces (linksonder) dan ooit tevoren."; -"all_chats_onboarding_page_title2" = "Toegang tot spaces"; -"all_chats_onboarding_page_message1" = "Om je Element te vereenvoudigen, zijn tabbladen nu optioneel. Beheer ze met behulp van het menu rechtsboven."; -"all_chats_onboarding_page_title1" = "Welkom bij de nieuwe weergave!"; "all_chats_edit_menu_space_settings" = "Space instellingen"; "all_chats_edit_menu_leave_space" = "Verlaat %@"; "all_chats_user_menu_settings" = "Gebruikersinstellingen"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 8532c4b4d..9106d3eea 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -2533,13 +2533,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nic nowego."; -"all_chats_onboarding_try_it" = "Wypróbuj"; -"all_chats_onboarding_title" = "Co nowego"; -"all_chats_onboarding_page_message3" = "Dotknij swojego profilu by poinformować nas, co o tym sądzisz."; -"all_chats_onboarding_page_title3" = "Prześlij opinię"; -"all_chats_onboarding_page_message2" = "Uzyskaj dostęp do twoich przestrzeni (lewy dolny róg) szybciej i prościej niż kiedykolwiek."; -"all_chats_onboarding_page_message1" = "Aby uprościć korzystanie z Element, karty są teraz opcjonalne. Możesz nimi zarządzać w menu w prawym górnym rogu."; -"all_chats_onboarding_page_title1" = "Witaj w nowym widoku!"; "all_chats_edit_menu_space_settings" = "Ustawienia przestrzeni"; "all_chats_edit_menu_leave_space" = "Opuść %@"; "all_chats_user_menu_settings" = "Ustawienia użytkownika"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 582951c39..0d9dbe5d9 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2446,14 +2446,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nada novo."; -"all_chats_onboarding_try_it" = "Experimentar"; -"all_chats_onboarding_title" = "O que tem de novo"; -"all_chats_onboarding_page_message3" = "Toque em seu perfil para nos deixar sabendo do que você acha."; -"all_chats_onboarding_page_title3" = "Dê Feedback"; -"all_chats_onboarding_page_message2" = "Acesse seus Espaços (esquerda fundo) mais rápido e fácil que jamais antes."; -"all_chats_onboarding_page_title2" = "Acesse Espaços"; -"all_chats_onboarding_page_message1" = "Para simplificar seu Element, abas são agora opcionais. Gerencie-as usando o menu direito topo."; -"all_chats_onboarding_page_title1" = "Boas vindas a uma nova visão!"; "all_chats_nothing_found_placeholder_message" = "Tente ajustar sua pesquisa."; "all_chats_nothing_found_placeholder_title" = "Nada encontrado."; "all_chats_empty_unreads_placeholder_message" = "Isto é onde suas mensagens não-lidas vão aparecer, quando você tiver algumas."; diff --git a/Riot/Assets/sk.lproj/Localizable.strings b/Riot/Assets/sk.lproj/Localizable.strings index 612c4f691..02c086065 100644 --- a/Riot/Assets/sk.lproj/Localizable.strings +++ b/Riot/Assets/sk.lproj/Localizable.strings @@ -168,3 +168,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ zdieľal/a svoju polohu"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ začal/a hlasové vysielanie"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 87edea36c..49be11fc5 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2668,14 +2668,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nič nové."; -"all_chats_onboarding_try_it" = "Vyskúšajte si to"; -"all_chats_onboarding_title" = "Čo je nové"; -"all_chats_onboarding_page_message3" = "Ťuknite na svoj profil a dajte nám vedieť, čo si myslíte."; -"all_chats_onboarding_page_title3" = "Poskytnite spätnú väzbu"; -"all_chats_onboarding_page_title2" = "Prístup k priestorom"; -"all_chats_onboarding_page_message2" = "Získajte prístup k svojim priestorom (vľavo dole) rýchlejšie a jednoduchšie ako kedykoľvek predtým."; -"all_chats_onboarding_page_message1" = "Pre zjednodušenie vašej aplikácie Element, sú teraz karty voliteľné. Spravujte ich pomocou ponuky vpravo hore."; -"all_chats_onboarding_page_title1" = "Vitajte v novom zobrazení!"; "all_chats_nothing_found_placeholder_message" = "Skúste upraviť svoje hľadanie."; "all_chats_nothing_found_placeholder_title" = "Nič sa nenašlo."; "all_chats_empty_unreads_placeholder_message" = "Tu sa zobrazia neprečítané správy, ak nejaké máte."; @@ -2891,3 +2883,45 @@ "poll_timeline_decryption_error" = "Z dôvodu chýb v dešifrovaní sa niektoré hlasy nemusia započítať"; "voice_message_broadcast_in_progress_message" = "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu"; "voice_message_broadcast_in_progress_title" = "Nemožno spustiť hlasovú správu"; +"poll_timeline_ended_text" = "Ukončil anketu"; +"voice_broadcast_voip_cannot_start_description" = "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor."; +"voice_broadcast_voip_cannot_start_title" = "Nie je možné začať hovor"; +"poll_history_no_past_poll_text" = "V tejto miestnosti nie sú žiadne predchádzajúce ankety"; +"poll_history_no_active_poll_text" = "V tejto miestnosti nie sú žiadne aktívne ankety"; +"poll_history_past_segment_title" = "Predchádzajúce ankety"; +"poll_history_active_segment_title" = "Aktívne ankety"; + +// MARK: - Polls history + +"poll_history_title" = "História ankety"; +"room_details_polls" = "História ankety"; +"accessibility_selected" = "vybrané"; +"voice_broadcast_connection_error_message" = "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr."; +"voice_broadcast_connection_error_title" = "Chyba pripojenia"; +"voice_broadcast_playback_lock_screen_placeholder" = "Hlasové vysielanie"; +"wysiwyg_composer_format_action_quote" = "Prepínanie citácie"; +"wysiwyg_composer_format_action_code_block" = "Prepnutie bloku kódu"; +"wysiwyg_composer_format_action_ordered_list" = "Prepínanie číslovaného zoznamu"; +"wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami"; +"voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené"; +"poll_timeline_reply_ended_poll" = "Ukončená anketa"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end šifrovanie (odhláste sa, aby ste ho vypli)"; +"settings_labs_confirm_crypto_sdk" = "Upozorňujeme, že táto funkcia je stále v experimentálnej fáze, preto nemusí fungovať podľa očakávaní a môže mať potenciálne nezamýšľané dôsledky. Ak chcete funkciu vrátiť späť, jednoducho sa odhláste a znova prihláste. Používajte ju podľa vlastného uváženia a s opatrnosťou."; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end šifrovanie"; +"poll_history_load_more" = "Načítať ďalšie ankety"; +"poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; +"poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; +"poll_history_loading_text" = "Zobrazenie ankiet"; +"poll_history_fetching_error" = "Chyba pri načítavaní ankiet."; +"voice_broadcast_playback_unable_to_decrypt" = "Toto hlasové vysielanie sa nedá dešifrovať."; +"home_context_menu_mark_as_unread" = "Označiť ako neprečítané"; +"key_backup_recover_from_private_key_progress" = "%@%% Dokončené"; +"wysiwyg_composer_format_action_indent" = "Zväčšenie odsadenia"; +"wysiwyg_composer_format_action_un_indent" = "Zmenšenie odsadenia"; +"poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi"; +"settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova."; +"authentication_qr_login_failure_device_not_supported" = "Prepojenie s týmto zariadením nie je podporované."; diff --git a/Riot/Assets/sq.lproj/Localizable.strings b/Riot/Assets/sq.lproj/Localizable.strings index a49dd9660..8036083e0 100644 --- a/Riot/Assets/sq.lproj/Localizable.strings +++ b/Riot/Assets/sq.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ tregoi vendndodhjen e vet"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ nisi një transmetim zanor"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index f484f7275..0bf63c8d7 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2417,7 +2417,6 @@ // MARK: Authentication "authentication_registration_title" = "Krijoni llogarinë tuaj"; -"all_chats_onboarding_page_message3" = "Prekni profilin tuaj që të na bëni të ditur se ç’mendoni."; "all_chats_edit_layout_add_section_message" = "Fiksoni ndarje te kreu, për hyrje të lehtë në ta"; "room_event_encryption_info_key_authenticity_not_guaranteed" = "S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje."; "deselect_all" = "Shpërzgjidhi Krejt"; @@ -2534,13 +2533,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "S’ka gjë të re."; -"all_chats_onboarding_try_it" = "Provojeni"; -"all_chats_onboarding_title" = "Ç’ka të re"; -"all_chats_onboarding_page_title3" = "Jepni Përshtypje"; -"all_chats_onboarding_page_message2" = "Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë."; -"all_chats_onboarding_page_title2" = "Hyni Në Hapësira"; -"all_chats_onboarding_page_message1" = "Që të thjeshtohet Element-i juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye."; -"all_chats_onboarding_page_title1" = "Mirë se vini te një pamje e re!"; "all_chats_edit_menu_space_settings" = "Rregullime hapësire"; "all_chats_edit_menu_leave_space" = "Braktise %@"; "all_chats_user_menu_settings" = "Rregullime përdoruesi"; @@ -2676,3 +2668,47 @@ "notice_voice_broadcast_ended" = "%@ përfundoi një transmetim zanor."; "notice_voice_broadcast_live" = "Transmetim i drejtëpërdrejtë"; "user_other_session_security_recommendation_title" = "Sesione të tjerë"; +"poll_timeline_ended_text" = "Përfundoi pyetësori"; +"poll_timeline_decryption_error" = "Për shkak gabimesh shfshehtëzimi, mund të mos jenë numëruar disa vota"; +"poll_history_no_past_poll_text" = "Në këtë dhomë s’ka pyetësorë të dikurshëm"; +"poll_history_no_active_poll_text" = "Në këtë dhomë s’ka pyetësorë aktivë"; +"poll_history_past_segment_title" = "Pyetësorë të dikurshëm"; +"poll_history_active_segment_title" = "Pyetësorë aktivë"; +"voice_broadcast_playback_lock_screen_placeholder" = "Transmetim zanor"; +"voice_broadcast_voip_cannot_start_description" = "S’mund të niset thirrje, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni një thirrje."; +"voice_broadcast_voip_cannot_start_title" = "S’niset dot një thirrje"; +"voice_message_broadcast_in_progress_message" = "S’mund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor"; +"voice_message_broadcast_in_progress_title" = "S’niset dot mesazh zanor"; +"wysiwyg_composer_format_action_quote" = "Shfaq/fshih citim"; +"wysiwyg_composer_format_action_code_block" = "Shfaq/fshih bllok kodi"; +"wysiwyg_composer_format_action_ordered_list" = "Shfaq/fshih listë të numërtuar"; +"wysiwyg_composer_format_action_unordered_list" = "Shfaq/fshih listë me toptha"; +"poll_timeline_reply_ended_poll" = "Pyetësor i përfunduar"; +"poll_history_fetching_error" = "Gabim në sjelle pyetësorë."; +"poll_history_load_more" = "Ngarko më tepër pyetësorë"; +"poll_history_no_past_poll_period_text" = "S’ka pyetësorë të kaluar për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë"; +"poll_history_no_active_poll_period_text" = "S’ka pyetësorë aktivë për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë"; +"poll_history_loading_text" = "Shfaqje pyetësorësh"; + +// MARK: - Polls history + +"poll_history_title" = "Historik pyetësorësh"; +"voice_broadcast_playback_unable_to_decrypt" = "S’arrihet të shfshehtëzohet ky transmetim zanor."; +"voice_broadcast_recorder_connection_error" = "Gabim lidhjeje - Incizimi u ndal"; +"voice_broadcast_connection_error_message" = "Mjerisht, s’jemi në gjendje të nisim një incizim mu tani. Ju lutemi, riprovoni më vonë."; +"voice_broadcast_connection_error_title" = "Gabim lidhjeje"; +"home_context_menu_mark_as_unread" = "Vëri shenjë si i palexuar"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%"; +"key_backup_recover_from_private_key_progress" = "Plotësuar %@%%"; +"room_details_polls" = "Historik pyetësorësh"; +"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust (që ta çaktivizoni, dilni)"; +"settings_labs_confirm_crypto_sdk" = "Ju lutemi, kini parasysh se kjo veçori është ende në fazë eksperimentale, mund të mos funksionojë siç pritet dhe mundet, në potencial, të ketë pasojë të paparashikuara. Që ta prapaktheni këtë veçori, thjesht dilni nga llogaria dhe rihyni. Përdoreni me përgjegjësinë tuaj dhe me kujdes."; +"settings_labs_enable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust"; +"settings_push_rules_error" = "Ndodhi një gabim, kur përditësoheshin parapëlqimet tuaja për njoftime. JU lutemi, provoni të aktivizoni mundësi tuaj sërish."; +"wysiwyg_composer_format_action_un_indent" = "Zvogëlo shmangie kryeradhë"; +"wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe"; +"poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore"; +"authentication_qr_login_failure_device_not_supported" = "Nuk mbulohet lidhja me këtë pajisje."; diff --git a/Riot/Assets/sv.lproj/Localizable.strings b/Riot/Assets/sv.lproj/Localizable.strings index 5b3e918dd..fc7c18b9e 100644 --- a/Riot/Assets/sv.lproj/Localizable.strings +++ b/Riot/Assets/sv.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ delade sin plats"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ påbörjade en röstsändning"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 53b8d822f..343877c48 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2310,7 +2310,7 @@ "authentication_terms_policy_url_error" = "Kan inte hitta den valda policyn. Vänligen pröva igen senare."; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "Vänligen läs villkor och policyer för %@"; -"authentication_terms_title" = "Serverpolicyer"; +"authentication_terms_title" = "Sekretesspolicyer"; "authentication_verify_msisdn_invalid_phone_number" = "Ogiltigt telefonnummer"; "authentication_verify_msisdn_waiting_button" = "Skicka kod igen"; /* The placeholder will show the phone number that was entered. */ @@ -2363,3 +2363,309 @@ // MARK: Authentication "authentication_registration_title" = "Skapa ditt konto"; +"voice_broadcast_time_left" = "%@ kvar"; +"all_chats_empty_list_placeholder_title" = "Du är ikapp."; +"all_chats_empty_view_information" = "Den säkra allt-i-ett-chattappen för lag, vänner och organisationer. Skapa en chatt, eller gå med i ett existerande rum, för att komma igång."; +"all_chats_empty_space_information" = "Utrymmen är ett nytt sätt att gruppera rum och personer. Lägg till ett existerande rum, eller skapa ett nytt, med knappen nere till höger."; +"all_chats_empty_view_title" = "%@\nser lite tom ut."; +"all_chats_all_filter" = "Alla"; +"all_chats_edit_layout_alphabetical_order" = "Sortera A-Ö"; +"all_chats_edit_layout_activity_order" = "Sortera efter aktivitet"; +"all_chats_edit_layout_show_filters" = "Visa filter"; +"all_chats_edit_layout_show_recents" = "Visa nyliga"; +"all_chats_edit_layout_sorting_options_title" = "Sortera meddelanden efter"; +"all_chats_edit_layout_pin_spaces_title" = "Fäst dina utrymmen"; +"all_chats_edit_layout_add_filters_message" = "Filtrera automatiskt dina meddelanden i valfria kategorier"; +"all_chats_edit_layout_add_filters_title" = "Filtrera dina meddelanden"; +"all_chats_edit_layout_add_section_message" = "Fäst sektioner till hem för enkel åtkomst"; +"all_chats_edit_layout_add_section_title" = "Lägg till sektion i hem"; +"all_chats_edit_layout_unreads" = "Olästa"; +"all_chats_edit_layout_recents" = "Nyliga"; +"all_chats_edit_layout" = "Layoutalternativ"; +"all_chats_section_title" = "Chattar"; + +// MARK: - All Chats + +"all_chats_title" = "Alla chattar"; +"voice_broadcast_voip_cannot_start_description" = "Du kan inte starta ett samtal eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta ett samtal."; +"voice_broadcast_voip_cannot_start_title" = "Kan inte starta ett samtal"; +"voice_broadcast_stop_alert_agree_button" = "Ja, avsluta"; +"voice_broadcast_stop_alert_description" = "Är du säker på att du vill avsluta din direktsändning? Det här kommer att avsluta sändningen, och den fulla inspelningen kommer att bli tillgänglig i rummet."; +"voice_broadcast_stop_alert_title" = "Avsluta direktsändning?"; +"voice_broadcast_buffering" = "Buffrar…"; +"voice_broadcast_tile" = "Röstsändning"; +"voice_broadcast_live" = "Live"; +"voice_broadcast_playback_loading_error" = "Kunde inte spela den här röstsändningen."; +"voice_broadcast_already_in_progress_message" = "Du spelar redan in en röstsändning. Vänligen avsluta din nuvarande röstsändning för att starta en ny."; +"voice_broadcast_blocked_by_someone_else_message" = "Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning avslutas för att starta en ny."; +"voice_broadcast_permission_denied_message" = "Du har inte behörigheten som krävs för att starta en röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera din behörighet."; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "Du kan inte starta en ny röstsändning"; +"voice_message_broadcast_in_progress_message" = "Du kan inte starta ett röstmeddelande eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att börja spela in ett röstmeddelande"; +"voice_message_broadcast_in_progress_title" = "Kan inte starta röstmeddelande"; +"spaces_subspace_creation_visibility_message" = "Det skapade utrymmet kommer att läggas till i %@."; +"spaces_subspace_creation_visibility_title" = "Vad för sorts utrymme vill du skapa?"; +"spaces_explore_rooms_format" = "Utforska %@"; +"spaces_create_subspace_title" = "Skapa ett underutrymme"; +"spaces_add_subspace_title" = "Skapa utrymme inuti %@"; +"launch_loading_processing_response" = "Hanterar data\n%@ %%"; +"launch_loading_server_syncing_nth_attempt" = "Synkar med servern\n(%@ försök)"; + +// MARK: - Launch loading + +"launch_loading_server_syncing" = "Synkar med servern"; +"key_verification_alert_body" = "Granska för att försäkra att ditt konto är säkert."; + +// Unverified sessions +"key_verification_alert_title" = "Du har overifierade sessioner"; +"sign_out_confirmation_message" = "Är du säker på att du vill logga ut?"; + +// MARK: Sign out warning + +"sign_out" = "Logga ut"; +// User sessions management +"user_sessions_settings" = "Hantera sessioner"; +"manage_session_sign_out_other_sessions" = "Logga ut ur alla andra sessioner"; +"manage_session_rename" = "Döp om session"; +"manage_session_name_info_link" = "Läs mer"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Observera att sessionsnamn också är synliga för personer du pratar med. %@"; +"manage_session_name_hint" = "Anpassade sessionsnamn kan hjälpa dig att känna igen dina enheter lättare."; +"settings_labs_enable_voice_broadcast" = "Röstsändning"; +"settings_labs_enable_wysiwyg_composer" = "Pröva den nya riktextredigeraren"; +"settings_labs_enable_new_app_layout" = "Ny applikationslayout"; +"settings_labs_enable_new_client_info_feature" = "Spara klientens namn, version och URL för att lättare känna igen sessioner i sessionshanteraren"; +"settings_labs_enable_new_session_manager" = "My sessionshanterare"; +"room_first_message_placeholder" = "Skicka ditt första meddelande…"; +"password_policy_pwd_in_dict_error" = "Det här lösenordet har hittats i en ordlista, och tillåts inte."; +"password_policy_weak_pwd_error" = "Det här lösenordet är för svagt. Det måste innehålla minst 8 tecken, och minst ett tecken av varje typ: stor bokstav, liten bokstav, siffra och specialtecken."; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "För kort lösenord"; +"authentication_qr_login_failure_retry" = "Pröva igen"; +"authentication_qr_login_failure_request_timed_out" = "Länkningen slutfördes inte inom den krävda tiden."; +"authentication_qr_login_failure_request_denied" = "Förfrågan nekades på en andra enheten."; +"authentication_qr_login_failure_invalid_qr" = "QR-kod är ogiltig."; +"authentication_qr_login_failure_title" = "Länkning misslyckades"; +"authentication_qr_login_loading_signed_in" = "Du är nu inloggad på din andra enhet."; +"authentication_qr_login_loading_waiting_signin" = "Väntar på att enheten loggar in."; +"authentication_qr_login_loading_connecting_device" = "Ansluter till enhet"; +"authentication_qr_login_confirm_alert" = "Vänligen försäkra att du känner till källan till den här koden. Genom att länka enheter så ger du någon full åtkomst till ditt konto."; +"authentication_qr_login_confirm_subtitle" = "Bekräfta att koden nedan matchar den andra enheten:"; +"authentication_qr_login_confirm_title" = "Säker kommunikation etablerad"; +"authentication_qr_login_scan_subtitle" = "Placera QR-koden i rutan nedan"; +"authentication_qr_login_scan_title" = "Skanna QR-kod"; +"authentication_qr_login_display_step2" = "Välj 'Logga in med QR-kod'"; +"authentication_qr_login_display_step1" = "Öppna Element på din andra enhet"; +"authentication_qr_login_display_subtitle" = "Skanna QR-koden nedan med din enhet som är utloggad."; +"authentication_qr_login_display_title" = "Länka en enhet"; +"authentication_qr_login_start_display_qr" = "Visa QR-kod på den här enheten"; +"authentication_qr_login_start_need_alternative" = "Behöver du en alternativ metod?"; +"authentication_qr_login_start_step4" = "Välj 'Visa QR-kod på den här enheten'"; +"authentication_qr_login_start_step3" = "Välj 'Länka en enhet'"; +"authentication_qr_login_start_step2" = "Gå till Inställningar -> Säkerhet & sekretess"; +"authentication_qr_login_start_step1" = "Öppna Element på den andra enheten"; +"authentication_qr_login_start_subtitle" = "Använd kameran på den här enheten för att skanna QR-koden som visas på den andra enheten:"; +"authentication_qr_login_start_title" = "Skanna QR-kod"; +"authentication_choose_password_not_verified_message" = "Kolla din inkorg"; +"authentication_choose_password_not_verified_title" = "E-post inte verifierad"; +"authentication_login_with_qr" = "Logga in med QR-kod"; +"invite_to" = "Bjud in till %@"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "Äktheten för det här krypterade meddelandet kan inte garanteras på den här enheten."; +"notice_voice_broadcast_ended_by_you" = "Du avslutade en röstsändning."; +"notice_voice_broadcast_ended" = "%@ avslutade en röstsändning."; +"notice_voice_broadcast_live" = "Direktsändning"; +"deselect_all" = "Välj bort alla"; +"wysiwyg_composer_link_action_edit_title" = "Redigera länk"; +"wysiwyg_composer_link_action_create_title" = "Skapa en länk"; +"wysiwyg_composer_link_action_link" = "Länk"; + + + +// Links +"wysiwyg_composer_link_action_text" = "Text"; +"wysiwyg_composer_format_action_quote" = "Växla citat"; +"wysiwyg_composer_format_action_code_block" = "Växla kodblock"; +"wysiwyg_composer_format_action_ordered_list" = "Växla numrerad lista"; +"wysiwyg_composer_format_action_unordered_list" = "Växla punktlista"; +"wysiwyg_composer_format_action_inline_code" = "Tillämpa inline-kodstil"; +"wysiwyg_composer_format_action_link" = "Tillämpa länkformat"; +"wysiwyg_composer_format_action_strikethrough" = "Tillämpa understruken stil"; +"wysiwyg_composer_format_action_underline" = "Tillämpa genomstruken stil"; +"wysiwyg_composer_format_action_italic" = "Tillämpa kursiv stil"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Tillämpa fetstil"; +"wysiwyg_composer_start_action_voice_broadcast" = "Röstsändning"; +"wysiwyg_composer_start_action_text_formatting" = "Textformatering"; +"wysiwyg_composer_start_action_camera" = "Kamera"; +"wysiwyg_composer_start_action_location" = "Plats"; +"wysiwyg_composer_start_action_polls" = "Omröstningar"; +"wysiwyg_composer_start_action_attachments" = "Bilagor"; +"wysiwyg_composer_start_action_stickers" = "Dekaler"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Fotobibliotek"; +"user_session_overview_session_details_button_title" = "Sessionsdetaljer"; +"user_session_overview_session_title" = "Session"; +"user_session_overview_current_session_title" = "Nuvarande session"; +"user_session_details_application_url" = "URL"; +"user_session_details_application_version" = "Version"; +"user_session_details_application_name" = "Namn"; +"user_session_details_device_os" = "Operativsystem"; +"user_session_details_device_browser" = "Webbläsare"; +"user_session_details_device_model" = "Modell"; +"user_session_details_device_ip_location" = "IP-plats"; +"user_session_details_device_ip_address" = "IP-adress"; +"user_session_details_last_activity" = "Senaste aktivitet"; +"user_session_details_session_section_footer" = "Kopiera data genom att trycka på den och hålla nere."; +"user_session_details_session_id" = "Sessions-ID"; +"user_session_details_session_name" = "Sessionsnamn"; +"user_session_details_device_section_header" = "Enhet"; +"user_session_details_application_section_header" = "Applikation"; +"user_session_details_session_section_header" = "Session"; +"user_session_details_title" = "Sessionsdetaljer"; +"device_type_name_unknown" = "Okänd"; +"device_type_name_mobile" = "Mobil"; +"device_type_name_web" = "Webb"; +"device_type_name_desktop" = "Skrivbord"; +"device_name_unknown" = "Okänd klient"; +"device_name_mobile" = "%@ Mobil"; +"device_name_web" = "%@ Webb"; +"device_name_desktop" = "%@ Skrivbord"; +"user_inactive_session_item_with_date" = "Inaktiv i 90+ dagar (%@)"; +"user_inactive_session_item" = "Inaktiv i 90+ dagar"; +"user_session_item_details_last_activity" = "Senast aktiv %@"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_other_session_menu_sign_out_sessions" = "Logga ut ur %@ sessioner"; +"user_other_session_menu_select_sessions" = "Välj sessioner"; +"user_other_session_selected_count" = "%@ valda"; +"user_other_session_clear_filter" = "Rensa filter"; +"user_other_session_no_unverified_sessions" = "Inga overifierade sessioner hittade."; +"user_other_session_no_verified_sessions" = "Inga verifierade sessioner hittade."; +"user_other_session_no_inactive_sessions" = "Inga inaktiva sessioner hittade."; +"user_other_session_filter_menu_inactive" = "Inaktiva"; +"user_other_session_filter_menu_unverified" = "Overifierade"; +"user_other_session_filter_menu_verified" = "Verifierade"; +"user_other_session_filter_menu_all" = "Alla sessioner"; +"user_other_session_filter" = "Filtrera"; +"user_other_session_verified_sessions_header_subtitle" = "För bäst säkerhet, logga ut ur alla sessioner du inte känner igen eller använder längre."; +"user_other_session_current_session_details" = "Din nuvarande session"; +"user_other_session_unverified_sessions_header_subtitle" = "Verifiera dina sessioner för förbättrade säkra meddelanden eller logga ut ur de du inte känner igen eller använder längre."; +"user_other_session_security_recommendation_title" = "Andra sessioner"; +"user_session_rename_session_description" = "Andra användare i direktmeddelanden och rum du går med i kan se den fulla listan över dina sessioner.\n\nDetta gör att de kan lita på att de verkligen pratar med dig, men det betyder också att de kan se sessionsnamnet du anger här."; +"user_session_rename_session_title" = "Döper om sessioner"; +"user_session_inactive_session_description" = "Inaktiva sessioner är sessioner du inte har använt på ett tag, men de fortsätter att ta emot krypteringsnycklar.\n\nBorttagning av inaktiva sessioner förbättrar säkerhet och prestanda, och gör det enklare för dig att identifiera om en ny session ser misstänkt ut."; +"user_session_inactive_session_title" = "Inaktiva sessioner"; +"user_session_permanently_unverified_session_description" = "Sessionen stöder inte kryptering, så den kan inte verifieras.\n\nDu kommer inte kunna delta i rum där kryptering är aktiverat när du använder den här sessionen.\n\nFör bäst säkerhet så rekommenderas det att använda Matrixklienter som stöder kryptering."; +"user_session_unverified_session_description" = "Overifierade sessioner är sessioner som har loggat in med dina uppgifter men som inte har korsverifierats.\n\nDu bör speciellt försäkra att du känner igen dessa sessioner eftersom de kan representera obehörig användning av ditt konto."; +"user_session_unverified_session_title" = "Overifierad session"; +"user_session_verified_session_description" = "Verifierade sessioner är alla ställen där du använder Element efter att ha angett din lösenfras eller bekräftat din identitet med en annan verifierad session.\n\nDet betyder att du har alla nycklar som krävs för att låsa upp krypterade meddelanden och bekräfta för andra användare att du litar på den här sessionen."; +"user_session_verified_session_title" = "Verifierade sessioner"; +"user_session_got_it" = "Förstått"; +"user_session_push_notifications_message" = "När aktiverad så tar den här sessionen emot pushnotiser."; +"user_session_push_notifications" = "Pushnotiser"; +"user_other_session_verified_additional_info" = "Den här sessioner är redo för säkra meddelanden."; +"user_other_session_permanently_unverified_additional_info" = "Den här sessionen stöder inte kryptering och kan därför inte verifieras."; +"user_other_session_unverified_additional_info" = "Verifiera eller logga ut ur den här sessionen för bäst säkerhet och pålitlighet."; +"user_session_verification_unknown_additional_info" = "Verifiera din nuvarande session för att avslöja den här sessionens verifieringsstatus."; +"user_session_unverified_additional_info" = "Verifiera din nuvarande session för förbättrade säkra meddelanden."; +"user_session_verified_additional_info" = "Din nuvarande session är redo för säkra meddelanden."; +"user_session_learn_more" = "Läs mer"; +"user_session_view_details" = "Visa detaljer"; +"user_session_verify_action" = "Verifiera session"; +"user_session_verification_unknown_short" = "Okänd"; +"user_session_unverified_short" = "Overifierad"; +"user_session_verified_short" = "Verifierad"; +"user_session_verification_unknown" = "Okänd verifieringsstatus"; +"user_session_unverified" = "Overifierad session"; +"user_session_verified" = "Verifierad session"; +"user_sessions_view_all_action" = "Visa alla (%d)"; +"user_sessions_overview_link_device" = "Länka en enhet"; +"user_sessions_overview_current_session_section_title" = "Nuvarande session"; +"user_sessions_hide_location_info" = "Dölj IP-adress"; +"user_sessions_show_location_info" = "Visa IP-adress"; +"user_sessions_overview_other_sessions_section_info" = "För bäst säkerhet, verifiera dina sessioner och logga ut ur alla sessioner du inte känner igen eller använder längre."; +"user_sessions_overview_other_sessions_section_title" = "Andra sessioner"; +"user_sessions_overview_security_recommendations_inactive_info" = "Överväg att logga ut ur gamla sessioner (90 dagar eller äldre) du inte använder längre."; +"user_sessions_overview_security_recommendations_inactive_title" = "Inaktiva sessioner"; +"user_sessions_overview_security_recommendations_unverified_info" = "Verifiera eller logga ut från overifierade sessioner."; +"user_sessions_overview_security_recommendations_unverified_title" = "Overifierade sessioner"; +"user_sessions_overview_security_recommendations_section_info" = "Förbättra din kontosäkerhet genom att följa dessa rekommendationer."; +"user_sessions_overview_security_recommendations_section_title" = "Säkerhetsrekommendationer"; +"user_sessions_overview_title" = "Sessioner"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"location_sharing_map_loading_error" = "Kan inte ladda karta.\nDen här hemservern är inte konfigurerad för att visa kartor"; +"location_sharing_invalid_power_level_message" = "Du har inte de behörigheter som krävs för att dela realtidsplats i det här rummet."; +"location_sharing_invalid_power_level_title" = "Du är inte behörig att dela realtidsplats"; +"poll_timeline_reply_ended_poll" = "Avslutade omröstning"; +"poll_timeline_ended_text" = "Avslutade omröstningen"; +"poll_timeline_decryption_error" = "På grund av avkrypteringsfel så kanske inte vissa röster räknas"; +"poll_history_fetching_error" = "Fel vid hämtning av omröstningar."; +"poll_history_load_more" = "Ladda fler omröstningar"; +"poll_history_no_past_poll_period_text" = "Det finns inga tidigare omröstningar från det senaste %@ dagarna. Ladda fler omröstningar för att se omröstningar från tidigare månader"; +"poll_history_no_active_poll_period_text" = "Det finns inga aktiva omröstningar under de senaste %@ dagarna. Ladda fler omröstningar för att visa omröstningar för tidigare månader"; +"poll_history_no_past_poll_text" = "Det finns inga tidigare omröstningar i det här rummet"; +"poll_history_no_active_poll_text" = "Det finns inga aktiva omröstningar i det här rummet"; +"poll_history_past_segment_title" = "Tidigare omröstningar"; +"poll_history_active_segment_title" = "Aktiva omröstningar"; +"poll_history_loading_text" = "Visar omröstningar"; + +// MARK: - Polls history + +"poll_history_title" = "Omröstningshistorik"; +"space_invite_nav_title" = "Utrymmesinbjudan"; +"space_detail_nav_title" = "Utrymmesdetalj"; +"space_selector_create_space" = "Skapa utrymme"; +"space_selector_empty_view_information" = "Utrymmen är ett sätt att gruppera rum och personer. Skapa et utrymme för att komma igång."; +"space_selector_empty_view_title" = "Inga utrymmen än."; + +// MARK: - Space Selector + +"space_selector_title" = "Mina utrymmen"; +"room_invites_empty_view_information" = "Det här är vart dina inbjudningar hamnar."; + +// MARK: - Room invites + +"room_invites_empty_view_title" = "Inget nytt."; +"all_chats_edit_menu_space_settings" = "Utrymmesinställningar"; +"all_chats_edit_menu_leave_space" = "Lämna %@"; +"all_chats_user_menu_settings" = "Användarinställningar"; +"all_chats_user_menu_accessibility_label" = "Användarmeny"; +"room_recents_recently_viewed_section" = "Nyligen sedda"; +"all_chats_nothing_found_placeholder_message" = "Pröva att justera din sökning."; +"all_chats_nothing_found_placeholder_title" = "Inget hittat."; +"all_chats_empty_unreads_placeholder_message" = "Det här är vart dina olästa meddelanden kommer att hamna, när du har några."; +"voice_broadcast_recorder_connection_error" = "Anslutningsfel - Inspelning pausad"; +"voice_broadcast_connection_error_message" = "Tyvärr kan vi inte starta en röstsändning för tillfället. Vänligen pröva igen senare."; +"voice_broadcast_connection_error_title" = "Anslutningsfel"; +"voice_broadcast_playback_lock_screen_placeholder" = "Röstsändning"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrerar data\n%@ %%"; +"room_details_polls" = "Omröstningshistorik"; +"settings_labs_disable_crypto_sdk" = "Totalsträckskryptering i Rust (logga ut för att stänga av)"; +"settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk."; +"settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust"; +"accessibility_selected" = "vald"; +"settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök igen."; +"wysiwyg_composer_format_action_un_indent" = "Minska indrag"; +"wysiwyg_composer_format_action_indent" = "Öka indrag"; +"poll_history_detail_view_in_timeline" = "Visa omröstning i tidslinje"; +"voice_broadcast_playback_unable_to_decrypt" = "Kunde inte avkryptera denna röstsändning."; +"home_context_menu_mark_as_unread" = "Markera som oläst"; +"key_backup_recover_from_private_key_progress" = "%@%% Färdig"; +"authentication_qr_login_failure_device_not_supported" = "Det finns inget stöd för att länka denna enhet."; diff --git a/Riot/Assets/uk.lproj/Localizable.strings b/Riot/Assets/uk.lproj/Localizable.strings index 276df4c3e..90e0de28e 100644 --- a/Riot/Assets/uk.lproj/Localizable.strings +++ b/Riot/Assets/uk.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ надсилає дані про своє місцеперебування"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ розпочинає голосову трансляцію"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 96e8166d7..143575185 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2670,14 +2670,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Нічого нового."; -"all_chats_onboarding_try_it" = "Спробувати"; -"all_chats_onboarding_title" = "Що нового"; -"all_chats_onboarding_page_message3" = "Торкніться свого профілю, щоб розповісти нам свою думку."; -"all_chats_onboarding_page_title3" = "Напишіть відгук"; -"all_chats_onboarding_page_message2" = "Отримуйте доступ до своїх просторів (унизу ліворуч) швидше та легше, ніж раніше."; -"all_chats_onboarding_page_title2" = "Доступ до просторів"; -"all_chats_onboarding_page_message1" = "Щоб спростити ваш Element, вкладки тепер необов’язкові. Керуйте ними у верхньому правому меню."; -"all_chats_onboarding_page_title1" = "Вітаємо в новому вигляді!"; "all_chats_nothing_found_placeholder_message" = "Спробуйте налаштувати пошук."; "all_chats_nothing_found_placeholder_title" = "Нічого не знайдено."; "all_chats_empty_unreads_placeholder_message" = "Тут з'являтимуться ваші непрочитані повідомлення, якщо вони є."; @@ -2893,3 +2885,45 @@ "poll_timeline_decryption_error" = "Через помилки під час розшифрування деякі голоси можуть бути не враховані"; "voice_message_broadcast_in_progress_title" = "Неможливо розпочати запис голосового повідомлення"; "voice_message_broadcast_in_progress_message" = "Ви не можете розпочати запис голосового повідомлення, оскільки зараз триває запис трансляції наживо. Будь ласка, завершіть трансляцію, щоб розпочати запис голосового повідомлення"; +"poll_timeline_ended_text" = "Опитування завершено"; +"voice_broadcast_voip_cannot_start_description" = "Ви не можете розпочати виклик, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати виклик."; +"voice_broadcast_voip_cannot_start_title" = "Неможливо розпочати виклик"; +"poll_history_no_past_poll_text" = "У цій кімнаті немає минулих опитувань"; +"poll_history_no_active_poll_text" = "У цій кімнаті немає активних опитувань"; +"poll_history_past_segment_title" = "Минулі опитування"; +"poll_history_active_segment_title" = "Активні опитування"; + +// MARK: - Polls history + +"poll_history_title" = "Історія опитувань"; +"room_details_polls" = "Історія опитувань"; +"accessibility_selected" = "вибрано"; +"voice_broadcast_playback_lock_screen_placeholder" = "Голосові трансляції"; +"voice_broadcast_connection_error_message" = "На жаль, ми не можемо розпочати запис прямо зараз. Повторіть спробу пізніше."; +"voice_broadcast_connection_error_title" = "Помилка з'єднання"; +"wysiwyg_composer_format_action_quote" = "Перемкнути цитування"; +"wysiwyg_composer_format_action_code_block" = "Перемкнути блок коду"; +"wysiwyg_composer_format_action_ordered_list" = "Перемкнути на нумерований список"; +"wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список"; +"voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено"; +"poll_timeline_reply_ended_poll" = "Завершене опитування"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Перенесення даних\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування Rust (вийдіть, щоб вимкнути)"; +"settings_labs_confirm_crypto_sdk" = "Зауважте, що оскільки ця функція досі перебуває на стадії експерименту, вона може працювати не так, як очікується, і може мати непередбачувані наслідки. Щоб вимкнути цю функцію, просто вийдіть з системи та увійдіть знову. Використовуйте на власний розсуд і з обережністю."; +"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування Rust"; +"poll_history_load_more" = "Завантажити більше опитувань"; +"poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; +"poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; +"poll_history_loading_text" = "Показ опитувань"; +"poll_history_fetching_error" = "Помилка отримання опитувань."; +"key_backup_recover_from_private_key_progress" = "%@%% виконано"; +"voice_broadcast_playback_unable_to_decrypt" = "Неможливо розшифрувати цю голосову трансляцію."; +"home_context_menu_mark_as_unread" = "Позначити непрочитаним"; +"wysiwyg_composer_format_action_un_indent" = "Зменшити відступ"; +"wysiwyg_composer_format_action_indent" = "Збільшити відступ"; +"settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз."; +"poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці"; +"authentication_qr_login_failure_device_not_supported" = "Пов'язування з цим пристроєм не підтримується."; diff --git a/Riot/Assets/zh_Hans.lproj/Localizable.strings b/Riot/Assets/zh_Hans.lproj/Localizable.strings index 1e13dc6f6..2b4f707e2 100644 --- a/Riot/Assets/zh_Hans.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hans.lproj/Localizable.strings @@ -123,3 +123,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ 分享了他们的位置"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@开始语音广播"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index d7a775d4b..69d2d7cb9 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -196,7 +196,7 @@ "room_event_action_copy" = "复制"; "room_event_action_quote" = "引用"; "room_event_action_redact" = "移除"; -"room_event_action_more" = "移动"; +"room_event_action_more" = "更多"; "room_event_action_share" = "分享"; "room_event_action_permalink" = "复制消息的链接"; "room_event_action_view_source" = "查看源数据"; @@ -287,8 +287,8 @@ "settings_old_password" = "旧密码"; "settings_new_password" = "新密码"; "settings_confirm_password" = "确认密码"; -"settings_fail_to_update_password" = "更新密码失败"; -"settings_password_updated" = "您的密码已经更新"; +"settings_fail_to_update_password" = "更新Matrix账户密码失败"; +"settings_password_updated" = "您的Matrix账户密码已经更新"; "settings_crypto_device_name" = "会话名称: "; "settings_crypto_device_id" = "\n会话ID: "; "settings_crypto_device_key" = "\n会话密钥:\n"; @@ -582,7 +582,7 @@ "deactivate_account_informations_part5" = "如果您希望我们忘记您的消息,请勾选下面的框\n\nMatrix中的消息可见性与电子邮件类似。 我们忘记您的消息意味着您已发送的消息将不会再与任何新用户或未注册用户共享,但已有权访问这些消息的注册用户仍可访问其副本。"; "deactivate_account_forget_messages_information_part1" = "当我的账户被停用时,请忘记我发送的所有消息("; "deactivate_account_forget_messages_information_part3" = ": 这会导致将来加入的用户看到的是一段不完整的对话)"; -"deactivate_account_password_alert_message" = "要继续,请输入您的密码"; +"deactivate_account_password_alert_message" = "要继续,请输入你的Matrix账户密码"; "rerequest_keys_alert_message" = "请在另一台可以解密消息的设备上启动%@,这样它就可以将密钥发送到此会话。"; "key_backup_setup_title" = "密钥备份"; "key_backup_setup_skip_alert_title" = "您确定吗?"; @@ -725,7 +725,7 @@ "settings_labs_enable_cross_signing" = "开启交叉签名按用户验证而不是按设备验证(开发中)"; "settings_add_3pid_password_title_email" = "添加邮箱地址"; "settings_add_3pid_password_title_msidsn" = "添加电话号码"; -"settings_add_3pid_password_message" = "请填写你的密码以继续"; +"settings_add_3pid_password_message" = "请填写你的Matrix账户的密码以继续"; "settings_add_3pid_invalid_password_message" = "验证信息无效"; "settings_key_backup_button_connect" = "关联此会话到密钥备份"; "settings_devices_description" = "会话的公开名字会对你联络的人可见"; @@ -1044,15 +1044,15 @@ "key_verification_bootstrap_not_setup_title" = "错误"; "key_verification_bootstrap_not_setup_message" = "您需要先启动交叉签名。"; "key_verification_verify_qr_code_title" = "通过扫描进行验证"; -"key_verification_verify_qr_code_information" = "扫描代码以安全地相互验证。"; -"key_verification_verify_qr_code_information_other_device" = "扫描以下代码以验证:"; +"key_verification_verify_qr_code_information" = "扫描条码以安全地相互验证。"; +"key_verification_verify_qr_code_information_other_device" = "扫描以下条码以验证:"; "key_verification_verify_qr_code_emoji_information" = "通过比较唯一的表情符号进行验证。"; -"key_verification_verify_qr_code_scan_code_action" = "扫描他们的代码"; +"key_verification_verify_qr_code_scan_code_action" = "扫描他们的条码"; "key_verification_verify_qr_code_cannot_scan_action" = "不能扫描吗?"; "key_verification_verify_qr_code_start_emoji_action" = "通过表情符号验证"; -"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了二维码?"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了QR码?"; "key_verification_verify_qr_code_scan_other_code_success_title" = "代码已验证!"; -"key_verification_verify_qr_code_scan_other_code_success_message" = "二维码已成功验证。"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR码已成功验证。"; // Scanning "key_verification_scan_confirmation_scanning_title" = "快好了!正在等待确认…"; "key_verification_scan_confirmation_scanning_user_waiting_other" = "等待中%@…"; @@ -1082,7 +1082,7 @@ "secrets_recovery_with_key_invalid_recovery_key_title" = "无法访问机密存储"; "secrets_recovery_with_key_invalid_recovery_key_message" = "请验证您输入的安全密钥是否正确。"; "rooms_empty_view_information" = "房间非常适合任何群聊,无论是私人的还是公共的。点击+以查找现有房间,或新建房间。"; -"security_settings_user_password_description" = "通过输入您的账户密码确认您的身份"; +"security_settings_user_password_description" = "通过输入您的Matrix账户密码确认您的身份"; "rooms_empty_view_title" = "房间"; "people_empty_view_information" = "与任何人安全聊天。点击+开始添加人员。"; "people_empty_view_title" = "用户"; @@ -1104,7 +1104,7 @@ "security_settings_secure_backup_synchronise" = "同步"; "security_settings_secure_backup_setup" = "设置"; "security_settings_secure_backup_description" = "备份你的账户数据备份和加密密钥,以防你无法访问会话。 你的密钥将受到唯一的安全密钥保护。"; -"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改密码并重置安全备份。"; +"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改Matrix账户的密码并重置安全备份。"; "settings_show_NSFW_public_rooms" = "显示 NSFW 公共房间"; "external_link_confirmation_message" = "此链接 %@ 会将您带至另一个网站:%@\n\n是否前往?"; "external_link_confirmation_title" = "双击此链接"; @@ -1167,14 +1167,14 @@ "room_info_list_section_other" = "其他"; "create_room_section_footer_encryption" = "加密一经启用,便无法禁用。"; "create_room_placeholder_address" = "#testroom:matrix.org"; -"create_room_section_header_address" = "房间地址"; -"create_room_section_header_type" = "房间类型"; +"create_room_section_header_address" = "地址"; +"create_room_section_header_type" = "谁可以加入"; "create_room_enable_encryption" = "启用加密"; -"create_room_section_header_encryption" = "房间加密"; +"create_room_section_header_encryption" = "加密"; "create_room_placeholder_topic" = "这个房间是关于什么的?"; -"create_room_section_header_topic" = "房间话题(可选)"; +"create_room_section_header_topic" = "话题(可选)"; "create_room_placeholder_name" = "名称"; -"create_room_section_header_name" = "房间名称"; +"create_room_section_header_name" = "名称"; // MARK: - Create Room @@ -1251,10 +1251,10 @@ "invite_friends_share_text" = "嗨,在 %@ 跟我说:%@"; "favourites_empty_view_information" = "你可以选择几种方法 - 最快只需按住。点击星星,它们会自动出现在这里,以确保安全。"; "home_empty_view_information" = "团队、朋友和组织的一体化安全聊天应用程序。 点击下面的「+」按钮添加人员和房间。"; -"create_room_show_in_directory" = "在目录中显示房间"; +"create_room_show_in_directory" = "在房间目录中显示"; "create_room_section_footer_type" = "人们只有在收到聊天室邀请后才可以进入私有房间。"; -"create_room_type_public" = "公开房间"; -"create_room_type_private" = "私有房间"; +"create_room_type_public" = "公开房间(任何人)"; +"create_room_type_private" = "私有房间(仅邀请)"; "biometrics_cant_unlocked_alert_message_login" = "重新登录"; "biometrics_cant_unlocked_alert_message_x" = "若要解锁,请使用 %@ 或重新登录并启用 %@"; "biometrics_cant_unlocked_alert_title" = "无法解锁应用程序"; @@ -1288,7 +1288,7 @@ // Banner "cross_signing_setup_banner_title" = "设置加密"; -"secrets_reset_authentication_message" = "请输入你的账户密码进行确认"; +"secrets_reset_authentication_message" = "请输入你的Matrix账户密码进行确认"; "secrets_reset_warning_message" = "您将重新启动,没有历史记录,消息,受信任的设备或受信任的用户。"; "secrets_reset_warning_title" = "如果你选择全部重置"; "secrets_reset_information" = "仅当没有其他设备可用来验证此设备时,才执行此操作。"; @@ -2231,3 +2231,57 @@ "onboarding_congratulations_home_button" = "带我到主页"; "onboarding_use_case_message" = "我们将帮助你连接"; "invite_to" = "邀请到%@"; +"threads_empty_title" = "保持讨论的有条理性"; +"threads_action_my_threads" = "我的消息列"; +"threads_action_all_threads" = "所有消息列"; +"threads_title" = "消息列"; +"thread_copy_link_to_thread" = "将链接复制到消息列"; + +// MARK: Threads +"room_thread_title" = "消息列"; +"room_accessibility_record_voice_message_hint" = "双击并保持录音。"; +"room_accessibility_record_voice_message" = "录制语音消息"; +"room_accessibility_thread_more" = "更多"; +"room_accessibility_threads" = "消息列"; +"room_event_copy_link_info" = "链接已复制到剪贴板。"; +"room_event_action_reply_in_thread" = "消息列"; +"room_event_action_view_in_room" = "在房间浏览"; +"room_first_message_placeholder" = "发送您的第一条消息……"; +"room_participants_invite_prompt_to_msg" = "您确定要邀请%@ 到 %@吗?"; +"room_participants_leave_success" = "离开房间"; +"room_participants_leave_processing" = "离开"; +"search_filter_placeholder" = "过滤"; +"password_policy_pwd_in_dict_error" = "此密码已在字典中找到,不允许使用。"; +"password_policy_weak_pwd_error" = "此密码太弱了。它必须包含至少8个字符,每种类型至少有一个字符: 大写、小写、数字和特殊字符。"; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "密码过短"; +"authentication_qr_login_failure_retry" = "再试一次"; +"authentication_qr_login_failure_request_timed_out" = "连接没有在规定的时间内完成。"; +"authentication_qr_login_failure_request_denied" = "请求在另一个设备上被拒绝。"; +"authentication_qr_login_failure_invalid_qr" = "QR码无效。"; +"authentication_qr_login_failure_title" = "连接失败"; +"authentication_qr_login_loading_signed_in" = "您现在已经登录到另一个设备上。"; +"authentication_qr_login_loading_waiting_signin" = "正在等待设备以登录。"; +"authentication_qr_login_loading_connecting_device" = "正在连接到设备"; +"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您账户的完全访问权限。"; +"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:"; +"authentication_qr_login_confirm_title" = "安全连接已建立"; +"authentication_qr_login_scan_subtitle" = "将QR码放置在下面的方框中"; +"authentication_qr_login_scan_title" = "扫描QR码"; +"authentication_qr_login_display_step2" = "选择“以QR码登入”"; +"authentication_qr_login_display_step1" = "在您的其它设备中打开Element"; +"onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。"; +"user_session_learn_more" = "了解更多"; +"manage_session_name_info_link" = "了解更多"; +"threads_beta_information_link" = "了解更多"; +"authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的QR码。"; +"room_invite_to_space_option_detail" = "他们可以探索 %@,但不会成为 %@ 的成员。"; +"analytics_prompt_message_new_user" = "通过分享匿名的使用数据,帮助我们识别问题并改进 %@ 。为了了解人们如何使用多个设备,我们将生成一个随机的标识符,由你的设备共享。"; +"threads_notice_done" = "知道了"; +"message_from_a_thread" = "来自消息列"; +"threads_empty_info_all" = "消息列帮助你的对话不离题且易于跟踪。"; +"accessibility_selected" = "已选中"; +"deselect_all" = "取消全选"; +"notice_voice_broadcast_ended" = "%@结束了一个语音广播。"; +"notice_voice_broadcast_ended_by_you" = "你结束了一个语音广播。"; diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 25c64fe45..ef3116270 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -1,6 +1,6 @@ // Titles "title_home" = "首頁"; -"title_favourites" = "收藏夾"; +"title_favourites" = "喜好項目"; "title_people" = "聯絡人"; "title_rooms" = "聊天室"; "title_groups" = "社群"; @@ -8,7 +8,7 @@ // Actions "view" = "檢視"; "next" = "下一步"; -"back" = "返回"; +"back" = "上一步"; "continue" = "繼續"; "create" = "建立"; "start" = "開始"; @@ -618,10 +618,10 @@ "joined" = "已加入"; "skip" = "跳過"; "close" = "關閉"; -"store_promotional_text" = "開放網絡上的隱私保護聊天和協作應用程序。 去中心化管理。 沒有數據挖掘,沒有後門,也沒有第三方存取。"; +"store_promotional_text" = "開放網路上的隱私保護聊天和協作應用程式。去中心化管理。沒有資料探勘,沒有後門,也沒有第三方存取。"; "store_full_description" = "Element是一種新型的通訊和協作應用程式,它可以使你:\n\n1.掌控您的隱私\n2.可以與Matrix網絡中的任何人進行通信,甚至可以與Slack等應用程式整合\n3.保護您免受廣告,數據挖掘,後門和封閉平台的侵害\n4.通過端到端加密和交互簽名來驗證他人,從而保護您的安全\n\nElement是去中心化的開源軟件,因此與其他通訊和協作應用程式完全不同。\n\nElement允許您自行架設(或選擇託管)伺服器,使您擁有隱私權,所有權以及對數據和會話的控制權。自行架設的伺服器可以使您訪問開放的網絡;因此,您不僅可以只與其他 Element 用戶聊天。而且非常安全。\n\nElement之所以能夠達至所有這些目標,是因為它在Matrix(開放,去中心化通信的標準)上運行。\n\nElement通過讓您選擇託管對話的伺服器來控制您的訊息和資料。在Element應用程式中,您可以選擇以不同方式託管你的訊息:\n\n1.在matrix.org公共伺服器上獲得一個免費帳戶\n2.通過在自己的硬件上架設伺服器來託管帳戶\n3.訂閱Element Matrix Services託管平台,即可在自定伺服器上註冊帳戶\n\n為什麼選擇Element?\n\n擁有您的數據:您可以決定將數據和訊息保留在何處。您擁有並控制它,而不是某些超大型企業一樣,會挖掘您的數據或把數據提供給第三方。\n\n開放的通訊和協作:您可以與Matrix網絡中的任何人聊天,無論他們使用的是Element還是其他Matrix應用程式,甚至他們使用的是Slack,IRC或XMPP之類的其他通訊系統。\n\n超級安全:真正的端到端加密(只有對話中的人才能解密消息),並進行交互簽名以驗證對話參與者的設備。\n\n完整的通信:文字通訊,語音和視像通話,文件共享,屏幕共享以及大量整合,機器人和小部件。建立房間、社群,保持聯繫並完成工作。\n\n無論您身在何處都可保持聯繫:無論您身在何處,都可以通過 https://element.io/app 在所有設備和網絡上完全同步訊息歷史記錄來保持聯繫。"; // String for App Store -"store_short_description" = "去中心化的安全通訊軟件"; +"store_short_description" = "去中心化的安全通訊軟體"; "settings_three_pids_management_information_part1" = "在此管理你可以用作登入或回復帳戶的電郵或電話號碼。你也可控制誰可以用這些資料找到你。 "; "external_link_confirmation_message" = "此鏈結 %@ 將帶你到另一網頁: %@\n\n確定要前往?"; "external_link_confirmation_title" = "按此鏈結"; @@ -1141,7 +1141,7 @@ "notice_room_history_visible_to_members" = "%@ 讓所有聊天室成員都能看到未來的房間歷史記錄。"; "stop" = "停止"; "joining" = "正在加入"; -"enable" = "啓用"; +"enable" = "啟用"; "service_terms_modal_policy_checkbox_accessibility_hint" = "確認接受 %@"; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "請閱讀 %@ 的條款與政策"; diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index 17c42cd71..0b659da6f 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -46,10 +46,10 @@ extension MXBugReportRestClient { // User info (TODO: handle multi-account and find a way to expose them in rageshake API) var userInfo = [String: String]() let mainAccount = MXKAccountManager.shared().accounts.first - if let userId = mainAccount?.mxSession.myUser.userId { + if let userId = mainAccount?.mxSession?.myUser?.userId { userInfo["user_id"] = userId } - if let deviceId = mainAccount?.mxSession.matrixRestClient.credentials.deviceId { + if let deviceId = mainAccount?.mxSession?.myDeviceId { userInfo["device_id"] = deviceId } diff --git a/Riot/Categories/MXKTableViewCellWithTextView.swift b/Riot/Categories/MXKTableViewCellWithTextView.swift index 9331b5fd6..03288d377 100644 --- a/Riot/Categories/MXKTableViewCellWithTextView.swift +++ b/Riot/Categories/MXKTableViewCellWithTextView.swift @@ -24,7 +24,6 @@ extension MXKTableViewCellWithTextView: Themable { func update(theme: Theme) { mxkTextView.backgroundColor = .clear mxkTextView.textColor = theme.textPrimaryColor - mxkTextView.tintColor = theme.tintColor backgroundColor = theme.backgroundColor contentView.backgroundColor = .clear } diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index df47c1674..04538ba80 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -637,7 +637,7 @@ }]; } - [notificationCenter enableRule:rule isEnabled:YES]; + [notificationCenter enableRule:rule isEnabled:YES completion:nil]; } - (void)setNotificationCenterDidFailObserver:(id)anObserver diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index 3ad3bef65..c6a55a230 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -27,7 +27,7 @@ { [mxkImageView vc_setRoomAvatarImageWith:self.avatar roomId:self.roomId - displayName:self.displayname + displayName:self.displayName mediaManager:self.mxSession.mediaManager]; } diff --git a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift new file mode 100644 index 000000000..4b314a0c1 --- /dev/null +++ b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift @@ -0,0 +1,26 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixSDKCrypto + +extension CryptoStoreError: LocalizedError { + public var errorDescription: String? { + // We dont really care about the type of error here when showing to the user. + // Details about the error are tracked independently + return VectorL10n.e2eNeedLogInAgain + } +} diff --git a/Riot/Categories/Publisher+Riot.swift b/Riot/Categories/Publisher+Riot.swift index 98bb522b3..7c70404c6 100644 --- a/Riot/Categories/Publisher+Riot.swift +++ b/Riot/Categories/Publisher+Riot.swift @@ -33,4 +33,10 @@ extension Publisher { Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler) } } + + func eraseOutput() -> AnyPublisher { + self + .map { _ in () } + .eraseToAnyPublisher() + } } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift new file mode 100644 index 000000000..fd73ce975 --- /dev/null +++ b/Riot/Experiments/CryptoSDKFeature.swift @@ -0,0 +1,97 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status +/// of `CryptoSDK`, and which uses feature flags to control rollout availability. +/// +/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. +/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, +/// it is not available to all users because it requires data tracking user consent. Remote therefore +/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually +/// targetting all users, but each target change requires new app release. +/// +/// Additionally users can manually enable this feature from the settings if they are not already in the +/// feature group. +@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { + @objc static let shared = CryptoSDKFeature() + + var isEnabled: Bool { + RiotSettings.shared.enableCryptoSDK + } + + private static let FeatureName = "ios-crypto-sdk" + private let remoteFeature: RemoteFeaturesClientProtocol + private let localFeature: PhasedRolloutFeature + + init(remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared) { + self.remoteFeature = remoteFeature + self.localFeature = PhasedRolloutFeature( + name: Self.FeatureName, + // Local feature is currently set to 0% target, and all availability is fully controlled + // by the remote feature. Once the remote is fully rolled out, target for local feature will + // be gradually increased. + targetPercentage: 0.0 + ) + } + + func enable() { + RiotSettings.shared.enableCryptoSDK = true + Analytics.shared.trackCryptoSDKEnabled() + + MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") + } + + func enableIfAvailable(forUserId userId: String!) { + guard !isEnabled else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") + return + } + + guard let userId else { + MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") + return + } + + guard isFeatureEnabled(userId: userId) else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") + return + } + + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") + enable() + } + + @objc func canManuallyEnable(forUserId userId: String!) -> Bool { + guard let userId else { + MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") + return false + } + + // User can manually enable only if not already within the automatic feature group + return !isFeatureEnabled(userId: userId) + } + + @objc func reset() { + RiotSettings.shared.enableCryptoSDK = false + MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") + } + + private func isFeatureEnabled(userId: String) -> Bool { + remoteFeature.isFeatureEnabled(Self.FeatureName) || localFeature.isEnabled(userId: userId) + } +} diff --git a/Riot/Experiments/Experiment.swift b/Riot/Experiments/Experiment.swift new file mode 100644 index 000000000..2a1531afc --- /dev/null +++ b/Riot/Experiments/Experiment.swift @@ -0,0 +1,50 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CryptoKit + +/// Object encapsulating an experiment with an arbitrary number of variants, and a method to deterministically +/// and uniformly assign a variant to user. Variants do not carry any implicit semantics, they are plain numbers +/// to be interpreted by the caller of the experiment. +struct Experiment { + let name: String + let variants: UInt + + /// Get the assigned variant from the total number of variants and for a given `userId` + /// + /// This variant is chosen deterministically (the same `userId` and experiment `name` will yield the same variant) + /// and uniformly (multiple users are distributed roughly evenly among the variants). + func variant(userId: String) -> UInt { + // Combine user id with experiment name to avoid identical variant + // for the same user in different experiments + let data = (userId + name).data(using: .utf8) ?? Data() + + // Get the first 8 bytes and map to decimal number (UInt64 = 8 bytes) + let decimal = digest(for: data) + .prefix(8) + .reduce(0) { $0 << 8 | UInt64($1) } + + // Compress the decimal into a set number of variants using modulo + return UInt(decimal % UInt64(variants)) + } + + private func digest(for data: Data) -> SHA256.Digest { + var sha = SHA256() + sha.update(data: data) + return sha.finalize() + } +} diff --git a/Riot/Experiments/PhasedRolloutFeature.swift b/Riot/Experiments/PhasedRolloutFeature.swift new file mode 100644 index 000000000..67025fd3e --- /dev/null +++ b/Riot/Experiments/PhasedRolloutFeature.swift @@ -0,0 +1,46 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Object enabling a phased rollout of features depending on the `userId` and `targetPercentage`. +/// +/// The feature uses an experiment under the hood with 100 variants representing 100%. +/// Each userId is deterministically and uniformly assigned a variant, and depending +/// on whether this falls below or above the `targetPercentage` threshold, the feature +/// is considered enabled or disabled. +struct PhasedRolloutFeature { + private let experiment: Experiment + private let targetPercentage: Double + + init(name: String, targetPercentage: Double) { + self.experiment = .init( + name: name, + // 100 variants where each variant represents a single percentage + variants: 100 + ) + self.targetPercentage = targetPercentage + } + + func isEnabled(userId: String) -> Bool { + // Get a bucket number between 0-99 + let variant = experiment.variant(userId: userId) + // Convert to a percentage + let percentage = Double(variant) / 100 + // Consider enabled if falls below rollout target + return percentage < targetPercentage + } +} diff --git a/Riot/Experiments/RemoteFeaturesClientProtocol.swift b/Riot/Experiments/RemoteFeaturesClientProtocol.swift new file mode 100644 index 000000000..95ca8393e --- /dev/null +++ b/Riot/Experiments/RemoteFeaturesClientProtocol.swift @@ -0,0 +1,22 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A protocol representing a remote features client +protocol RemoteFeaturesClientProtocol { + func isFeatureEnabled(_ feature: String) -> Bool +} diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 134ce07ef..8a9c9b5c5 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -123,7 +123,7 @@ public class BWIL10n: NSObject { public static var bwiAuthBetaSelectionTitle: String { return BWIL10n.tr("Bwi", "bwi_auth_beta_selection_title") } - /// Falls du dein Passwort vergessen hast, wende dich an deinen Poolverwalter oder gehe auf das SelfServicePortal im IntranetBw und setze dein Passwort zurück. + /// Falls Du dein Passwort vergessen hast, wende dich an deinen Poolverwalter oder gehe auf das SelfServicePortal und setze dein Passwort zurück. public static var bwiAuthForgotPasswordAlertText: String { return BWIL10n.tr("Bwi", "bwi_auth_forgot_password_alert_text") } @@ -203,6 +203,18 @@ public class BWIL10n: NSObject { public static func bwiErrorInviteGeneral(_ p1: String) -> String { return BWIL10n.tr("Bwi", "bwi_error_invite_general", p1) } + /// Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich "Umfrageverlauf"). + public static var bwiFeatureBannerAdvertisementText: String { + return BWIL10n.tr("Bwi", "bwi_feature_banner_advertisement_text") + } + /// Neue Funktionen + public static var bwiFeatureBannerHeader: String { + return BWIL10n.tr("Bwi", "bwi_feature_banner_header") + } + /// Erfahre mehr + public static var bwiFeatureBannerShowMoreButton: String { + return BWIL10n.tr("Bwi", "bwi_feature_banner_show_more_button") + } /// Der angegebene Server ist nicht für die Nutzung mit dem %@ vorgesehen public static func bwiLoginProtectionErrorMessage(_ p1: String) -> String { return BWIL10n.tr("Bwi", "bwi_login_protection_error_message", p1) @@ -447,6 +459,10 @@ public class BWIL10n: NSObject { public static var bwiRoomParticipantsSectionAdmin: String { return BWIL10n.tr("Bwi", "bwi_room_participants_section_admin") } + /// Eingeladen + public static var bwiRoomParticipantsSectionInvite: String { + return BWIL10n.tr("Bwi", "bwi_room_participants_section_invite") + } /// Mitglied public static var bwiRoomParticipantsSectionMember: String { return BWIL10n.tr("Bwi", "bwi_room_participants_section_member") @@ -455,10 +471,18 @@ public class BWIL10n: NSObject { public static var bwiRoomParticipantsSectionModerator: String { return BWIL10n.tr("Bwi", "bwi_room_participants_section_moderator") } + /// Details dazu findest du in der Datenschutzerklärung + public static var bwiSettingsAnalyticsSectionFooter: String { + return BWIL10n.tr("Bwi", "bwi_settings_analytics_section_footer") + } /// Fehleranalyse public static var bwiSettingsAnalyticsSectionHeader: String { return BWIL10n.tr("Bwi", "bwi_settings_analytics_section_header") } + /// Fehleranalyse unterstützen + public static var bwiSettingsAnalyticsSwitchText: String { + return BWIL10n.tr("Bwi", "bwi_settings_analytics_switch_text") + } /// Entwickler public static var bwiSettingsDeveloper: String { return BWIL10n.tr("Bwi", "bwi_settings_developer") @@ -871,22 +895,6 @@ public class BWIL10n: NSObject { public static var locationSharingMapCreditsTitle: String { return BWIL10n.tr("Bwi", "location_sharing_map_credits_title") } - /// Diagnosedaten senden - public static var matomoSettingsSendDiagnosticData: String { - return BWIL10n.tr("Bwi", "MATOMO_SETTINGS_SEND_DIAGNOSTIC_DATA") - } - /// Wir brauchen Deine Hilfe, um einige Fehler im Messenger besser analysieren zu können.\nDazu erfassen wir Diagnosedaten.\nDetails dazu findest Du in der Datenschutzerklärung. - public static var matomoUserInfo: String { - return BWIL10n.tr("Bwi", "MATOMO_USER_INFO") - } - /// Datenschutzerklärung - public static var matomoUserInfoButtonMoreInfo: String { - return BWIL10n.tr("Bwi", "MATOMO_USER_INFO_BUTTON_MORE_INFO") - } - /// OK - public static var matomoUserInfoButtonOk: String { - return BWIL10n.tr("Bwi", "MATOMO_USER_INFO_BUTTON_OK") - } /// mehr public static var more: String { return BWIL10n.tr("Bwi", "more") @@ -903,6 +911,10 @@ public class BWIL10n: NSObject { public static var next: String { return BWIL10n.tr("Bwi", "next") } + /// %@ verließ den Raum + public static func noticeRoomLeave(_ p1: String) -> String { + return BWIL10n.tr("Bwi", "notice_room_leave", p1) + } /// Dein Account %@ wurde erstellt. public static func onboardingCongratulationsMessage(_ p1: String) -> String { return BWIL10n.tr("Bwi", "onboarding_congratulations_message", p1) @@ -1059,6 +1071,26 @@ public class BWIL10n: NSObject { public static func roomMemberPowerLevelModeratorIn(_ p1: String) -> String { return BWIL10n.tr("Bwi", "room_member_power_level_moderator_in", p1) } + /// Es gelten globale Benachrichtigungseinstellungen + public static var roomNotifsSettingsAllDescription: String { + return BWIL10n.tr("Bwi", "room_notifs_settings_all_description") + } + /// Standardeinstellungen + public static var roomNotifsSettingsAllMessages: String { + return BWIL10n.tr("Bwi", "room_notifs_settings_all_messages") + } + /// + public static var roomNotifsSettingsEncryptedRoomNotice: String { + return BWIL10n.tr("Bwi", "room_notifs_settings_encrypted_room_notice") + } + /// Benachrichtigungen aus + public static var roomNotifsSettingsNone: String { + return BWIL10n.tr("Bwi", "room_notifs_settings_none") + } + /// Einstellungen für diesen Raum + public static var roomNotifsSettingsNotifyMeFor: String { + return BWIL10n.tr("Bwi", "room_notifs_settings_notify_me_for") + } /// Aus diesem Raum bannen public static var roomParticipantsActionBan: String { return BWIL10n.tr("Bwi", "room_participants_action_ban") @@ -1131,6 +1163,18 @@ public class BWIL10n: NSObject { public static var roomRecentsExploreRooms: String { return BWIL10n.tr("Bwi", "room_recents_explore_rooms") } + /// Dieser QR Code entspricht keinem gültigen Permalink. + public static var roomRecentsScanFailedMessage: String { + return BWIL10n.tr("Bwi", "room_recents_scan_failed_message") + } + /// Scan fehlgeschlagen + public static var roomRecentsScanFailedTitle: String { + return BWIL10n.tr("Bwi", "room_recents_scan_failed_title") + } + /// QR Code scannen + public static var roomRecentsScanQrCode: String { + return BWIL10n.tr("Bwi", "room_recents_scan_qr_code") + } /// Neue Direktnachricht public static var roomRecentsStartChatWith: String { return BWIL10n.tr("Bwi", "room_recents_start_chat_with") @@ -1371,13 +1415,13 @@ public class BWIL10n: NSObject { public static var settingsDirectMessages: String { return BWIL10n.tr("Bwi", "settings_direct_messages") } - /// Der %@ steht von %@ Uhr (UTC%@) bis %@ Uhr (UTC%@) nicht zur Verfügung. - public static func settingsDowntimeMessageDifferentDays(_ p1: String, _ p2: String, _ p3: String, _ p4: String, _ p5: String) -> String { - return BWIL10n.tr("Bwi", "settings_downtime_message_different_days", p1, p2, p3, p4, p5) + /// Der %@ steht von %@, %@ Uhr (UTC%@) bis %@, %@ Uhr (UTC%@) nicht zur Verfügung. Nachrichten können in dieser Zeit nicht verschickt oder empfangen werden. + public static func settingsDowntimeMessageDifferentDays(_ p1: String, _ p2: String, _ p3: String, _ p4: String, _ p5: String, _ p6: String, _ p7: String) -> String { + return BWIL10n.tr("Bwi", "settings_downtime_message_different_days", p1, p2, p3, p4, p5, p6, p7) } - /// Der %@ steht von %@-%@ Uhr (UTC%@) nicht zur Verfügung. - public static func settingsDowntimeMessageSameDay(_ p1: String, _ p2: String, _ p3: String, _ p4: String) -> String { - return BWIL10n.tr("Bwi", "settings_downtime_message_same_day", p1, p2, p3, p4) + /// Der %@ steht am %@, %@ von %@ bis %@ Uhr (UTC%@) nicht zur Verfügung. Nachrichten können in dieser Zeit nicht verschickt oder empfangen werden. + public static func settingsDowntimeMessageSameDay(_ p1: String, _ p2: String, _ p3: String, _ p4: String, _ p5: String, _ p6: String) -> String { + return BWIL10n.tr("Bwi", "settings_downtime_message_same_day", p1, p2, p3, p4, p5, p6) } /// Element-Version %@ public static func settingsElementVersion(_ p1: String) -> String { @@ -1511,6 +1555,10 @@ public class BWIL10n: NSObject { public static var settingsPasswordUpdated: String { return BWIL10n.tr("Bwi", "settings_password_updated") } + /// Permalink Prefix + public static var settingsPermalinkPrefixPickerTitle: String { + return BWIL10n.tr("Bwi", "settings_permalink_prefix_picker_title") + } /// Einladungen public static var settingsRoomInvitations: String { return BWIL10n.tr("Bwi", "settings_room_invitations") @@ -1531,6 +1579,18 @@ public class BWIL10n: NSObject { public static var settingsYourKeywords: String { return BWIL10n.tr("Bwi", "settings_your_keywords") } + /// Dies ist der QR Code zu deinem Profil.\nLasse den QR Code mit der %@ App scannen, damit andere mit dir in Kontakt treten können. + public static func showMyQrScreenMessage(_ p1: String) -> String { + return BWIL10n.tr("Bwi", "show_my_qr_screen_message", p1) + } + /// Mein QR Code + public static var showMyQrScreenTitle: String { + return BWIL10n.tr("Bwi", "show_my_qr_screen_title") + } + /// Meinen QR Code anzeigen + public static var showMyQrSettingsTitle: String { + return BWIL10n.tr("Bwi", "show_my_qr_settings_title") + } /// Überspringen public static var skip: String { return BWIL10n.tr("Bwi", "skip") @@ -1563,6 +1623,14 @@ public class BWIL10n: NSObject { public static var splashScreenTitle: String { return BWIL10n.tr("Bwi", "splash_screen_title") } + /// Der Server ist momentan nicht erreichbar. Versuche es später erneut. + public static var standardErrorAlertTitleDowntime: String { + return BWIL10n.tr("Bwi", "standard_error_alert_title_downtime") + } + /// Der Server ist momentan nicht erreichbar. Versuche es später erneut. + public static var standardErrorAlertTitleNoDowntime: String { + return BWIL10n.tr("Bwi", "standard_error_alert_title_no_downtime") + } /// Starte public static var start: String { return BWIL10n.tr("Bwi", "start") diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 6775e0529..3a16e898b 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -22,9 +22,6 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image internal class Asset: NSObject { @objcMembers @objc(AssetImages) internal class Images: NSObject { - internal static let allChatsOnboarding1 = ImageAsset(name: "all_chats_onboarding1") - internal static let allChatsOnboarding2 = ImageAsset(name: "all_chats_onboarding2") - internal static let allChatsOnboarding3 = ImageAsset(name: "all_chats_onboarding3") internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark") internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo") internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple") @@ -51,6 +48,8 @@ internal class Asset: NSObject { internal static let birthdayCake = ImageAsset(name: "birthday_cake") internal static let fileAttachmentIcon = ImageAsset(name: "file_attachment_icon") internal static let fileScanInfected = ImageAsset(name: "file_scan_infected") + internal static let newFeatures = ImageAsset(name: "new_features") + internal static let qrcodeViewfinder = ImageAsset(name: "qrcode_viewfinder") internal static let welcomeExperience1 = ImageAsset(name: "welcome_experience_1") internal static let welcomeExperience2 = ImageAsset(name: "welcome_experience_2") internal static let welcomeExperience3 = ImageAsset(name: "welcome_experience_3") @@ -127,6 +126,7 @@ internal class Asset: NSObject { internal static let strikethrough = ImageAsset(name: "Strikethrough") internal static let underlined = ImageAsset(name: "Underlined") internal static let bulletList = ImageAsset(name: "bullet_list") + internal static let codeBlock = ImageAsset(name: "code_block") internal static let indentDecrease = ImageAsset(name: "indent_decrease") internal static let maximiseComposer = ImageAsset(name: "maximise_composer") internal static let minimiseComposer = ImageAsset(name: "minimise_composer") @@ -180,6 +180,7 @@ internal class Asset: NSObject { internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action") internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon") internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon") + internal static let image = ImageAsset(name: "Image") internal static let integrationManagerIconpile = ImageAsset(name: "integration_manager_iconpile") internal static let closeBanner = ImageAsset(name: "close_banner") internal static let importFilesButton = ImageAsset(name: "import_files_button") @@ -228,6 +229,7 @@ internal class Asset: NSObject { internal static let newClose = ImageAsset(name: "new_close") internal static let roomActivitiesRetry = ImageAsset(name: "room_activities_retry") internal static let roomScrollUp = ImageAsset(name: "room_scroll_up") + internal static let roomScrollUpBum = ImageAsset(name: "room_scroll_up_bum") internal static let scrolldown = ImageAsset(name: "scrolldown") internal static let scrolldownDark = ImageAsset(name: "scrolldown_dark") internal static let sendingMessageTick = ImageAsset(name: "sending_message_tick") @@ -246,6 +248,7 @@ internal class Asset: NSObject { internal static let inputTextBackground = ImageAsset(name: "input_text_background") internal static let saveIcon = ImageAsset(name: "save_icon") internal static let sendIcon = ImageAsset(name: "send_icon") + internal static let sendIconBum = ImageAsset(name: "send_icon_bum") internal static let uploadIcon = ImageAsset(name: "upload_icon") internal static let uploadIconDark = ImageAsset(name: "upload_icon_dark") internal static let videoCall = ImageAsset(name: "video_call") @@ -263,10 +266,12 @@ internal class Asset: NSObject { internal static let locationMapError = ImageAsset(name: "location_map_error") internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") internal static let locationPinIcon = ImageAsset(name: "location_pin_icon") + internal static let locationPinIconBum = ImageAsset(name: "location_pin_icon_bum") internal static let locationShareIcon = ImageAsset(name: "location_share_icon") internal static let locationUserMarker = ImageAsset(name: "location_user_marker") internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default") internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected") + internal static let pollCheckboxSelectedBum = ImageAsset(name: "poll_checkbox_selected_bum") internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon") internal static let pollDeleteOptionIcon = ImageAsset(name: "poll_delete_option_icon") internal static let pollEditIcon = ImageAsset(name: "poll_edit_icon") @@ -290,6 +295,7 @@ internal class Asset: NSObject { internal static let voiceMessagePlayButton = ImageAsset(name: "voice_message_play_button") internal static let voiceMessageRecordButtonDefault = ImageAsset(name: "voice_message_record_button_default") internal static let voiceMessageRecordButtonRecording = ImageAsset(name: "voice_message_record_button_recording") + internal static let voiceMessageRecordButtonRecordingBum = ImageAsset(name: "voice_message_record_button_recording_bum") internal static let voiceMessageRecordIcon = ImageAsset(name: "voice_message_record_icon") internal static let addMemberFloatingAction = ImageAsset(name: "add_member_floating_action") internal static let addParticipant = ImageAsset(name: "add_participant") @@ -304,6 +310,7 @@ internal class Asset: NSObject { internal static let modIcon = ImageAsset(name: "mod_icon") internal static let moreReactions = ImageAsset(name: "more_reactions") internal static let notifications = ImageAsset(name: "notifications") + internal static let pollHistory = ImageAsset(name: "pollHistory") internal static let reactionsMoreAction = ImageAsset(name: "reactions_more_action") internal static let rollsAndRights = ImageAsset(name: "rolls_and_rights") internal static let roomAccessInfoHeaderIcon = ImageAsset(name: "room_access_info_header_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1dde36ec4..a9c7f938e 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -27,6 +27,10 @@ public class VectorL10n: NSObject { public static var accessibilityCheckboxLabel: String { return VectorL10n.tr("Vector", "accessibility_checkbox_label") } + /// selected + public static var accessibilitySelected: String { + return VectorL10n.tr("Vector", "accessibility_selected") + } /// Unable to verify email address. Please check your email and click on the link it contains. Once this is done, click continue public static var accountEmailValidationError: String { return VectorL10n.tr("Vector", "account_email_validation_error") @@ -207,38 +211,6 @@ public class VectorL10n: NSObject { public static var allChatsNothingFoundPlaceholderTitle: String { return VectorL10n.tr("Vector", "all_chats_nothing_found_placeholder_title") } - /// To simplify your Element, tabs are now optional. Manage them using the top-right menu. - public static var allChatsOnboardingPageMessage1: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message1") - } - /// Access your Spaces (bottom-left) faster and easier than ever before. - public static var allChatsOnboardingPageMessage2: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message2") - } - /// Tap your profile to let us know what you think. - public static var allChatsOnboardingPageMessage3: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message3") - } - /// Welcome to a new view! - public static var allChatsOnboardingPageTitle1: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title1") - } - /// Access Spaces - public static var allChatsOnboardingPageTitle2: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title2") - } - /// Give Feedback - public static var allChatsOnboardingPageTitle3: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title3") - } - /// What's new - public static var allChatsOnboardingTitle: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_title") - } - /// Try it out - public static var allChatsOnboardingTryIt: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_try_it") - } /// Chats public static var allChatsSectionTitle: String { return VectorL10n.tr("Vector", "all_chats_section_title") @@ -771,6 +743,10 @@ public class VectorL10n: NSObject { public static var authenticationQrLoginDisplayTitle: String { return VectorL10n.tr("Vector", "authentication_qr_login_display_title") } + /// Linking with this device is not supported. + public static var authenticationQrLoginFailureDeviceNotSupported: String { + return VectorL10n.tr("Vector", "authentication_qr_login_failure_device_not_supported") + } /// QR code is invalid. public static var authenticationQrLoginFailureInvalidQr: String { return VectorL10n.tr("Vector", "authentication_qr_login_failure_invalid_qr") @@ -1895,6 +1871,14 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyAlertValidateAction: String { return VectorL10n.tr("Vector", "device_verification_self_verify_alert_validate_action") } + /// You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions. + public static var deviceVerificationSelfVerifyOpenOnOtherDeviceInformation: String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_information") + } + /// Open %@ on your other device + public static func deviceVerificationSelfVerifyOpenOnOtherDeviceTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_title", p1) + } /// Use this session to verify your new one, granting it access to encrypted messages. public static var deviceVerificationSelfVerifyStartInformation: String { return VectorL10n.tr("Vector", "device_verification_self_verify_start_information") @@ -1919,9 +1903,9 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyWaitNewSignInTitle: String { return VectorL10n.tr("Vector", "device_verification_self_verify_wait_new_sign_in_title") } - /// If you can't access an existing session - public static var deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation: String { - return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_information") + /// Can't access an existing %@ session? + public static func deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalHelp(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_help", p1) } /// Checking for other verification capabilities ... public static var deviceVerificationSelfVerifyWaitRecoverSecretsCheckingAvailability: String { @@ -2543,6 +2527,10 @@ public class VectorL10n: NSObject { public static var homeContextMenuMarkAsRead: String { return VectorL10n.tr("Vector", "home_context_menu_mark_as_read") } + /// Mark as unread + public static var homeContextMenuMarkAsUnread: String { + return VectorL10n.tr("Vector", "home_context_menu_mark_as_unread") + } /// Mute public static var homeContextMenuMute: String { return VectorL10n.tr("Vector", "home_context_menu_mute") @@ -2751,6 +2739,10 @@ public class VectorL10n: NSObject { public static var keyBackupRecoverFromPrivateKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_info") } + /// %@%% Complete + public static func keyBackupRecoverFromPrivateKeyProgress(_ p1: String) -> String { + return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_progress", p1) + } /// Use your Security Key to unlock your secure message history public static var keyBackupRecoverFromRecoveryKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_recovery_key_info") @@ -3003,6 +2995,26 @@ public class VectorL10n: NSObject { public static func keyVerificationScanConfirmationScanningUserWaitingOther(_ p1: String) -> String { return VectorL10n.tr("Vector", "key_verification_scan_confirmation_scanning_user_waiting_other", p1) } + /// Point your camera at the QR code displayed on your other device to verify your new session + public static var keyVerificationScanQrCodeInformationNewSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_new_session") + } + /// Point your camera at the QR code displayed on your other device to verify this session + public static var keyVerificationScanQrCodeInformationOtherDevice: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_device") + } + /// Point your camera at the QR code displayed on your other device to verify your session + public static var keyVerificationScanQrCodeInformationOtherSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_session") + } + /// Point your camera at the QR code displayed on their device to verify their session + public static var keyVerificationScanQrCodeInformationOtherUser: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_user") + } + /// Scan QR code + public static var keyVerificationScanQrCodeTitle: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_title") + } /// Other users may not trust it. public static var keyVerificationSelfVerifyCurrentSessionAlertMessage: String { return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_message") @@ -3179,6 +3191,10 @@ public class VectorL10n: NSObject { public static var later: String { return VectorL10n.tr("Vector", "later") } + /// Migrating data\n%@ %% + public static func launchLoadingMigratingData(_ p1: String) -> String { + return VectorL10n.tr("Vector", "launch_loading_migrating_data", p1) + } /// Processing data\n%@ %% public static func launchLoadingProcessingResponse(_ p1: String) -> String { return VectorL10n.tr("Vector", "launch_loading_processing_response", p1) @@ -4839,6 +4855,50 @@ public class VectorL10n: NSObject { public static var pollEditFormUpdateFailureTitle: String { return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title") } + /// Active polls + public static var pollHistoryActiveSegmentTitle: String { + return VectorL10n.tr("Vector", "poll_history_active_segment_title") + } + /// View poll in timeline + public static var pollHistoryDetailViewInTimeline: String { + return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline") + } + /// Error fetching polls. + public static var pollHistoryFetchingError: String { + return VectorL10n.tr("Vector", "poll_history_fetching_error") + } + /// Load more polls + public static var pollHistoryLoadMore: String { + return VectorL10n.tr("Vector", "poll_history_load_more") + } + /// Displaying polls + public static var pollHistoryLoadingText: String { + return VectorL10n.tr("Vector", "poll_history_loading_text") + } + /// There are no active polls for the past %@ days. Load more polls to view polls for previous months + public static func pollHistoryNoActivePollPeriodText(_ p1: String) -> String { + return VectorL10n.tr("Vector", "poll_history_no_active_poll_period_text", p1) + } + /// There are no active polls in this room + public static var pollHistoryNoActivePollText: String { + return VectorL10n.tr("Vector", "poll_history_no_active_poll_text") + } + /// There are no past polls for the past %@ days. Load more polls to view polls for previous months + public static func pollHistoryNoPastPollPeriodText(_ p1: String) -> String { + return VectorL10n.tr("Vector", "poll_history_no_past_poll_period_text", p1) + } + /// There are no past polls in this room + public static var pollHistoryNoPastPollText: String { + return VectorL10n.tr("Vector", "poll_history_no_past_poll_text") + } + /// Past polls + public static var pollHistoryPastSegmentTitle: String { + return VectorL10n.tr("Vector", "poll_history_past_segment_title") + } + /// Poll history + public static var pollHistoryTitle: String { + return VectorL10n.tr("Vector", "poll_history_title") + } /// Due to decryption errors, some votes may not be counted public static var pollTimelineDecryptionError: String { return VectorL10n.tr("Vector", "poll_timeline_decryption_error") @@ -4859,6 +4919,10 @@ public class VectorL10n: NSObject { public static var pollTimelineOneVote: String { return VectorL10n.tr("Vector", "poll_timeline_one_vote") } + /// Ended poll + public static var pollTimelineReplyEndedPoll: String { + return VectorL10n.tr("Vector", "poll_timeline_reply_ended_poll") + } /// Final results based on %lu votes public static func pollTimelineTotalFinalResults(_ p1: Int) -> String { return VectorL10n.tr("Vector", "poll_timeline_total_final_results", p1) @@ -5203,6 +5267,10 @@ public class VectorL10n: NSObject { public static var roomCreationNameTitle: String { return VectorL10n.tr("Vector", "room_creation_name_title") } + /// You can only invite one email at a time + public static var roomCreationOnlyOneEmailInvite: String { + return VectorL10n.tr("Vector", "room_creation_only_one_email_invite") + } /// (e.g. @bob:homeserver1; @john:homeserver2...) public static var roomCreationParticipantsPlaceholder: String { return VectorL10n.tr("Vector", "room_creation_participants_placeholder") @@ -5503,6 +5571,10 @@ public class VectorL10n: NSObject { public static var roomDetailsPhotoForDm: String { return VectorL10n.tr("Vector", "room_details_photo_for_dm") } + /// Poll history + public static var roomDetailsPolls: String { + return VectorL10n.tr("Vector", "room_details_polls") + } /// Suggest to space members public static var roomDetailsPromoteRoomSuggestTitle: String { return VectorL10n.tr("Vector", "room_details_promote_room_suggest_title") @@ -6555,6 +6627,14 @@ public class VectorL10n: NSObject { public static var roomUnsentMessagesUnknownDevicesNotification: String { return VectorL10n.tr("Vector", "room_unsent_messages_unknown_devices_notification") } + /// Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted + public static func roomWaitingOtherParticipantsMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_waiting_other_participants_message", p1) + } + /// Waiting for users to join %@ + public static func roomWaitingOtherParticipantsTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_waiting_other_participants_title", p1) + } /// End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption. public static var roomWarningAboutEncryption: String { return VectorL10n.tr("Vector", "room_warning_about_encryption") @@ -7543,14 +7623,22 @@ public class VectorL10n: NSObject { public static func settingsKeyBackupInfoVersion(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_key_backup_info_version", p1) } - /// LABS + /// LAB public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } + /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. + public static var settingsLabsConfirmCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") + } /// Create conference calls with jitsi public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } + /// Rust end-to-end encryption (log out to disable) + public static var settingsLabsDisableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") + } /// End-to-End Encryption public static var settingsLabsE2eEncryption: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") @@ -7563,6 +7651,10 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } + /// Rust end-to-end encryption + public static var settingsLabsEnableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") + } /// Live location sharing - share current location (active development, and temporarily, locations persist in room history) public static var settingsLabsEnableLiveLocationSharing: String { return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") @@ -7723,6 +7815,10 @@ public class VectorL10n: NSObject { public static var settingsProfilePicture: String { return VectorL10n.tr("Vector", "settings_profile_picture") } + /// An error occurred when updating your notification preferences. Please try to toggle your option again. + public static var settingsPushRulesError: String { + return VectorL10n.tr("Vector", "settings_push_rules_error") + } /// Are you sure you want to remove the email address %@? public static func settingsRemoveEmailPromptMsg(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_remove_email_prompt_msg", p1) @@ -9179,6 +9275,14 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBuffering: String { return VectorL10n.tr("Vector", "voice_broadcast_buffering") } + /// Unfortunately we’re unable to start a recording right now. Please try again later. + public static var voiceBroadcastConnectionErrorMessage: String { + return VectorL10n.tr("Vector", "voice_broadcast_connection_error_message") + } + /// Connection error + public static var voiceBroadcastConnectionErrorTitle: String { + return VectorL10n.tr("Vector", "voice_broadcast_connection_error_title") + } /// Live public static var voiceBroadcastLive: String { return VectorL10n.tr("Vector", "voice_broadcast_live") @@ -9191,6 +9295,18 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLoadingError: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error") } + /// Voice broadcast + public static var voiceBroadcastPlaybackLockScreenPlaceholder: String { + return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder") + } + /// Unable to decrypt this voice broadcast. + public static var voiceBroadcastPlaybackUnableToDecrypt: String { + return VectorL10n.tr("Vector", "voice_broadcast_playback_unable_to_decrypt") + } + /// Connection error - Recording paused + public static var voiceBroadcastRecorderConnectionError: String { + return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error") + } /// Yes, stop public static var voiceBroadcastStopAlertAgreeButton: String { return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_agree_button") @@ -9215,6 +9331,14 @@ public class VectorL10n: NSObject { public static var voiceBroadcastUnauthorizedTitle: String { return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") } + /// You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. + public static var voiceBroadcastVoipCannotStartDescription: String { + return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_description") + } + /// Can’t start a call + public static var voiceBroadcastVoipCannotStartTitle: String { + return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_title") + } /// You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message public static var voiceMessageBroadcastInProgressMessage: String { return VectorL10n.tr("Vector", "voice_message_broadcast_in_progress_message") @@ -9339,6 +9463,14 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionBold: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_bold") } + /// Toggle code block + public static var wysiwygComposerFormatActionCodeBlock: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_code_block") + } + /// Increase indentation + public static var wysiwygComposerFormatActionIndent: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_indent") + } /// Apply inline code format public static var wysiwygComposerFormatActionInlineCode: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code") @@ -9351,14 +9483,30 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionLink: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_link") } + /// Toggle numbered list + public static var wysiwygComposerFormatActionOrderedList: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_ordered_list") + } + /// Toggle quote + public static var wysiwygComposerFormatActionQuote: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_quote") + } /// Apply underline format public static var wysiwygComposerFormatActionStrikethrough: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough") } + /// Decrease indentation + public static var wysiwygComposerFormatActionUnIndent: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_un_indent") + } /// Apply strikethrough format public static var wysiwygComposerFormatActionUnderline: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline") } + /// Toggle bulleted list + public static var wysiwygComposerFormatActionUnorderedList: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_unordered_list") + } /// Create a link public static var wysiwygComposerLinkActionCreateTitle: String { return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_create_title") diff --git a/Riot/Managers/Call/CallPresenter.swift b/Riot/Managers/Call/CallPresenter.swift index 9d9465f8b..721f13079 100644 --- a/Riot/Managers/Call/CallPresenter.swift +++ b/Riot/Managers/Call/CallPresenter.swift @@ -208,7 +208,7 @@ class CallPresenter: NSObject { if error == nil { JMCallKitProxy.reportCallUpdate(with: newUUID, handle: roomId, - displayName: room.summary.displayname, + displayName: room.summary.displayName, hasVideo: true) JMCallKitProxy.reportOutgoingCall(with: newUUID, connectedAt: nil) diff --git a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift index c8e3a95b5..747b5a603 100644 --- a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift +++ b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift @@ -32,6 +32,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { private static let cryptoOlmPickleKey = "cryptoOlmPickleKey" private static let roomLastMessageIv = "roomLastMessageIv" private static let roomLastMessageAesKey = "roomLastMessageAesKey" + private static let cryptoSDKStoreKey = "cryptoSDKStoreKey" private var initialized = false @@ -54,9 +55,11 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { generateIvIfNotExists(forKey: EncryptionKeyManager.roomLastMessageIv, inStore: keychainVault) generateAesKeyIfNotExists(forKey: EncryptionKeyManager.roomLastMessageAesKey, inStore: keychainVault) + generateKeyIfNotExists(forKey: EncryptionKeyManager.cryptoSDKStoreKey, inStore: keychainVault, size: 32) assert(keychainVault.objectExists(withKey: EncryptionKeyManager.roomLastMessageIv), "[EncryptionKeyManager] initKeys: Failed to generate IV for room last message") assert(keychainVault.objectExists(withKey: EncryptionKeyManager.roomLastMessageAesKey), "[EncryptionKeyManager] initKeys: Failed to generate AES Key for room last message encryption") + assert(keychainVault.objectExists(withKey: EncryptionKeyManager.cryptoSDKStoreKey), "[EncryptionKeyManager] initKeys: Failed to generate Key for crypto sdk store") guard !BWIBuildSettings.shared.forcedPinProtection || !SecureFileStorage.shared.locked else { MXLog.debug("[EncryptionKeyManager] initKeys: cannot init keys as store is not ready") @@ -79,6 +82,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { || dataType == MXKAccountManagerDataType || dataType == MXCryptoOlmPickleKeyDataType || dataType == MXRoomLastMessageDataType + || dataType == MXCryptoSDKStoreKeyDataType } func hasKeyForData(ofType dataType: String) -> Bool { @@ -92,7 +96,10 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { case MXRoomLastMessageDataType: return keychainVault.objectExists(withKey: EncryptionKeyManager.roomLastMessageIv) && keychainVault.objectExists(withKey: EncryptionKeyManager.roomLastMessageAesKey) + case MXCryptoSDKStoreKeyDataType: + return keychainVault.objectExists(withKey: EncryptionKeyManager.cryptoSDKStoreKey) default: + MXLog.warning("[EncryptionKeyManager] hasKeyForData: No key for \(dataType)") return false } } @@ -118,7 +125,12 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { let aesKey = try? keychainVault.data(forKey: EncryptionKeyManager.roomLastMessageAesKey) { return MXAesKeyData(iv: ivKey, key: aesKey) } + case MXCryptoSDKStoreKeyDataType: + if let key = try? keychainVault.data(forKey: EncryptionKeyManager.cryptoSDKStoreKey) { + return MXRawDataKey(key: key) + } default: + MXLog.failure("[EncryptionKeyManager] keyDataForData: Attempting to get data for unknown type", dataType) return nil } return nil diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift new file mode 100644 index 000000000..a0ffea92f --- /dev/null +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -0,0 +1,62 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +final class PushRulesUpdater { + private var cancellables: Set = .init() + private var rules: [NotificationPushRuleType] = [] + private let notificationSettingsService: NotificationSettingsServiceType + + init(notificationSettingsService: NotificationSettingsServiceType) { + self.notificationSettingsService = notificationSettingsService + + notificationSettingsService + .rulesPublisher + .weakAssign(to: \.rules, on: self) + .store(in: &cancellables) + } + + func syncRulesIfNeeded() async { + await withTaskGroup(of: Void.self) { [rules, notificationSettingsService] group in + for rule in rules { + guard let ruleId = rule.pushRuleId else { + continue + } + + let relatedRules = ruleId.syncedRules(in: rules) + + for relatedRule in relatedRules { + guard rule.hasSameContentOf(relatedRule) == false else { + continue + } + + group.addTask { + try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, + enabled: rule.enabled, + actions: rule.ruleActions) + } + } + } + } + } +} + +private extension NotificationPushRuleType { + func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool? { + enabled == otherRule.enabled && ruleActions == otherRule.ruleActions + } +} diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 3553d6865..27c9551b7 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -205,6 +205,10 @@ final class RiotSettings: NSObject { /// Flag indicating if the voice broadcast feature is enabled @UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults) var enableVoiceBroadcast + + /// Flag indicating if we are using rust-based `MatrixCryptoSDK` instead of `MatrixSDK`'s internal crypto module + @UserDefault(key: "enableCryptoSDK", defaultValue: false, storage: defaults) + var enableCryptoSDK // MARK: Calls @@ -279,8 +283,8 @@ final class RiotSettings: NSObject { var roomScreenEnableMessageBubbles var roomTimelineStyleIdentifier: RoomTimelineStyleIdentifier { -// return .bubble // bwi: on by default because there is no toggle in the settings - return self.roomScreenEnableMessageBubbles ? .bubble : .plain + return .bubble // bwi: on by default because there is no toggle in the settings + // return self.roomScreenEnableMessageBubbles ? .bubble : .plain } /// A setting used to display the latest known display name and avatar in the timeline @@ -471,11 +475,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults) var lastNumberOfTrackedSpaces: Int? - // MARK: - All Chats Onboarding - - @UserDefault(key: "allChatsOnboardingHasBeenDisplayed", defaultValue: false, storage: defaults) - var allChatsOnboardingHasBeenDisplayed - } // MARK: - RiotSettings notification constants diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 99ac03e33..8527fdccf 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -169,14 +169,8 @@ class DarkTheme: NSObject, Theme { searchBar.backgroundImage = UIImage() // Remove top and bottom shadow searchBar.tintColor = self.tintColor - if #available(iOS 13.0, *) { - searchBar.searchTextField.backgroundColor = self.searchBackgroundColor - searchBar.searchTextField.textColor = self.searchPlaceholderColor - } else { - if let searchBarTextField = searchBar.vc_searchTextField { - searchBarTextField.textColor = self.searchPlaceholderColor - } - } + searchBar.searchTextField.backgroundColor = self.searchBackgroundColor + searchBar.searchTextField.textColor = self.searchPlaceholderColor } func applyStyle(onTextField texField: UITextField) { diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index b1af3622e..8babf1f75 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -177,14 +177,8 @@ class DefaultTheme: NSObject, Theme { return } - if #available(iOS 13.0, *) { - searchBar.searchTextField.backgroundColor = self.searchBackgroundColor - searchBar.searchTextField.textColor = self.searchPlaceholderColor - } else { - if let searchBarTextField = searchBar.vc_searchTextField { - searchBarTextField.textColor = self.searchPlaceholderColor - } - } + searchBar.searchTextField.backgroundColor = self.searchBackgroundColor + searchBar.searchTextField.textColor = self.searchPlaceholderColor } func applyStyle(onTextField texField: UITextField) { diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift index 210f394d9..41514fdb0 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -36,19 +36,21 @@ final class HomeserverConfigurationBuilder: NSObject { vectorWellKnownJitsiConfiguration = self.getJitsiConfiguration(from: vectorWellKnown) } + // bwi: (#4218) the well-known defaults for bwi are different than the element defaults. E2EE decryption is true instead of false, presharingmode is .whenEnteringRoom instead of .whenTyping and our backup method is only .passphrase instead of all + // Encryption configuration // Enable E2EE by default when there is no value let isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true // Disable mandatory secure backup when there is no value - let isSecureBackupRequired = vectorWellKnownEncryptionConfiguration?.isSecureBackupRequired ?? false + let isSecureBackupRequired = vectorWellKnownEncryptionConfiguration?.isSecureBackupRequired ?? true // Default to `MXKKeyPreSharingWhenTyping` when there is no value - let outboundKeysPreSharingMode = vectorWellKnownEncryptionConfiguration?.outboundKeysPreSharingMode ?? .whenTyping + let outboundKeysPreSharingMode = vectorWellKnownEncryptionConfiguration?.outboundKeysPreSharingMode ?? .whenEnteringRoom // Defaults to all secure backup methods available when there is no value let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod] if let backupSetupMethods = vectorWellKnownEncryptionConfiguration?.secureBackupSetupMethods { - secureBackupSetupMethods = backupSetupMethods.isEmpty ? VectorWellKnownBackupSetupMethod.allCases : backupSetupMethods + secureBackupSetupMethods = backupSetupMethods.isEmpty ? [.passphrase] : backupSetupMethods } else { - secureBackupSetupMethods = VectorWellKnownBackupSetupMethod.allCases + secureBackupSetupMethods = [.passphrase] } let encryptionConfiguration = HomeserverEncryptionConfiguration(isE2EEByDefaultEnabled: isE2EEByDefaultEnabled, diff --git a/Riot/Model/Room/RoomPreviewData.m b/Riot/Model/Room/RoomPreviewData.m index f619344ed..af751ab04 100644 --- a/Riot/Model/Room/RoomPreviewData.m +++ b/Riot/Model/Room/RoomPreviewData.m @@ -123,7 +123,7 @@ [self.roomDataSource finalizeInitialization]; self.roomDataSource.markTimelineInitialEvent = YES; - self->_roomName = peekingRoom.summary.displayname; + self->_roomName = peekingRoom.summary.displayName; self->_roomAvatarUrl = peekingRoom.summary.avatar; self->_roomTopic = [MXTools stripNewlineCharacters:peekingRoom.summary.topic];; diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index b722b4606..5e5b20ece 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -24,17 +24,8 @@ import AnalyticsEvents /// non-fatal issues and performance. `Analytics` class serves as a façade /// to all these use cases. /// -/// ## Creating Analytics Events -/// -/// Events are managed in a shared repo for all Element clients https://github.com/matrix-org/matrix-analytics-events -/// To add a new event create a PR to that repo with the new/updated schema. Element's Podfile has -/// a local version of the pod (commented out) for development purposes. -/// Once merged into `main`, follow the steps below to integrate the changes into the project: -/// 1. Check if `main` contains any source breaking changes to the events. If so, please -/// wait until you are ready to merge your work into element-ios. -/// 2. Merge `main` into the `release/swift` branch. -/// 3. Run `bundle exec pod update AnalyticsEvents` to update the pod. -/// 4. Make sure to commit `Podfile.lock` with the new commit hash. +/// Events are managed in a shared repo for all Element clients +/// https://github.com/matrix-org/matrix-analytics-events and integrated via SwiftPM /// @objcMembers class Analytics: NSObject { @@ -44,7 +35,7 @@ import AnalyticsEvents static let shared = Analytics() /// The analytics client to send events with. - private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() + private var client: AnalyticsClientProtocol = PostHogAnalyticsClient.shared /// The monitoring client to track crashes, issues and performance private var monitoringClient = SentryMonitoringClient() @@ -230,10 +221,10 @@ extension Analytics { /// /// Only non-nil properties will be updated when calling this method. func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil, allChatsActiveFilter: UserSessionProperties.AllChatsActiveFilter? = nil) { - let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, + let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName, + ftueUseCaseSelection: ftueUseCase?.analyticsName, numFavouriteRooms: numFavouriteRooms, - numSpaces: numSpaces, - allChatsActiveFilter: allChatsActiveFilter?.analyticsName) + numSpaces: numSpaces) client.updateUserProperties(userProperties) } @@ -281,7 +272,12 @@ extension Analytics { /// - reason: The error that occurred. /// - context: Additional context of the error that occured func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { - let event = AnalyticsEvent.Error(context: context, domain: .E2EE, name: reason.errorName) + let event = AnalyticsEvent.Error( + context: context, + cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, + domain: .E2EE, + name: reason.errorName + ) capture(event: event) } @@ -324,6 +320,11 @@ extension Analytics { viewRoomTrigger = .unknown capture(event: event) } + + func trackCryptoSDKEnabled() { + let event = AnalyticsEvent.CryptoSDKEnabled() + capture(event: event) + } } // MARK: - MXAnalyticsDelegate @@ -354,7 +355,7 @@ extension Analytics: MXAnalyticsDelegate { func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming) - let event = AnalyticsEvent.Error(context: nil, domain: .VOIP, name: reason.errorName) + let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, domain: .VOIP, name: reason.errorName) capture(event: callEvent) capture(event: event) } @@ -393,3 +394,14 @@ extension Analytics: MXAnalyticsDelegate { monitoringClient.trackNonFatalIssue(issue, details: details) } } + +/// iOS-specific analytics event triggered when users select the Crypto SDK labs option +/// +/// Due to this event being iOS only, and temporary during gradual rollout of Crypto SDK, +/// this event is not added into the shared analytics schema +extension AnalyticsEvent { + struct CryptoSDKEnabled: AnalyticsEventProtocol { + let eventName = "CryptoSDKEnabled" + let properties: [String: Any] = [:] + } +} diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 2f33f6369..bbb26fc4f 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -16,12 +16,10 @@ import AnalyticsEvents -/// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. @objc enum DecryptionFailureReason: Int { case unspecified case olmKeysNotSent case olmIndexError - case unexpected var errorName: AnalyticsEvent.Error.Name { switch self { @@ -31,8 +29,6 @@ import AnalyticsEvents return .OlmKeysNotSentError case .olmIndexError: return .OlmIndexError - case .unexpected: - return .UnknownError } } } @@ -47,10 +43,13 @@ import AnalyticsEvents let reason: DecryptionFailureReason /// Additional context of failure let context: String + // bwi: tracking information + let errorCode: NSInteger - init(failedEventId: String, reason: DecryptionFailureReason, context: String) { + init(failedEventId: String, reason: DecryptionFailureReason, context: String, errorCode: NSInteger) { self.failedEventId = failedEventId self.reason = reason self.context = context + self.errorCode = errorCode } } diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m index f17066060..f66a61006 100644 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.m @@ -106,12 +106,9 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; reason = DecryptionFailureReasonOlmIndexError; break; - case MXDecryptingErrorEncryptionNotEnabledCode: - case MXDecryptingErrorUnableToDecryptCode: - reason = DecryptionFailureReasonUnexpected; - break; - default: + // All other error codes will be tracked as `OlmUnspecifiedError` and will include `context` containing + // the actual error code and localized description reason = DecryptionFailureReasonUnspecified; break; } @@ -119,7 +116,8 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; NSString *context = [NSString stringWithFormat:@"code: %ld, description: %@", event.decryptionError.code, event.decryptionError.localizedDescription]; reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId reason:reason - context:context]; + context:context + errorCode:event.decryptionError.code]; } - (void)dispatch @@ -160,16 +158,18 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; for (DecryptionFailure *failure in failuresToTrack) { - failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); - //[self.delegate trackE2EEError:failure.reason context:failure.context]; + if ( failure.reason == DecryptionFailureReasonUnspecified) { + [BWIAnalytics.sharedTracker trackE2EEError:failure.errorCode]; + } else { + failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); + } } MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); for (NSNumber *reason in failuresCounts) { - // FRANK181 - //[self.delegate trackE2EEError:reason.integerValue count:failuresCounts[reason].integerValue]; + [BWIAnalytics.sharedTracker trackE2EEErrors:reason.integerValue count:failuresCounts[reason].integerValue]; } } } diff --git a/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift index 4b8911ce8..c60f35446 100644 --- a/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift +++ b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift @@ -21,6 +21,9 @@ extension __MXCallHangupReason { switch self { case .userHangup: return .VoipUserHangup + case .userBusy: + // There is no dedicated analytics event for `userBusy` error + return .UnknownError case .inviteTimeout: return .VoipInviteTimeout case .iceFailed: @@ -32,6 +35,9 @@ extension __MXCallHangupReason { case .unknownError: return .UnknownError default: + MXLog.failure("Unknown or unhandled hangup reason", context: [ + "reason": rawValue + ]) return .UnknownError } } diff --git a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift b/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift deleted file mode 100644 index e0d219279..000000000 --- a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import AnalyticsEvents - -extension AnalyticsEvent.UserProperties { - - // Initializer for Element. Strips all Web properties. - public init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?, allChatsActiveFilter: AllChatsActiveFilter?) { - self.init(WebMetaSpaceFavouritesEnabled: nil, - WebMetaSpaceHomeAllRooms: nil, - WebMetaSpaceHomeEnabled: nil, - WebMetaSpaceOrphansEnabled: nil, - WebMetaSpacePeopleEnabled: nil, - allChatsActiveFilter: allChatsActiveFilter, - ftueUseCaseSelection: ftueUseCaseSelection, - numFavouriteRooms: numFavouriteRooms, - numSpaces: numSpaces) - } - -} diff --git a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift index 6b27affea..ec49716be 100644 --- a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift +++ b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift @@ -25,6 +25,8 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { /// Any user properties to be included with the next captured event. private(set) var pendingUserProperties: AnalyticsEvent.UserProperties? + static let shared = PostHogAnalyticsClient() + var isRunning: Bool { postHog?.enabled ?? false } func start() { @@ -79,10 +81,10 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { } // Merge the updated user properties with the existing ones - self.pendingUserProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, + self.pendingUserProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter, + ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms, - numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces, - allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter) + numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces) } // MARK: - Private @@ -102,3 +104,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { return properties } } + +extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol { + func isFeatureEnabled(_ feature: String) -> Bool { + postHog?.isFeatureEnabled(feature) == true + } +} diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index ad17da025..013419e9c 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -15,6 +15,7 @@ limitations under the License. */ +import Combine import Foundation import Intents import MatrixSDK @@ -61,6 +62,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } private var currentSpaceId: String? + private var cancellables: Set = .init() + private var pushRulesUpdater: PushRulesUpdater? // MARK: Public @@ -82,9 +85,10 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // MARK: - Public methods func start() { - self.setupLogger() - self.setupTheme() - self.excludeAllItemsFromBackup() + setupLogger() + setupTheme() + excludeAllItemsFromBackup() + setupPushRulesSessionEvents() // Setup navigation router store _ = NavigationRouterStore.shared @@ -102,7 +106,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { if AppDelegate.theDelegate().isOffline { self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash")) } else { - self.splitViewCoordinator?.hideAppStateIndicator() + self.splitViewCoordinator?.hideAppStateIndicator() } } @@ -260,6 +264,47 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Reload split view with selected space id self.splitViewCoordinator?.start(with: spaceId) } + + private func setupPushRulesSessionEvents() { + let sessionReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .compactMap { $0.object as? MXSession } + .filter { $0.state == .running } + .removeDuplicates { session1, session2 in + session1 == session2 + } + + sessionReady + .sink { [weak self] session in + self?.setupPushRulesUpdater(session: session) + } + .store(in: &cancellables) + + + let sessionClosed = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .compactMap { $0.object as? MXSession } + .filter { $0.state == .closed } + + sessionClosed + .sink { [weak self] _ in + self?.pushRulesUpdater = nil + } + .store(in: &cancellables) + } + + private func setupPushRulesUpdater(session: MXSession) { + pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session)) + + let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() + let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher() + + needsCheckPublisher + .sink { _ in + Task { @MainActor [weak self] in + await self?.pushRulesUpdater?.syncRulesIfNeeded() + } + } + .store(in: &cancellables) + } } // MARK: - LegacyAppDelegateDelegate diff --git a/Riot/Modules/Application/AppDelegate.swift b/Riot/Modules/Application/AppDelegate.swift index 5ab09997b..3dcbfbf24 100644 --- a/Riot/Modules/Application/AppDelegate.swift +++ b/Riot/Modules/Application/AppDelegate.swift @@ -72,6 +72,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.legacyAppDelegate.window = window self.legacyAppDelegate.application(application, didFinishLaunchingWithOptions: launchOptions) + // bwi: set the tint color for the alert buttons and tableview cells + UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = ThemeService.shared().theme.tintColor + UITableViewCell.appearance().tintColor = ThemeService.shared().theme.tintColor + return true } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 1f229023a..c1803a4f8 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -634,6 +634,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Pause Voice Broadcast recording if needed [VoiceBroadcastRecorderProvider.shared pauseRecording]; + // Pause Voice Broadcast playing if needed + [VoiceBroadcastPlaybackProvider.shared pausePlayingInProgressVoiceBroadcast]; + // bwi: the app is really going into background self.isApplicationActiveFromSystemAlert = NO; } @@ -790,6 +793,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Riot has its own dark theme. Prevent iOS from applying its one [application keyWindow].accessibilityIgnoresInvertColors = YES; + [BWIAnalytics.sharedTracker firstCall]; [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"PinLogin"]; @@ -1018,6 +1022,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (UIAlertController*)showAlertWithTitle:(NSString*)title message:(NSString*)message { + // bwi: (#4174) handling of client errors that are shown when the backend is down + if ([title containsString:BWIBuildSettings.shared.clientErrorSearchTerm]) { + ServerDowntimeDefaultService *service = [[ServerDowntimeDefaultService alloc] init]; + + if ([service isDowntimeNow]) { + title = BWIL10n.standardErrorAlertTitleDowntime; + } else { + title = BWIL10n.standardErrorAlertTitleNoDowntime; + } + } + [_errorNotification dismissViewControllerAnimated:NO completion:nil]; _errorNotification = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; @@ -1853,7 +1868,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni matrixSessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXSession *mxSession = (MXSession*)notif.object; - [BWIAnalytics.sharedTracker trackEvent:@"initMatrixSessions" action:[NSString stringWithFormat:@"%ld", mxSession.state]]; + //[BWIAnalytics.sharedTracker trackEvent:@"initMatrixSessions" action:[NSString stringWithFormat:@"%ld", mxSession.state]]; // Check whether the concerned session is a new one if (mxSession.state == MXSessionStateInitialised) @@ -2078,7 +2093,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Do the one time check on device id [self checkDeviceId:mxSession]; - BWIAnalytics.sharedTracker.session = mxSession; + [BWIAnalytics.sharedTracker setSessionWithSession:mxSession]; [self.delegate legacyAppDelegate:self didAddMatrixSession:mxSession]; } @@ -2237,6 +2252,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [topVC startActivityIndicator]; } + [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker dispatchAll]; + [self logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) { if (completion) { @@ -2244,7 +2262,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { //[RiotSettings.shared reset]; [BWIBuildSettings.shared reset]; - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker resetUserdefaults]; } completion (YES); @@ -2315,6 +2333,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [topVC startActivityIndicator]; } + [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker dispatchAll]; + [self logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) { if (completion) { @@ -2322,7 +2343,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { //[RiotSettings.shared reset]; [BWIBuildSettings.shared reset]; - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker resetUserdefaults]; } completion (YES); @@ -2341,6 +2362,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; + // Reset Crypto SDK configuration (labs flag for which crypto module to use) + [CryptoSDKFeature.shared reset]; + // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; @@ -2374,9 +2398,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Logout all matrix account [[MXKAccountManager sharedManager] logoutWithCompletion:^{ - // We reset allChatsOnboardingHasBeenDisplayed flag on logout - RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = NO; - if (completion) { completion (YES); @@ -2417,7 +2438,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (mainSession) { - [BWIAnalytics.sharedTracker trackEvent:@"handleAppState" action:[MXTools readableSessionState:mainSession.state]]; + //[BWIAnalytics.sharedTracker trackEvent:@"handleAppState" action:[MXTools readableSessionState:mainSession.state]]; switch (mainSession.state) { @@ -2559,18 +2580,27 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] showLaunchAnimation"); UIView *launchLoadingView; - if (MXSDKOptions.sharedInstance.enableSyncProgress) + if (MXSDKOptions.sharedInstance.enableStartupProgress) { if (BWIBuildSettings.shared.showBUMLottieAnimation) { launchLoadingView = [BUMLaunchLoadingViewController makeView]; } else { - MXSession *mainSession = self.mxSessions.firstObject; - launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:mainSession.syncProgress]; + if (MXSDKOptions.sharedInstance.enableStartupProgress) + { + MXSession *mainSession = self.mxSessions.firstObject; + launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; + } + else + { + launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil]; + } + [(LaunchLoadingView *) launchLoadingView updateWithTheme:ThemeService.shared.theme]; } launchLoadingView.frame = window.bounds; launchLoadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [window addSubview:launchLoadingView]; } @@ -2593,7 +2623,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] hideLaunchAnimation: LaunchAnimation was shown for %.3fms", launchTaskProfile.duration * 1000); } - [BWIAnalytics.sharedTracker trackEvent:@"hideLaunchAnimation" action:@"Hide"]; + //[BWIAnalytics.sharedTracker trackEvent:@"hideLaunchAnimation" action:@"Hide"]; [self->launchAnimationContainerView removeFromSuperview]; self->launchAnimationContainerView = nil; } @@ -3957,7 +3987,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; } [self dismissKeyVerificationCoordinatorBridgePresenter]; - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Login"]; } - (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 79851d655..3ce727c48 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -29,7 +29,7 @@ struct AuthenticationCoordinatorParameters { /// A coordinator that handles authentication, verification and setting a PIN. final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtocol { - + enum EntryPoint { case registration case login @@ -111,6 +111,16 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } } + // bwi: (#4394) manually set present security in special cases + func setPresentSecurityScreens(present: Bool) { + isWaitingToPresentCompleteSecurity = present + } + + // bwi: (#4394) set session for the same case + func setSession(session: MXSession) { + self.session = session + } + // MARK: - Private /// Starts the authentication flow. @@ -130,27 +140,33 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } let flow: AuthenticationFlow = initialScreen == .login ? .login : .register - do { - // Start the flow using the default server (or a provisioning link if set). - try await authenticationService.startFlow(flow) - } catch { - MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") + + // bwi: force serverselection on test apps + if BWIBuildSettings.shared.authScreenShowTestServerOptions && BWIBuildSettings.shared.bwiLoginFlowLayout { showServerSelectionScreen(for: flow) - return - } - - switch initialScreen { - case .registration: - if authenticationService.state.homeserver.needsRegistrationFallback { - showFallback(for: flow) - } else { - showRegistrationScreen() + } else { + do { + // Start the flow using the default server (or a provisioning link if set). + try await authenticationService.startFlow(flow) + } catch { + MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") + showServerSelectionScreen(for: flow) + return } - case .login: - if authenticationService.state.homeserver.needsLoginFallback { - showFallback(for: flow) - } else { - showLoginScreen() + + switch initialScreen { + case .registration: + if authenticationService.state.homeserver.needsRegistrationFallback { + showFallback(for: flow) + } else { + showRegistrationScreen() + } + case .login: + if authenticationService.state.homeserver.needsLoginFallback { + showFallback(for: flow) + } else { + showLoginScreen() + } } } } @@ -618,8 +634,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc loadingViewController.modalPresentationStyle = .fullScreen navigationRouter.setRootModule(loadingViewController) } else { - let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil - let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress) + let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil + let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift index a606654f9..f3ace7833 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinatorProtocol.swift @@ -42,4 +42,10 @@ protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable { /// Indicates to the coordinator to display any pending screens if it was created with /// the `canPresentAdditionalScreens` parameter set to `false` func presentPendingScreensIfNecessary() + + // bwi: (#4394) manually set present security in special cases + func setPresentSecurityScreens(present: Bool) + + // bwi: (#4394) set session for the same case + func setSession(session: MXSession) } diff --git a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m index c16c1ff66..6da9da77f 100644 --- a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m @@ -1941,7 +1941,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void) bwiPresentRegisterInformationAlert { - // $$$ bwi_auth_register_alert_text does not exists in strings table UIAlertController *alert = [UIAlertController alertControllerWithTitle:BWIL10n.bwiAuthRegisterAlertTitle message:BWIL10n.bwiAuthRegisterAlertText preferredStyle:UIAlertControllerStyleAlert]; diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index 38fb3b931..5b13dc810 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -26,7 +26,7 @@ struct LegacyAuthenticationCoordinatorParameters { /// A coordinator that handles authentication, verification and setting a PIN using the old UIViewController flow for iOS 12 & 13. final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtocol { - + // MARK: - Properties // MARK: Private @@ -103,6 +103,16 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator } } + // bwi: (#4394) manually set present security in special cases + func setPresentSecurityScreens(present: Bool) { + isWaitingToPresentCompleteSecurity = present + } + + // bwi: (#4394) set session for the same case + func setSession(session: MXSession) { + self.session = session + } + // MARK: - Private private func showLoadingAnimation() { @@ -111,8 +121,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator loadingViewController.modalPresentationStyle = .fullScreen navigationRouter.setRootModule(loadingViewController) } else { - let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil - let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress) + let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil + let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index a8019c06c..3e8227e7c 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -484,7 +484,7 @@ CallAudioRouteMenuViewDelegate> else if (self.mxCall.room) { return [AvatarGenerator generateAvatarForMatrixItem:self.mxCall.room.roomId - withDisplayName:self.mxCall.room.summary.displayname + withDisplayName:self.mxCall.room.summary.displayName size:self.callerImageViewWidthConstraint.constant andFontSize:fontSize]; } diff --git a/Riot/Modules/Call/PiP/CallPiPView.swift b/Riot/Modules/Call/PiP/CallPiPView.swift index 4a553a3d0..97f06caf6 100644 --- a/Riot/Modules/Call/PiP/CallPiPView.swift +++ b/Riot/Modules/Call/PiP/CallPiPView.swift @@ -158,7 +158,7 @@ class CallPiPView: UIView { andFontSize: fontSize) } else if let room = call?.room { return AvatarGenerator.generateAvatar(forMatrixItem: room.roomId, - withDisplayName: room.summary.displayname, + withDisplayName: room.summary.displayName, size: imageView.bounds.width, andFontSize: fontSize) } diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 681c10c57..282507835 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -56,6 +56,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. NSTimer *publicRoomsTriggerTimer; + + // bwi: new feature banner + bool shouldHideFeatureBanner; } @property (nonatomic, strong, readwrite) RecentsDataSourceSections *sections; @@ -77,6 +80,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (instancetype)initWithMatrixSession:(MXSession *)mxSession recentsListService:(id)theRecentsListService { + // bwi: new feature banner + shouldHideFeatureBanner = TRUE; if (self = [super initWithMatrixSession:mxSession]) { processingQueue = dispatch_queue_create("RecentsDataSource", DISPATCH_QUEUE_SERIAL); @@ -101,6 +106,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [self registerAllChatsSettingsUpdateNotification]; self.allChatsFilterOptions = [AllChatsFilterOptions new]; + // bwi: new feature banner + [self registerFeatureBannerReadStatusChangedNotification]; + [self shouldShowFeatureBanner]; } return self; } @@ -109,6 +117,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { [self unregisterSpaceServiceDidBuildGraphNotification]; [self unregisterAllChatsSettingsUpdateNotification]; + // bwi: new feature banner + [self unregisterFeatureBannerNotification]; } #pragma mark - Properties @@ -169,7 +179,17 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } - (NSArray> *)breadcrumbsRoomCellDataArray { - return self.recentsListService.breadcrumbsRoomListData.rooms; + // bwi: for personal notes room + NSMutableArray> *filteredArray = [NSMutableArray arrayWithCapacity:self.recentsListService.breadcrumbsRoomListData.rooms.count]; + + for ( id object in self.recentsListService.breadcrumbsRoomListData.rooms) { + MXRoom* room = [self.mxSession roomWithRoomId:object.roomId]; + if (!room.isPersonalNotesRoom) { + [filteredArray addObject:object]; + } + } + + return filteredArray; } - (NSArray> *)allChatsRoomCellDataArray { @@ -211,6 +231,11 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (RecentsDataSourceSections *)makeDataSourceSections { NSMutableArray *types = [NSMutableArray array]; + // bwi: add new feature banner + if (!shouldHideFeatureBanner && BWIBuildSettings.shared.showTopBanner){ + [types addObject:@(RecentsDataSourceSectionTypeFeatureBanner)]; + } + if (self.recentsDataSourceMode == RecentsDataSourceModeRoomInvites) { [types addObject:@(RecentsDataSourceSectionTypeInvites)]; @@ -633,6 +658,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou NSUInteger count = 0; RecentsDataSourceSectionType sectionType = [self.sections sectionTypeForSectionIndex:section]; + // bwi: new feature banner + if (sectionType == RecentsDataSourceSectionTypeFeatureBanner) { + count = 1; + } if (sectionType == RecentsDataSourceSectionTypeCrossSigningBanner && self.crossSigningBannerDisplay != CrossSigningBannerDisplayNone) { count = 1; @@ -715,7 +744,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou (sectionType == RecentsDataSourceSectionTypeInvites && self.recentsDataSourceMode == RecentsDataSourceModeAllChats) || (sectionType == RecentsDataSourceSectionTypeAllChats && !self.allChatsFilterOptions.optionsCount) || (sectionType == RecentsDataSourceSectionTypeAllChats && self.currentSpace != nil && self.currentSpace.childRoomIds.count == 0) || - sectionType == RecentsDataSourceSectionTypePersonalNotes ) + sectionType == RecentsDataSourceSectionTypePersonalNotes || sectionType == RecentsDataSourceSectionTypeFeatureBanner) { return 0.0; } @@ -1149,7 +1178,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou RecentEmptySectionTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentEmptySpaceSectionTableViewCell defaultReuseIdentifier]]; tableViewCell.iconView.image = [ThemeService.shared isCurrentThemeDark] ? AssetImages.allChatsEmptySpaceArtworkDark.image : AssetImages.allChatsEmptySpaceArtwork.image; - tableViewCell.titleLabel.text = [VectorL10n allChatsEmptyViewTitle: self.currentSpace.summary.displayname]; + tableViewCell.titleLabel.text = [VectorL10n allChatsEmptyViewTitle: self.currentSpace.summary.displayName]; tableViewCell.messageLabel.text = VectorL10n.allChatsEmptySpaceInformation; return tableViewCell; @@ -1167,8 +1196,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { RecentsInvitesTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentsInvitesTableViewCell defaultReuseIdentifier]]; - tableViewCell.invitesCount = self.recentsListService.invitedRoomListData.counts.total.numberOfRooms; - + // bwi: fixes mismatched number of invites + tableViewCell.invitesCount = self.invitesCellDataArray.count; + return tableViewCell; } @@ -1721,7 +1751,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou NSString* tagOrder = [room.mxSession tagOrderToBeAtIndex:newPath.row from:oldPos withTag:dstRoomTag]; - MXLogDebug(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.roomId, room.summary.displayname, oldRoomTag, dstRoomTag, tagOrder); + MXLogDebug(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.roomId, room.summary.displayName, oldRoomTag, dstRoomTag, tagOrder); [room replaceTag:oldRoomTag byTag:dstRoomTag @@ -1900,4 +1930,63 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return NO; } +#pragma mark - bwi new feature banner + +- (void) shouldShowFeatureBanner +{ + if (BWIBuildSettings.shared.showTopBanner){ + MXSession* session = [self mxSession]; + if(!session) + return; + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + FeatureBannerVisibilityService *featureBannerService = [[FeatureBannerVisibilityService alloc] initWithMxSession:session]; + [featureBannerService isUnreadWithVersion:version completion:^(BOOL unread) { + if (unread) { + // this notification will be called either if the user clicked on the banner or wants to hide it + [[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.hide_top_banner" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { + [self markFeatureBannerAsRead]; + }]; + + // this notification will be called either if the user clicked on the banner or wants to hide it using a swipe gesture + [[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.mark_top_banner_as_read" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { + [self markFeatureBannerAsRead]; + }]; + + self->shouldHideFeatureBanner = FALSE; + } else { + self->shouldHideFeatureBanner = TRUE; + } + [self.delegate dataSource:self didCellChange:nil]; + }]; + } else { + self->shouldHideFeatureBanner = TRUE; + } +} + +- (void) registerFeatureBannerReadStatusChangedNotification +{ + [[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.mark_top_banner_as_unread" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { + [self shouldShowFeatureBanner]; + }]; +} + +- (void) markFeatureBannerAsRead +{ + MXSession* session = [self mxSession]; + if(!session) + return; + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + FeatureBannerVisibilityService *featureBannerService = [[FeatureBannerVisibilityService alloc] initWithMxSession:session]; + [featureBannerService markAsReadWithVersion:version]; + self->shouldHideFeatureBanner = TRUE; + [self.delegate dataSource:self didCellChange:nil]; +} + +- (void)unregisterFeatureBannerNotification +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"de.bwi.messenger.mark_top_banner_as_read" object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"de.bwi.messenger.hide_top_banner" object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"de.bwi.messenger.mark_top_banner_as_unread" object:nil]; +} + @end diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceSectionType.swift b/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceSectionType.swift index ec11a5471..d0a3aa9f1 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceSectionType.swift +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceSectionType.swift @@ -32,4 +32,5 @@ import Foundation case searchedRoom case allChats case unknown + case featureBanner } diff --git a/Riot/Modules/Common/Recents/RecentsBannerViewController.h b/Riot/Modules/Common/Recents/RecentsBannerViewController.h deleted file mode 100644 index 031b45985..000000000 --- a/Riot/Modules/Common/Recents/RecentsBannerViewController.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2021 BWI GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "RecentsViewController.h" - -@interface RecentsBannerViewController : RecentsViewController - -+ (bool)isBannerHidden; - -- (void)setupTopBanner; - -@end diff --git a/Riot/Modules/Common/Recents/RecentsBannerViewController.m b/Riot/Modules/Common/Recents/RecentsBannerViewController.m deleted file mode 100644 index a8259a972..000000000 --- a/Riot/Modules/Common/Recents/RecentsBannerViewController.m +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (c) 2021 BWI GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "RecentsBannerViewController.h" -#import "GeneratedInterface-Swift.h" - -@interface RecentsBannerViewController () -{ - TopBannerViewController *topBannerViewController; - bool bannersInitialized; - bool isBannerVisible; - bool shouldHideBanner; -} -- (void)setTopBannerViewConstraints; -- (void)showTopBanner; -- (void)hideTopBanner; -@end - -@implementation RecentsBannerViewController - -+ (bool)isBannerHidden { - NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; - NSString *version = [info objectForKey:@"CFBundleShortVersionString"]; - if( [version isEqual:[[NSUserDefaults standardUserDefaults] objectForKey:@"de.bwi.messenger.top_banner_confirmation"]] ) - return TRUE; - else - return FALSE; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - bannersInitialized = FALSE; - isBannerVisible = FALSE; - shouldHideBanner = FALSE; -} - -- (void)onMatrixSessionChange -{ - [super onMatrixSessionChange]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - - if( [BWIBuildSettings.shared showTopBanner] ) { - [self showTopBanner]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - if( bannersInitialized && [BWIBuildSettings.shared showTopBanner] ) { - [self hideTopBanner]; - } -} - -- (void)setTopBannerViewConstraints -{ - // get the offset in the y-coordiante so that the banner appears below the navigation bar - UIWindow *window = UIApplication.sharedApplication.windows.firstObject; - CGFloat statusBarHeight = window.safeAreaInsets.top; - CGFloat navigationBarHeight = self.navigationController.navigationBar.frame.size.height; - - NSLayoutConstraint *alignTopConstraint = [NSLayoutConstraint - constraintWithItem:topBannerViewController.view - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.view.safeAreaLayoutGuide - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:0]; - [[self view] addConstraint:alignTopConstraint]; - - NSLayoutConstraint *alignLeadingConstraint = [NSLayoutConstraint - constraintWithItem:topBannerViewController.view - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeLeading - multiplier:1.0 - constant:0]; - [[self view] addConstraint:alignLeadingConstraint]; - - NSLayoutConstraint *alignTrailingConstraint = [NSLayoutConstraint - constraintWithItem:topBannerViewController.view - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:self.view - attribute:NSLayoutAttributeTrailing - multiplier:1.0 - constant:0]; - [[self view] addConstraint:alignTrailingConstraint]; - - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint - constraintWithItem:topBannerViewController.view - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:NULL - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:130]; - [[self view] addConstraint:heightConstraint]; -} - -- (void)setupTopBanner -{ - NSMutableArray *bannerControllers = [NSMutableArray array]; - - MXSession* session = [self mainSession]; - if( !session || bannersInitialized) - return; - - NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - - FeatureBannerVisibilityService *service = [[FeatureBannerVisibilityService alloc] initWithMxSession:session]; - [service isUnreadWithVersion:version completion:^(BOOL unread) { - if (unread) { - // [bannerControllers addObject:[[FeatureBannerViewController alloc] initWithSession:session version:version]]; - - self->topBannerViewController = [[TopBannerViewController alloc] initWithControllers: bannerControllers]; - - // this notification will be called either if the user clicked on the banner or wants to hide it using a swipe gesture - [[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.hide_top_banner" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { - [self hideTopBanner]; - [self->topBannerViewController removeAdvertiseViewControllers]; - }]; - - // this notification will be called either if the user clicked on the banner or wants to hide it using a swipe gesture - [[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.mark_top_banner_as_read" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { - self->shouldHideBanner = TRUE; - }]; - - self->bannersInitialized = TRUE; - - if( [BWIBuildSettings.shared showTopBanner] ) { - [self showTopBanner]; - } - } - }]; - -} - -- (void)showTopBanner -{ - if( isBannerVisible || shouldHideBanner) { - return; // nothing to do because the banner is already visible for the user - } - - if( [topBannerViewController.advertiseViewControllers count] > 0 ) - { - // add child view controller - [self addChildViewController:topBannerViewController]; - [[topBannerViewController view] setTranslatesAutoresizingMaskIntoConstraints:FALSE]; - [[topBannerViewController view] setBounds:CGRectMake(0, 0, self.view.bounds.size.width, 100)]; - [[self view] addSubview:[topBannerViewController view]]; - [topBannerViewController didMoveToParentViewController:self]; - - [self setTopBannerViewConstraints]; - - isBannerVisible = TRUE; - } -} - -- (void)hideTopBanner -{ - if( [[topBannerViewController advertiseViewControllers] count] > 0 ) - { - // remove child view controller - [topBannerViewController willMoveToParentViewController:nil]; - [topBannerViewController.view removeFromSuperview]; - [topBannerViewController removeFromParentViewController]; - } - - isBannerVisible = FALSE; -} - -@end diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index 2e5d037af..86173513c 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -162,6 +162,11 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ - (void)startChat; +/** + Open the QR code scanner for scanning permalinks. + */ +- (void)scanPermalink; + /** Open screen to create a new room. */ diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 6bbc5c84f..e2217648c 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1917,6 +1917,28 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [self performSegueWithIdentifier:@"presentStartChat" sender:self]; } +- (void)scanPermalink { + AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if(authStatus == AVAuthorizationStatusDenied) { + UIAlertController* alert = [UIAlertController alertControllerWithTitle:BWIL10n.authenticationServerSelectionQrMissingAuthorizationTitle + message:BWIL10n.authenticationServerSelectionQrMissingAuthorizationMessage + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* primaryAction = [UIAlertAction actionWithTitle:VectorL10n.settingsTitle style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + [[UIApplication sharedApplication] openURL: [NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; + }]; + UIAlertAction* secondaryAction = [UIAlertAction actionWithTitle:VectorL10n.cancel style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) {}]; + [alert addAction:primaryAction]; + [alert addAction:secondaryAction]; + [self presentViewController:alert animated:YES completion:nil]; + } else { + UIViewController* viewController = [PermalinkQRCodeScannerController createFromSwiftUIView]; + [self presentViewController:viewController animated: YES completion: nil]; + } +} + - (void)createNewRoom { // Sanity check @@ -2656,6 +2678,11 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro editedRoomId = nil; } +-(void)roomContextActionServiceDidMarkRoom:(id)service +{ + [self refreshRecentsTable]; +} + #pragma mark - RecentCellContextMenuProviderDelegate - (void)recentCellContextMenuProviderDidStartShowingPreview:(RecentCellContextMenuProvider *)menuProvider diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift index 582612e0d..d823c2bff 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift @@ -95,7 +95,7 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { } else if i % 11 == 0 { room.dataTypes = .serverNotice } - room.displayname = "Room \(i+1)" + room.displayName = "Room \(i+1)" if let event = MXEvent(fromJSON: [ "event_id": MXTools.generateTransactionId() as Any, "room_id": room.roomId, diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift index 3503282ab..88596c75f 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift @@ -26,7 +26,7 @@ public class MockRoomSummary: NSObject, MXRoomSummaryProtocol { public var avatar: String? - public var displayname: String? + public var displayName: String? public var topic: String? diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index ea2be2eae..c98d5606e 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -69,6 +69,7 @@ self.missedNotifAndUnreadIndicator.hidden = YES; self.missedNotifAndUnreadBadgeBgView.hidden = YES; self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; + self.missedNotifAndUnreadBadgeLabel.text = @""; roomCellData = (id)cellData; if (roomCellData) @@ -91,10 +92,15 @@ self.lastEventDecriptionLabelTrailingConstraint.constant = self.unsentImageView.hidden ? 10 : 30; // Notify unreads and bing - if (roomCellData.hasUnread) + if (roomCellData.isRoomMarkedAsUnread) + { + self.missedNotifAndUnreadBadgeBgView.hidden = NO; + self.missedNotifAndUnreadBadgeBgView.backgroundColor = ThemeService.shared.theme.tintColor; + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 20; + } + else if (roomCellData.hasUnread) { self.missedNotifAndUnreadIndicator.hidden = NO; - if (0 < roomCellData.notificationCount) { if( [BWIBuildSettings.shared showMentionsInRoom] ) { diff --git a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift index 0fe8d1fee..43a3e3398 100644 --- a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift +++ b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift @@ -93,7 +93,7 @@ class AllChatsFilterOptionListView: UIView, Themable { backgroundColor = theme.colors.background.withAlphaComponent(0.7) tabListView.itemFont = theme.fonts.calloutSB - tabListView.tintColor = theme.colors.accent + tabListView.tintColor = ThemeService.shared().theme.tintColor tabListView.unselectedItemColor = theme.colors.tertiaryContent separator.backgroundColor = theme.colors.system diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 4381ee27e..cae9cc66c 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -215,8 +215,19 @@ self->filteredMatrixContacts = [NSMutableArray arrayWithCapacity:userSearchResponse.results.count]; + NSArray *sortedArray; + + if (BWIBuildSettings.shared.sortUserSearchResultsAlphabetically) { + sortedArray = [userSearchResponse.results sortedArrayUsingComparator:^NSComparisonResult(MXUser *a, MXUser *b) { + return [a.displayname caseInsensitiveCompare:b.displayname]; + }]; + } else { + sortedArray = userSearchResponse.results; + } + + // Keep the response order as the hs ordered users by relevance - for (MXUser *mxUser in userSearchResponse.results) + for (MXUser *mxUser in sortedArray) { MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:mxUser.displayname andMatrixID:mxUser.userId]; [self->filteredMatrixContacts addObject:contact]; diff --git a/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m b/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m index 22036093c..ce6ee4399 100644 --- a/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m +++ b/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m @@ -48,7 +48,7 @@ { [room.summary setRoomAvatarImageIn:self.avatarImageView]; - self.titleLabel.text = room.summary.displayname; + self.titleLabel.text = room.summary.displayName; } + (CGFloat)cellHeight diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift index bd46a7e8e..fb4db2c1d 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift @@ -22,6 +22,7 @@ enum AllChatsEditActionProviderOption { case createRoom case startChat case createSpace + case scanPermalink } protocol AllChatsEditActionProviderDelegate: AnyObject { @@ -40,7 +41,7 @@ class AllChatsEditActionProvider { private var rootSpaceCount: Int = 0 private var parentSpace: MXSpace? { didSet { - parentName = parentSpace?.summary?.displayname ?? VectorL10n.spaceTag + parentName = parentSpace?.summary?.displayName ?? VectorL10n.spaceTag } } private var parentName: String = VectorL10n.spaceTag @@ -50,29 +51,57 @@ class AllChatsEditActionProvider { // MARK: - RoomActionProviderProtocol var menu: UIMenu { - guard parentSpace != nil else { - var createActions = [ - self.createRoomAction, - self.startChatAction - ] - if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces { - createActions.insert(self.createSpaceAction, at: 0) + if BWIBuildSettings.shared.allowScanPermalinkQRCode { + guard parentSpace != nil else { + var createActions = [ + self.exploreRoomsAction, + self.createRoomAction, + self.startChatAction + ] + if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces { + createActions.insert(self.createSpaceAction, at: 0) + } + return UIMenu(title: "", children: [ + self.scanPermalinkAction, + UIMenu(title: "", options: .displayInline, children: createActions) + ]) } + return UIMenu(title: "", children: [ - self.exploreRoomsAction, - UIMenu(title: "", options: .displayInline, children: createActions) + UIMenu(title: "", options: .displayInline, children: [ + self.scanPermalinkAction + ]), + UIMenu(title: "", options: .displayInline, children: [ + self.exploreRoomsAction, + self.createSpaceAction, + self.createRoomAction + ]) + ]) + } else { + guard parentSpace != nil else { + var createActions = [ + self.createRoomAction, + self.startChatAction + ] + if rootSpaceCount > 0 && BWIBuildSettings.shared.enableSpaces { + createActions.insert(self.createSpaceAction, at: 0) + } + return UIMenu(title: "", children: [ + self.exploreRoomsAction, + UIMenu(title: "", options: .displayInline, children: createActions) + ]) + } + + return UIMenu(title: "", children: [ + UIMenu(title: "", options: .displayInline, children: [ + self.exploreRoomsAction + ]), + UIMenu(title: "", options: .displayInline, children: [ + self.createSpaceAction, + self.createRoomAction + ]) ]) } - - return UIMenu(title: "", children: [ - UIMenu(title: "", options: .displayInline, children: [ - self.exploreRoomsAction - ]), - UIMenu(title: "", options: .displayInline, children: [ - self.createSpaceAction, - self.createRoomAction - ]) - ]) } // MARK: - Public @@ -167,4 +196,14 @@ class AllChatsEditActionProvider { self.delegate?.allChatsEditActionProvider(self, didSelect: .createSpace) } } + + private var scanPermalinkAction: UIAction { + UIAction(title: BWIL10n.roomRecentsScanQrCode, + image: Asset.Images.qrcodeViewfinder.image) { [weak self] action in + guard let self = self else { return } + + self.delegate?.allChatsEditActionProvider(self, didSelect: .scanPermalink) + } + } + } diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift index 0d9e3d2fa..1d13e26de 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift @@ -39,7 +39,7 @@ class AllChatsSpaceActionProvider { private var currentSpace: MXSpace? { didSet { - spaceName = currentSpace?.summary?.displayname ?? VectorL10n.spaceTag + spaceName = currentSpace?.summary?.displayName ?? VectorL10n.spaceTag } } private var spaceName: String = VectorL10n.spaceTag diff --git a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift index 29ac1f854..631103713 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift @@ -48,13 +48,15 @@ class RoomActionProvider: RoomActionProviderProtocol { ]) } } else { - return UIMenu(children: [ + var children = service.hasUnread ? [self.markAsReadAction] : [self.markAsUnreadAction] + children.append(contentsOf: [ self.directChatAction, self.notificationsAction, self.favouriteAction, self.lowPriorityAction, self.leaveAction ]) + return UIMenu(children: children) } } else { if service.roomMembership == .invite { @@ -126,6 +128,14 @@ class RoomActionProvider: RoomActionProviderProtocol { self.service.markAsRead() } } + private var markAsUnreadAction: UIAction { + return UIAction( + title: VectorL10n.homeContextMenuMarkAsUnread, + image: UIImage(systemName: "envelope.badge")) { [weak self] action in + guard let self = self else { return } + self.service.markAsUnread() + } + } private var leaveAction: UIAction { let image = UIImage(systemName: "rectangle.righthalf.inset.fill.arrow.right") diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift index ecc36b4bc..72a38e906 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift @@ -38,7 +38,7 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { self.room = room self.delegate = delegate self.isRoomJoined = room.summary?.isJoined ?? false - self.hasUnread = room.summary?.hasAnyUnread ?? false + self.hasUnread = (room.summary?.hasAnyUnread ?? false) || room.isMarkedAsUnread self.roomMembership = room.summary?.membership ?? .unknown self.session = room.mxSession self.unownedRoomService = UnownedRoomContextActionService(roomId: room.roomId, canonicalAlias: room.summary?.aliases?.first, session: self.session, delegate: delegate) @@ -108,6 +108,11 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { func markAsRead() { room.markAllAsRead() + self.delegate?.roomContextActionServiceDidMarkRoom(self) + } + func markAsUnread() { + room.setUnread() + self.delegate?.roomContextActionServiceDidMarkRoom(self) } // MARK: - Private diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift index d44213bd4..25c66773f 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift @@ -22,6 +22,7 @@ import Foundation func roomContextActionService(_ service: RoomContextActionServiceProtocol, showRoomNotificationSettingsForRoomWithId roomId: String) func roomContextActionServiceDidJoinRoom(_ service: RoomContextActionServiceProtocol) func roomContextActionServiceDidLeaveRoom(_ service: RoomContextActionServiceProtocol) + func roomContextActionServiceDidMarkRoom(_ service: RoomContextActionServiceProtocol) } /// `RoomContextActionServiceProtocol` classes are meant to be called by a `RoomActionProviderProtocol` instance so it provides the implementation of the menu actions. diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift index d1d3f08af..7b545b1df 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift @@ -430,7 +430,7 @@ extension EnterNewRoomDetailsViewController: UITableViewDataSource { cell.detailTextLabel?.textColor = theme.textSecondaryColor cell.backgroundColor = theme.backgroundColor cell.contentView.backgroundColor = .clear - cell.tintColor = theme.tintColor + cell.tintColor = ThemeService.shared().theme.tintColor return cell case .avatar(let image): let cell: ChooseAvatarTableViewCell = tableView.dequeueReusableCell(for: indexPath) diff --git a/Riot/Modules/Favorites/FavouritesViewController.h b/Riot/Modules/Favorites/FavouritesViewController.h index a9a68a4b2..3be388271 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.h +++ b/Riot/Modules/Favorites/FavouritesViewController.h @@ -15,12 +15,12 @@ limitations under the License. */ -#import "RecentsBannerViewController.h" +#import "RecentsViewController.h" /** The `FavouritesViewController` screen is the view controller displayed when `Favourites` tab is selected. */ -@interface FavouritesViewController : RecentsBannerViewController +@interface FavouritesViewController : RecentsViewController + (instancetype)instantiate; diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index b77185d10..de5dafaef 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -100,7 +100,7 @@ MXRoom *room = [searchDataSource.mxSession roomWithRoomId:roomId]; if (room) { - roomDisplayName = room.summary.displayname; + roomDisplayName = room.summary.displayName; if (!roomDisplayName.length) { roomDisplayName = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m index 561b0aac5..0e5a0312b 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m @@ -28,9 +28,7 @@ [super customizeTableViewCellRendering]; self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; - + [self updateUserNameColor]; } @@ -43,7 +41,7 @@ MXRoom* room = [bubbleData.mxSession roomWithRoomId:bubbleData.roomId]; if (room) { - self.roomNameLabel.text = room.summary.displayname; + self.roomNameLabel.text = room.summary.displayName; if (!self.roomNameLabel.text.length) { self.roomNameLabel.text = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m index 40818f8a0..6b68de2fa 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m @@ -30,8 +30,6 @@ [self updateUserNameColor]; self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData @@ -43,7 +41,7 @@ MXRoom* room = [bubbleData.mxSession roomWithRoomId:bubbleData.roomId]; if (room) { - self.roomNameLabel.text = room.summary.displayname; + self.roomNameLabel.text = room.summary.displayName; } else { diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index ef1e46101..ef84d8f75 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -154,6 +154,8 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { }) } } + + self.bwiCheckForMatomoPromt() } func bwiOnUnlockedByPin() { @@ -702,6 +704,13 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { service.createPersonalNotesRoomIfNeeded() } } + + // bwi: check if matomo promt was shown for this session + private func bwiCheckForMatomoPromt() { + if BWIBuildSettings.shared.bwiMatomoEnabled && BWIAnalytics.sharedTracker.needsToShowPromt() { + self.allChatsViewController.bwiPresentMatomoConsentAlert() + } + } } extension AllChatsCoordinator: SignOutFlowPresenterDelegate { @@ -724,6 +733,7 @@ extension AllChatsCoordinator: SignOutFlowPresenterDelegate { extension AllChatsCoordinator: AllChatsViewControllerDelegate { func allChatsViewControllerDidCompleteAuthentication(_ allChatsViewController: AllChatsViewController) { self.delegate?.splitViewMasterCoordinatorDidCompleteAuthentication(self) + self.bwiCheckForMatomoPromt() } func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomWithParameters roomNavigationParameters: RoomNavigationParameters, completion: @escaping () -> Void) { diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index a317e6f31..1b69b5497 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -61,6 +61,9 @@ class AllChatsViewController: HomeViewController { private let reviewSessionAlertSnoozeController = ReviewSessionAlertSnoozeController() + // bwi: new feature banner height + private var featureBannerViewHeight: CGFloat = 0 + private var bannerView: UIView? { didSet { bannerView?.translatesAutoresizingMaskIntoConstraints = false @@ -70,8 +73,6 @@ class AllChatsViewController: HomeViewController { private var isOnboardingCoordinatorPreparing: Bool = false - private var allChatsOnboardingCoordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter? - private var theme: Theme { ThemeService.shared().theme } @@ -125,10 +126,14 @@ class AllChatsViewController: HomeViewController { recentsTableView.register(RecentEmptySectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySectionTableViewCell.reuseIdentifier) recentsTableView.register(RecentEmptySpaceSectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySpaceSectionTableViewCell.reuseIdentifier) recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier) + recentsTableView.register(FeatureBannerViewCell.self, forCellReuseIdentifier: "featureBanner") recentsTableView.contentInsetAdjustmentBehavior = .automatic toolbarHeight = toolbar.frame.height emptyViewBottomAnchor = toolbar.topAnchor + + // bwi: 4179 + toolbar.tintColor = ThemeService.shared().theme.tintColor updateUI() @@ -162,7 +167,9 @@ class AllChatsViewController: HomeViewController { // Check whether we're not logged in let authIsShown: Bool - if MXKAccountManager.shared().accounts.isEmpty { + + // bwi: (#4394) handling of the case of canceled login during pincode stage. + if MXKAccountManager.shared().accounts.isEmpty || !PinCodePreferences.shared.isPinSet && PinCodePreferences.shared.forcePinProtection { showOnboardingFlow() authIsShown = true } else { @@ -182,10 +189,7 @@ class AllChatsViewController: HomeViewController { AppDelegate.theDelegate().checkAppVersion() - // bwi: we don't want to show element specific onboarding screens - if BuildSettings.newAppLayoutEnabled && !RiotSettings.shared.allChatsOnboardingHasBeenDisplayed && BWIBuildSettings.shared.shouldShowAllChatsOnboarding { - self.showAllChatsOnboardingScreen() - } + } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -203,7 +207,7 @@ class AllChatsViewController: HomeViewController { searchController.isActive = false guard let spaceId = spaceId else { - self.dataSource?.currentSpace = nil + dataSource?.currentSpace = nil updateUI() return @@ -214,7 +218,7 @@ class AllChatsViewController: HomeViewController { return } - self.dataSource.currentSpace = space + dataSource?.currentSpace = space updateUI() self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) @@ -284,7 +288,8 @@ class AllChatsViewController: HomeViewController { RecentsDataSourceSectionType.serverNotice.rawValue, RecentsDataSourceSectionType.suggestedRooms.rawValue, RecentsDataSourceSectionType.breadcrumbs.rawValue, - RecentsDataSourceSectionType.personalNotes.rawValue + RecentsDataSourceSectionType.personalNotes.rawValue, + RecentsDataSourceSectionType.featureBanner.rawValue ] } @@ -292,11 +297,42 @@ class AllChatsViewController: HomeViewController { super.startActivityIndicator() } + func bwiPresentMatomoConsentAlert() { + let alert = UIAlertController(title: BWIL10n.bwiAnalyticsAlertTitle, + message: BWIL10n.bwiAnalyticsAlertBody(AppInfo.current.displayName), + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertInfoButton, + style: .default, + handler: { action in + if let webviewController = WebViewViewController(url: BWIBuildSettings.shared.applicationPrivacyPolicyUrlString) { + webviewController.title = VectorL10n.settingsPrivacyPolicy + + self.present(webviewController, animated: true) + } + })) + + alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertCancelButton, + style: .default, + handler: { action in + BWIAnalytics.sharedTracker.running = false + })) + + alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertOkButton, + style: .cancel, + handler: { action in + BWIAnalytics.sharedTracker.running = true + BWIAnalytics.sharedTracker.trackBwiValue(0, "General", "ConsentGiven", "popup") + })) + + self.present(alert, animated: true) + } + // MARK: - Actions @objc private func showSpaceSelectorAction(sender: AnyObject) { Analytics.shared.viewRoomTrigger = .roomList - let currentSpaceId = self.dataSource.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId + let currentSpaceId = dataSource?.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true) spaceSelectorBridgePresenter.present(from: self, animated: true) spaceSelectorBridgePresenter.delegate = self @@ -317,35 +353,58 @@ class AllChatsViewController: HomeViewController { guard let sectionType = sectionType(forSectionAt: section), sectionType == .invites else { return super.tableView(tableView, numberOfRowsInSection: section) } + if sectionType == .featureBanner { + return 1 + } - return dataSource.tableView(tableView, numberOfRowsInSection: section) + return dataSource?.tableView(tableView, numberOfRowsInSection: section) ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { return super.tableView(tableView, cellForRowAt: indexPath) } - + // bwi: feature banner cell + if sectionType == .featureBanner { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "featureBanner", for: indexPath) as? FeatureBannerViewCell else { + return UITableViewCell() + } + cell.selectionStyle = .none + cell.setupView(parent: self, rootView: FeatureBannerView(delegate: cell)) + featureBannerViewHeight = cell.calculateHeight() + return cell + } + guard let dataSource = dataSource else { + MXLog.failure("Missing data source") + return UITableViewCell() + } return dataSource.tableView(tableView, cellForRowAt: indexPath) } // MARK: - UITableViewDelegate override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { return super.tableView(tableView, heightForRowAt: indexPath) } - - return dataSource.cellHeight(at: indexPath) + if sectionType == .featureBanner { + return featureBannerViewHeight + } else { + return dataSource?.cellHeight(at: indexPath) ?? 0 + } } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else { + guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites || sectionType == .featureBanner else { super.tableView(tableView, didSelectRowAt: indexPath) return } - - showRoomInviteList() + if sectionType == .invites { + showRoomInviteList() + } else { + // bwi: feature banner is not selectable + return + } } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { @@ -419,7 +478,7 @@ class AllChatsViewController: HomeViewController { let title: String let informationText: String if let currentSpace = self.dataSource?.currentSpace { - title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayname ?? VectorL10n.spaceTag) + title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayName ?? VectorL10n.spaceTag) informationText = VectorL10n.allChatsEmptySpaceInformation } else { let myUser = mainSession.myUser @@ -499,7 +558,7 @@ class AllChatsViewController: HomeViewController { private func updateUI() { let currentSpace = self.dataSource?.currentSpace - self.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle + self.title = currentSpace?.summary?.displayName ?? VectorL10n.allChatsTitle setupEditOptions() updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in @@ -552,16 +611,20 @@ class AllChatsViewController: HomeViewController { self.isToolbarHidden = false self.update(with: theme) + // bwi: 4179 + var allChatsEditButton = UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) + allChatsEditButton.tintColor = ThemeService.shared().theme.tintColor + if BWIBuildSettings.shared.enableSpaces { self.toolbar.items = [ spacesButton, UIBarButtonItem.flexibleSpace(), - UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) + allChatsEditButton ] } else { self.toolbar.items = [ UIBarButtonItem.flexibleSpace(), - UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) + allChatsEditButton ] } @@ -599,7 +662,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceInvite() { - guard let session = mainSession, let spaceRoom = dataSource.currentSpace?.room else { + guard let session = mainSession, let spaceRoom = dataSource?.currentSpace?.room else { return } @@ -611,7 +674,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceMembers() { - guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else { + guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } @@ -625,7 +688,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceSettings() { - guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else { + guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } @@ -646,11 +709,11 @@ class AllChatsViewController: HomeViewController { } private func showLeaveSpace() { - guard let session = mainSession, let spaceSummary = dataSource.currentSpace?.summary else { + guard let session = mainSession, let spaceSummary = dataSource?.currentSpace?.summary else { return } - let name = spaceSummary.displayname ?? VectorL10n.spaceTag + let name = spaceSummary.displayName ?? VectorL10n.spaceTag let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, @@ -684,20 +747,6 @@ class AllChatsViewController: HomeViewController { self.navigationController?.pushViewController(invitesViewController, animated: true) } - private func showAllChatsOnboardingScreen() { - let allChatsOnboardingCoordinatorBridgePresenter = AllChatsOnboardingCoordinatorBridgePresenter() - allChatsOnboardingCoordinatorBridgePresenter.completion = { [weak self] in - RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = true - - guard let self = self else { return } - self.allChatsOnboardingCoordinatorBridgePresenter?.dismiss(animated: true, completion: { - self.allChatsOnboardingCoordinatorBridgePresenter = nil - }) - } - - allChatsOnboardingCoordinatorBridgePresenter.present(from: self, animated: true) - self.allChatsOnboardingCoordinatorBridgePresenter = allChatsOnboardingCoordinatorBridgePresenter - } } private extension AllChatsViewController { @@ -744,11 +793,11 @@ extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePrese extension AllChatsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let searchText = searchController.searchBar.text, !searchText.isEmpty else { - self.dataSource.search(withPatterns: nil) + self.dataSource?.search(withPatterns: nil) return } - self.dataSource.search(withPatterns: [searchText]) + self.dataSource?.search(withPatterns: [searchText]) } } @@ -784,7 +833,9 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { case .startChat: startChat() case .createSpace: - showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) + showCreateSpace(parentSpaceId: dataSource?.currentSpace?.spaceId) + case .scanPermalink: + scanPermalink() } } @@ -895,10 +946,12 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { return } - let devices = mainSession.crypto.devices(forUser: mainSession.myUserId).values - let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) - if userHasOneUnverifiedDevice { - presentReviewUnverifiedSessionsAlert(with: session) + if let userId = mainSession.myUserId, let crypto = mainSession.crypto { + let devices = crypto.devices(forUser: userId).values + let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) + if userHasOneUnverifiedDevice { + presentReviewUnverifiedSessionsAlert(with: session) + } } } @@ -1006,7 +1059,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { func bwiOnUnlockedByPin() { // bwi specific - self.setupTopBanner() + } // MARK: - Private diff --git a/Riot/Modules/Home/HomeViewController.h b/Riot/Modules/Home/HomeViewController.h index 0343e397c..842b79eb9 100644 --- a/Riot/Modules/Home/HomeViewController.h +++ b/Riot/Modules/Home/HomeViewController.h @@ -15,13 +15,13 @@ limitations under the License. */ -#import "RecentsBannerViewController.h" +#import "RecentsViewController.h" #import "RecentsDataSource.h" /** The `HomeViewController` screen is the main app screen. */ -@interface HomeViewController : RecentsBannerViewController +@interface HomeViewController : RecentsViewController + (instancetype)instantiate; diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index d178e9cc4..cf379f4fa 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -398,6 +398,7 @@ || (sectionType == RecentsDataSourceSectionTypePeople && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms) || (sectionType == RecentsDataSourceSectionTypeSecureBackupBanner) || (sectionType == RecentsDataSourceSectionTypeCrossSigningBanner) + || (sectionType == RecentsDataSourceSectionTypePersonalNotes) ) { return [recentsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; @@ -501,7 +502,8 @@ RecentsDataSourceSectionType sectionType = [recentsDataSource.sections sectionTypeForSectionIndex:indexPath.section]; if ((sectionType == RecentsDataSourceSectionTypeConversation && !recentsDataSource.recentsListService.conversationRoomListData.counts.numberOfRooms) - || (sectionType == RecentsDataSourceSectionTypePeople && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms)) + || (sectionType == RecentsDataSourceSectionTypePeople && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms) + || (sectionType == RecentsDataSourceSectionTypePersonalNotes)) { return [recentsDataSource cellHeightAtIndexPath:indexPath]; } @@ -580,7 +582,7 @@ { [self showCrossSigningSetup]; } - else if (sectionType == RecentsDataSourceSectionTypeAllChats) + else if (sectionType == RecentsDataSourceSectionTypeAllChats || sectionType == RecentsDataSourceSectionTypePersonalNotes) { [super tableView:tableView didSelectRowAtIndexPath:indexPath]; } diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m index c8fa03fdd..39552e4f0 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m @@ -272,7 +272,7 @@ static NSString * _Nonnull kJitsiFeatureFlagChatEnabled = @"chat.enabled"; builder.room = conferenceId; builder.videoMuted = !self.startWithVideo; - builder.subject = roomSummary.displayname; + builder.subject = roomSummary.displayName; builder.userInfo = [[JitsiMeetUserInfo alloc] initWithDisplayName:userDisplayName andEmail:nil andAvatar:avatarUrl]; diff --git a/Riot/Modules/KeyBackup/Recover/Passphrase/KeyBackupRecoverFromPassphraseViewController.swift b/Riot/Modules/KeyBackup/Recover/Passphrase/KeyBackupRecoverFromPassphraseViewController.swift index 3f4a8dec5..c185869cd 100644 --- a/Riot/Modules/KeyBackup/Recover/Passphrase/KeyBackupRecoverFromPassphraseViewController.swift +++ b/Riot/Modules/KeyBackup/Recover/Passphrase/KeyBackupRecoverFromPassphraseViewController.swift @@ -159,6 +159,15 @@ final class KeyBackupRecoverFromPassphraseViewController: UIViewController { self.recoverButton.isEnabled = self.viewModel.isFormValid } + // bwi: MESSENGER-3948 + private func updateShowHidePassphraseButton() { + if self.passphraseTextField.isSecureTextEntry { + passphraseVisibilityButton.setImage(Asset.Images.revealPasswordButton.image, for: .normal) + } else { + passphraseVisibilityButton.setImage(Asset.Images.hidePasswordButton.image, for: .normal) + } + } + private func render(viewState: KeyBackupRecoverFromPassphraseViewState) { switch viewState { case .loading: @@ -199,6 +208,7 @@ final class KeyBackupRecoverFromPassphraseViewController: UIViewController { @IBAction private func passphraseVisibilityButtonAction(_ sender: Any) { self.passphraseTextField.isSecureTextEntry = !self.passphraseTextField.isSecureTextEntry + self.updateShowHidePassphraseButton() // bwi: MESSENGER-3948 } @objc private func passphraseTextFieldDidChange(_ textField: UITextField) { diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard index 1c8ba341c..42e99205e 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard @@ -1,25 +1,23 @@ - - - - + + - + - + - + - + @@ -40,15 +38,24 @@ + + + + @@ -72,6 +79,7 @@ + @@ -79,10 +87,10 @@ - + diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift index a716ec0c6..e78a61351 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift @@ -29,6 +29,7 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { @IBOutlet private weak var shieldImageView: UIImageView! @IBOutlet private weak var informationLabel: UILabel! + @IBOutlet private weak var progressLabel: UILabel! // MARK: Private @@ -118,8 +119,8 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { private func render(viewState: KeyBackupRecoverFromPrivateKeyViewState) { switch viewState { - case .loading: - self.renderLoading() + case .loading(let progress): + self.renderLoading(progress: progress) case .loaded: self.renderLoaded() case .error(let error): @@ -127,8 +128,11 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { } } - private func renderLoading() { + private func renderLoading(progress: Double) { self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + + let percent = Int(round(progress * 100)) + self.progressLabel.text = VectorL10n.keyBackupRecoverFromPrivateKeyProgress("\(percent)") } private func renderLoaded() { diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift index cef1d7c0c..04fb48850 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift @@ -27,6 +27,7 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private let keyBackup: MXKeyBackup private var currentHTTPOperation: MXHTTPOperation? private let keyBackupVersion: MXKeyBackupVersion + private var progressUpdateTimer: Timer? // MARK: Public @@ -56,7 +57,14 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private func recoverWithPrivateKey() { - self.update(viewState: .loading) + self.update(viewState: .loading(0)) + + // Update loading progress every second until no longer loading + progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + if let progress = self?.keyBackup.importProgress { + self?.update(viewState: .loading(progress.fractionCompleted)) + } + } self.currentHTTPOperation = keyBackup.restore(usingPrivateKeyKeyBackup: keyBackupVersion, room: nil, session: nil, success: { [weak self] (_, _) in guard let self = self else { @@ -91,6 +99,11 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate } private func update(viewState: KeyBackupRecoverFromPrivateKeyViewState) { + if case .loading = viewState {} else { + progressUpdateTimer?.invalidate() + progressUpdateTimer = nil + } + self.viewDelegate?.keyBackupRecoverFromPrivateKeyViewModel(self, didUpdateViewState: viewState) } } diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift index bdd417853..b4ef05fb9 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift @@ -20,7 +20,7 @@ import Foundation /// KeyBackupRecoverFromPrivateKeyViewController view state enum KeyBackupRecoverFromPrivateKeyViewState { - case loading + case loading(Double) case loaded case error(Error) } diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift index 555c392c1..0b7622a54 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift @@ -264,6 +264,9 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } private func showVerifyBySAS(transaction: MXSASTransaction, animated: Bool) { + if navigationRouter.modules.last is KeyVerificationVerifyBySASCoordinator { + return + } let coordinator = KeyVerificationVerifyBySASCoordinator(session: self.session, transaction: transaction, verificationKind: self.verificationKind) coordinator.delegate = self coordinator.start() diff --git a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard index b6b94a838..f353aaa83 100644 --- a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard +++ b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard @@ -20,26 +20,26 @@ - + - + - - + + - - + + - + + - + + - @@ -142,10 +143,8 @@ - - diff --git a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.swift b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.swift index 2eb478574..4b32cec18 100644 --- a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.swift +++ b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.swift @@ -173,7 +173,6 @@ final class KeyVerificationScanConfirmationViewController: UIViewController { } } - self.title = viewData.verificationKind.verificationTitle self.titleLabel.text = title self.waitingLabel.text = waitingInfo self.scannedInformationLabel.text = scannedInfo diff --git a/Riot/Modules/KeyVerification/Common/Verified/KeyVerificationVerifiedViewController.storyboard b/Riot/Modules/KeyVerification/Common/Verified/KeyVerificationVerifiedViewController.storyboard index bc069c0d7..20c347d74 100644 --- a/Riot/Modules/KeyVerification/Common/Verified/KeyVerificationVerifiedViewController.storyboard +++ b/Riot/Modules/KeyVerification/Common/Verified/KeyVerificationVerifiedViewController.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -13,41 +11,41 @@ - + - + - + - + - + - - - - - - + + + @@ -157,16 +145,16 @@ Use the latest Riot on your other devices: - - - + - + + + - + @@ -200,7 +188,6 @@ Use the latest Riot on your other devices: - @@ -210,6 +197,7 @@ Use the latest Riot on your other devices: + diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift index 665d3d274..b68a8ef89 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift @@ -30,13 +30,12 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { // MARK: Outlets + @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var informationLabel: UILabel! @IBOutlet private weak var desktopClientImageView: UIImageView! @IBOutlet private weak var mobileClientImageView: UIImageView! - - @IBOutlet private weak var additionalInformationLabel: UILabel! - + @IBOutlet private weak var recoverSecretsAvailabilityLoadingContainerView: UIView! @IBOutlet private weak var recoverSecretsAvailabilityLoadingLabel: UILabel! @IBOutlet private weak var recoverSecretsAvailabilityActivityIndicatorView: UIActivityIndicatorView! @@ -70,7 +69,6 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. - self.setupViews() self.activityPresenter = ActivityIndicatorPresenter() self.errorPresenter = MXKErrorAlertPresentation() @@ -96,11 +94,11 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { if let navigationBar = self.navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } - - self.informationLabel.textColor = theme.textPrimaryColor + + self.titleLabel.textColor = theme.textPrimaryColor + self.informationLabel.textColor = theme.textSecondaryColor self.desktopClientImageView.tintColor = theme.tintColor self.mobileClientImageView.tintColor = theme.tintColor - self.additionalInformationLabel.textColor = theme.textPrimaryColor self.recoverSecretsAvailabilityLoadingLabel.textColor = theme.textSecondaryColor self.recoverSecretsAvailabilityActivityIndicatorView.color = theme.tintColor } @@ -126,14 +124,13 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { } self.title = BWIL10n.deviceVerificationSelfVerifyWaitTitle - + self.titleLabel.text = BWIL10n.deviceVerificationSelfVerifyWaitTitle + self.informationLabel.text = BWIL10n.deviceVerificationSelfVerifyWaitInformation self.desktopClientImageView.image = Asset.Images.monitor.image.withRenderingMode(.alwaysTemplate) self.mobileClientImageView.image = Asset.Images.smartphone.image.withRenderingMode(.alwaysTemplate) - self.additionalInformationLabel.text = nil // bwi: hidden text - self.recoverSecretsAdditionalInformationLabel.text = BWIL10n.deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation } @@ -217,7 +214,10 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { private func render(error: Error) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) - self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + // bwi: don't show unneccessary error alert when user only has one device -> he is able to use passphrase + if BWIBuildSettings.shared.showNoOtherDeviceError { + self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + } } // MARK: - Actions diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift index b064d4f84..2f1b8a2ce 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift @@ -26,11 +26,19 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai private let session: MXSession private let keyVerificationService: KeyVerificationService - private let verificationManager: MXKeyVerificationManager + private let verificationManager: MXKeyVerificationManager? private let isNewSignIn: Bool - private var secretsRecoveryAvailability: SecretsRecoveryAvailability + private var secretsRecoveryAvailability: SecretsRecoveryAvailability? private var keyVerificationRequest: MXKeyVerificationRequest? + private var myUserId: String { + guard let userId = session.myUserId else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] userId is missing") + return "" + } + return userId + } + // MARK: Public weak var viewDelegate: KeyVerificationSelfVerifyWaitViewModelViewDelegate? @@ -40,10 +48,10 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai init(session: MXSession, isNewSignIn: Bool) { self.session = session - self.verificationManager = session.crypto.keyVerificationManager + self.verificationManager = session.crypto?.keyVerificationManager self.keyVerificationService = KeyVerificationService() self.isNewSignIn = isNewSignIn - self.secretsRecoveryAvailability = session.crypto.recoveryService.vc_availability + self.secretsRecoveryAvailability = session.crypto?.recoveryService.vc_availability } deinit { @@ -59,9 +67,16 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai case .cancel: self.cancel() case .recoverSecrets: - switch self.secretsRecoveryAvailability { + guard let availability = secretsRecoveryAvailability else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] process: secretsRecoveryAvailability not set") + self.cancel() + return + } + + switch availability { case .notAvailable: - fatalError("Should not happen: When recovery is not available button is hidden") + MXLog.error("Should not happen: When recovery is not available button is hidden") + self.cancel() case .available(let secretsRecoveryMode): self.coordinatorDelegate?.keyVerificationSelfVerifyWaitViewModel(self, wantsToRecoverSecretsWith: secretsRecoveryMode) } @@ -71,12 +86,16 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai // MARK: - Private private func loadData() { + guard let verificationManager = verificationManager else { + MXLog.failure("Verification manager is not set") + return + } if !self.isNewSignIn { MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices") let keyVerificationService = KeyVerificationService() - self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in + verificationManager.requestVerificationByToDevice(withUserId: self.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in guard let self = self else { return } @@ -103,7 +122,7 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting") let keyVerificationService = KeyVerificationService() - self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in + verificationManager.requestVerificationByToDevice(withUserId: self.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in guard let self = self else { return } @@ -132,12 +151,18 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai } private func continueLoadData() { + guard let verificationManager = verificationManager, let recoveryService = session.crypto?.recoveryService else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] continueLoadData: Missing dependencies") + return + } + // update availability again - self.secretsRecoveryAvailability = session.crypto.recoveryService.vc_availability + let availability = recoveryService.vc_availability + self.secretsRecoveryAvailability = availability - let viewData = KeyVerificationSelfVerifyWaitViewData(isNewSignIn: self.isNewSignIn, secretsRecoveryAvailability: self.secretsRecoveryAvailability) + let viewData = KeyVerificationSelfVerifyWaitViewData(isNewSignIn: self.isNewSignIn, secretsRecoveryAvailability: availability) - self.registerKeyVerificationManagerNewRequestNotification(for: self.verificationManager) + self.registerKeyVerificationManagerNewRequestNotification(for: verificationManager) self.update(viewState: .loaded(viewData)) self.registerTransactionDidStateChangeNotification() self.registerKeyVerificationRequestChangeNotification() @@ -251,7 +276,7 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai @objc private func transactionDidStateChange(notification: Notification) { guard let sasTransaction = notification.object as? MXSASTransaction, - sasTransaction.isIncoming, sasTransaction.otherUserId == self.session.myUserId else { + sasTransaction.isIncoming, sasTransaction.otherUserId == self.myUserId else { return } self.sasTransactionDidStateChange(sasTransaction) @@ -260,7 +285,8 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai private func sasTransactionDidStateChange(_ transaction: MXSASTransaction) { switch transaction.state { case MXSASTransactionStateIncomingShowAccept: - transaction.accept() + // The transaction will be automatically accepted by the MXKeyVerificationManager when the SAS start event is handled + break case MXSASTransactionStateShowSAS: self.unregisterTransactionDidStateChangeNotification() self.coordinatorDelegate?.keyVerificationSelfVerifyWaitViewModel(self, didAcceptIncomingSASTransaction: transaction) diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift index 55f3aff05..18d6add9d 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift @@ -41,9 +41,9 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { // MARK: - Setup - static func instantiate(syncProgress: MXSessionSyncProgress?) -> LaunchLoadingView { + static func instantiate(startupProgress: MXSessionStartupProgress?) -> LaunchLoadingView { let view = LaunchLoadingView.loadFromNib() - syncProgress?.delegate = view + startupProgress?.delegate = view return view } @@ -54,7 +54,7 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { animationTimeline.play() self.animationTimeline = animationTimeline - self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableSyncProgress + self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableStartupProgress } // MARK: - Public @@ -65,21 +65,35 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { } } -extension LaunchLoadingView: MXSessionSyncProgressDelegate { - func sessionDidUpdateSyncState(_ state: MXSessionSyncState) { - guard MXSDKOptions.sharedInstance().enableSyncProgress else { +extension LaunchLoadingView: MXSessionStartupProgressDelegate { + func sessionDidUpdateStartupStage(_ stage: MXSessionStartupStage) { + guard MXSDKOptions.sharedInstance().enableStartupProgress else { + return + } + updateStatusText(for: stage) + + } + + private func updateStatusText(for stage: MXSessionStartupStage) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.updateStatusText(for: stage) + } return } // Sync may be doing a lot of heavy work on the main thread and the status text // does not update reliably enough without explicitly refreshing CATransaction.begin() - statusLabel.text = statusText(for: state) + statusLabel.text = statusText(for: stage) CATransaction.commit() } - private func statusText(for state: MXSessionSyncState) -> String { - switch state { + private func statusText(for stage: MXSessionStartupStage) -> String { + switch stage { + case .migratingData(let progress): + let percent = Int(floor(progress * 100)) + return VectorL10n.launchLoadingMigratingData("\(percent)") case .serverSyncing(let attempts): if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) { return VectorL10n.launchLoadingServerSyncingNthAttempt(nth) diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift index 1da229b79..ef9630dda 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift @@ -21,10 +21,10 @@ class LaunchLoadingViewController: UIViewController, Reusable { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - init(syncProgress: MXSessionSyncProgress?) { + init(startupProgress: MXSessionStartupProgress?) { super.init(nibName: "LaunchLoadingViewController", bundle: nil) - let launchLoadingView = LaunchLoadingView.instantiate(syncProgress: syncProgress) + let launchLoadingView = LaunchLoadingView.instantiate(startupProgress: startupProgress) launchLoadingView.update(theme: ThemeService.shared().theme) view.vc_addSubViewMatchingParent(launchLoadingView) diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index 5fb222493..d51bdcac4 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -43,6 +43,7 @@ class LocationManager: NSObject { private let locationManager: CLLocationManager private var authorizationHandler: LocationAuthorizationHandler? + private var authorizationReturnedSinceRequestingAlways = false // MARK: Public @@ -144,14 +145,16 @@ class LocationManager: NSObject { // See https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?changes=_6_6 private func tryToRequestAlwaysAuthorization(handler: @escaping LocationAuthorizationHandler) { self.authorizationHandler = handler + self.authorizationReturnedSinceRequestingAlways = false + self.locationManager.delegate = self self.locationManager.requestAlwaysAuthorization() Timer.scheduledTimer(withTimeInterval: Constants.waitForAuthorizationStatusDelay, repeats: false) { [weak self] _ in - guard let self = self else { + guard let self = self, !self.authorizationReturnedSinceRequestingAlways else { return } - self.authorizationRequestDidComplete(with: self.locationManager.authorizationStatus) + self.authorizationAlwaysRequestDidComplete(with: self.locationManager.authorizationStatus) } } @@ -174,8 +177,7 @@ class LocationManager: NSObject { return status } - - private func authorizationRequestDidComplete(with status: CLAuthorizationStatus) { + private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { return } @@ -191,7 +193,14 @@ extension LocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = self.locationManager.authorizationStatus - self.authorizationRequestDidComplete(with: status) + authorizationReturnedSinceRequestingAlways = true + if status == .authorizedAlways { + // LocationManager can call locationManagerDidChangeAuthorization multiple times. + // For example it calls it at initialisation of LocationManager manager and we are also seeing it called + // after requestAlwaysAuthorization but before the user has actually selected on option on the prompt. + // Therefore we should only call `authorizationAlwaysRequestDidComplete` once on the success of authorizedAlways being granted. + self.authorizationAlwaysRequestDidComplete(with: status) + } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { @@ -217,3 +226,19 @@ extension LocationManager: CLLocationManagerDelegate { MXLog.error("[LocationManager] Did failed", context: error) } } + +extension CLLocationManager { + func requestAuthorizationIfNeeded() -> Bool { + switch authorizationStatus { + case .notDetermined: + requestWhenInUseAuthorization() + return false + case .restricted, .denied: + return false + case .authorizedAlways, .authorizedWhenInUse, .authorized: + return true + @unknown default: + return false + } + } +} diff --git a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m index bd92e520b..620c4bcbd 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m @@ -508,7 +508,7 @@ static const CGFloat kLocalPreviewMargin = 20; } else if (mxCall.isConferenceCall) { - peerDisplayName = mxCall.room.summary.displayname; + peerDisplayName = mxCall.room.summary.displayName; peerAvatarURL = mxCall.room.summary.avatar; } diff --git a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m index 1524eb2d7..7007c12bf 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m @@ -193,7 +193,7 @@ MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID]; if (pushRule) { - [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled]; + [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled completion:nil]; } } } diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 5e7a0dfcc..70cc2f71a 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -954,9 +954,10 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; if (clearStore) { - // Force a reload of device keys at the next session start. + // Force a reload of device keys at the next session start, unless we are just about to migrate + // all data and device keys into CryptoSDK. // This will fix potential UISIs other peoples receive for our messages. - if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK) { [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; } diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index ed523160e..5442a0f2a 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -199,7 +199,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringCallHangup, kMXEventTypeStringSticker, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringPollEnd, + kMXEventTypeStringPollEndMSC3381 ].mutableCopy; _messageDetailsAllowSharing = YES; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 9928892f2..87aabe50b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -572,6 +572,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; Once complete, this local echo will be replaced by the event saved by the homeserver. @param audioFileLocalURL the local filesystem path of the audio file to send. + @param additionalContentParams (optional) the additional parameters to the content. @param mimeType (optional) the mime type of the file. Defaults to `audio/ogg` @param duration the length of the voice message in milliseconds @param samples an array of floating point values normalized to [0, 1], boxed within NSNumbers @@ -580,6 +581,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param failure A block object called when the operation fails. */ - (void)sendVoiceMessage:(NSURL *)audioFileLocalURL + additionalContentParams:(NSDictionary*)additionalContentParams mimeType:mimeType duration:(NSUInteger)duration samples:(NSArray *)samples diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index d8f55bf14..0998122ae 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1998,6 +1998,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } - (void)sendVoiceMessage:(NSURL *)audioFileLocalURL + additionalContentParams:(NSDictionary *)additionalContentParams mimeType:mimeType duration:(NSUInteger)duration samples:(NSArray *)samples @@ -2006,7 +2007,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendVoiceMessage:audioFileLocalURL mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; + [_room sendVoiceMessage:audioFileLocalURL additionalContentParams:additionalContentParams mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; if (localEchoEvent) { @@ -2149,7 +2150,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } @@ -2176,7 +2180,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { NSURL *localFileURL = [NSURL URLWithString:localFilePath]; if (![NSFileManager.defaultManager fileExistsAtPath:localFilePath]) { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidLocalFilePath userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidLocalFilePath userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend voice message, invalid file path.", self); return; } @@ -2185,10 +2192,20 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self removeEventWithEventId:eventId]; if (event.isVoiceMessage) { + // Voice message NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; - - [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; + + // Additional content params in case it is a voicebroacast chunk + NSDictionary* additionalContentParams = nil; + if (event.content[kMXEventRelationRelatesToKey] != nil && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) { + additionalContentParams = @{ + kMXEventRelationRelatesToKey: event.content[kMXEventRelationRelatesToKey], + VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] + }; + } + + [self sendVoiceMessage:localFileURL additionalContentParams:additionalContentParams mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; } else { [self sendAudioFile:localFileURL mimeType:mimetype success:success failure:failure]; } @@ -2236,7 +2253,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } @@ -2248,13 +2268,19 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] MXKRoomDataSource: Warning - Only resend of MXEventTypeRoomMessage is allowed. Event.type: %@", self, event.type); } } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m index 181552e53..b0b9bb7ad 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m @@ -26,10 +26,21 @@ */ NSMutableDictionary *roomDataSources; + /** + The list of rooms with a "late decryption" event. Causing bubbles issues + Each element is a room ID. + */ + NSMutableSet *roomDataSourcesToDestroy; + /** Observe UIApplicationDidReceiveMemoryWarningNotification to dispose of any resources that can be recreated. */ id UIApplicationDidReceiveMemoryWarningNotificationObserver; + + /** + Observe kMXEventDidDecryptNotification to get late decrypted events. + */ + id mxEventDidDecryptNotificationObserver; } @end @@ -119,6 +130,7 @@ static Class _roomDataSourceClass; { mxSession = matrixSession; roomDataSources = [NSMutableDictionary dictionary]; + roomDataSourcesToDestroy = [NSMutableSet set]; _releasePolicy = MXKRoomDataSourceManagerReleasePolicyNeverRelease; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionDidLeaveRoom:) name:kMXSessionDidLeaveRoomNotification object:nil]; @@ -138,6 +150,12 @@ static Class _roomDataSourceClass; } }]; + + // Observe late decrypted events, and store rooms ids in memory + mxEventDidDecryptNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXEventDidDecryptNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + MXEvent *decryptedEvent = notif.object; + [self->roomDataSourcesToDestroy addObject:decryptedEvent.roomId]; + }]; } return self; } @@ -156,6 +174,11 @@ static Class _roomDataSourceClass; [[NSNotificationCenter defaultCenter] removeObserver:UIApplicationDidReceiveMemoryWarningNotificationObserver]; UIApplicationDidReceiveMemoryWarningNotificationObserver = nil; } + if (mxEventDidDecryptNotificationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver]; + mxEventDidDecryptNotificationObserver = nil; + } } #pragma mark @@ -202,9 +225,19 @@ static Class _roomDataSourceClass; // If not available yet, create the room data source MXKRoomDataSource *roomDataSource = roomDataSources[roomId]; - + + // check if the room's dataSource has events with late decryption issues and destroys it + BOOL roomDataSourceToBeDestroyed = [roomDataSourcesToDestroy containsObject:roomId]; + + if (roomDataSource && roomDataSourceToBeDestroyed && create) { + [roomDataSource destroy]; + roomDataSources[roomId] = nil; + roomDataSource = nil; + } + if (!roomDataSource && create && roomId) { + [roomDataSourcesToDestroy removeObject:roomId]; [_roomDataSourceClass loadRoomDataSourceWithRoomId:roomId threadId:nil andMatrixSession:mxSession onComplete:^(id roomDataSource) { [self addRoomDataSource:roomDataSource]; onComplete(roomDataSource); diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift index f22c779fd..197096e86 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift @@ -18,34 +18,38 @@ import Foundation class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalizerProtocol { func senderSentAnImage() -> String { - return VectorL10n.messageReplyToSenderSentAnImage + VectorL10n.messageReplyToSenderSentAnImage } func senderSentAVideo() -> String { - return VectorL10n.messageReplyToSenderSentAVideo + VectorL10n.messageReplyToSenderSentAVideo } func senderSentAnAudioFile() -> String { - return VectorL10n.messageReplyToSenderSentAnAudioFile + VectorL10n.messageReplyToSenderSentAnAudioFile } func senderSentAVoiceMessage() -> String { - return VectorL10n.messageReplyToSenderSentAVoiceMessage + VectorL10n.messageReplyToSenderSentAVoiceMessage } func senderSentAFile() -> String { - return VectorL10n.messageReplyToSenderSentAFile + VectorL10n.messageReplyToSenderSentAFile } func senderSentTheirLocation() -> String { - return VectorL10n.messageReplyToSenderSentTheirLocation + VectorL10n.messageReplyToSenderSentTheirLocation } func senderSentTheirLiveLocation() -> String { - return VectorL10n.messageReplyToSenderSentTheirLiveLocation + VectorL10n.messageReplyToSenderSentTheirLiveLocation } func messageToReplyToPrefix() -> String { - return VectorL10n.messageReplyToMessageToReplyToPrefix + VectorL10n.messageReplyToMessageToReplyToPrefix + } + + func endedPollMessage() -> String { + VectorL10n.pollTimelineReplyEndedPoll } } diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m index 958e316fa..f4f0be833 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m @@ -66,6 +66,11 @@ return (roomSummary.localUnreadEventCount != 0); } +- (BOOL)isRoomMarkedAsUnread +{ + return [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId];; +} + - (NSString *)roomIdentifier { if (self.isSuggestedRoom) @@ -81,7 +86,7 @@ { return self.roomSummary.spaceChildInfo.displayName; } - return roomSummary.displayname; + return roomSummary.displayName; } - (NSString *)avatarUrl diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h index 7185ae4eb..3c417c1fa 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h @@ -50,6 +50,7 @@ @property (nonatomic, readonly) NSString *lastEventDate; @property (nonatomic, readonly) BOOL hasUnread; +@property (nonatomic, readonly) BOOL isRoomMarkedAsUnread; @property (nonatomic, readonly) NSUInteger notificationCount; @property (nonatomic, readonly) NSUInteger highlightCount; @property (nonatomic, readonly) NSString *notificationCountStringValue; diff --git a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m index d1daa83ad..a5e76b03a 100644 --- a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m +++ b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m @@ -45,7 +45,7 @@ MXRoom *room = [searchDataSource.mxSession roomWithRoomId:searchResult.result.roomId]; if (room) { - title = room.summary.displayname; + title = room.summary.displayName; } else { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h index 776b79927..999da7034 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h @@ -366,6 +366,12 @@ typedef enum : NSUInteger { */ @property (nonatomic) UIColor *sendingTextColor; +/** + Color used to display links and hyperlinks contentt. + Default is [UIColor linkColor]. + */ +@property (nonatomic) UIColor *linksColor; + /** Color used to display error text. Default is red. diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 21be58eb1..189eea1bd 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -31,6 +31,7 @@ #import "GeneratedInterface-Swift.h" static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; +static NSString *const kRepliedTextPattern = @".*

.*
(.*)
"; @interface MXKEventFormatter () { @@ -89,6 +90,7 @@ static NSString *const kHTMLATagRegexPattern = @"( _encryptingTextColor = [UIColor lightGrayColor]; _sendingTextColor = [UIColor lightGrayColor]; _errorTextColor = [UIColor redColor]; + _linksColor = [UIColor linkColor]; _htmlBlockquoteBorderColor = [MXKTools colorWithRGBValue:0xDDDDDD]; _defaultTextFont = [UIFont systemFontOfSize:14]; @@ -738,7 +740,7 @@ static NSString *const kHTMLATagRegexPattern = @"( } else { - displayText = [VectorL10n noticeRoomLeave:targetDisplayName]; + displayText = [BWIL10n noticeRoomLeave:targetDisplayName]; } } } @@ -1051,8 +1053,22 @@ static NSString *const kHTMLATagRegexPattern = @"( else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode) { - // Make the unknown inbound session id error description more user friendly - errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + // Hide the decryption error for VoiceBroadcast chunks + BOOL isVoiceBroadcastChunk = NO; + if ([event.relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { + MXEvent *startEvent = [mxSession.store eventWithEventId:event.relatesTo.eventId + inRoom:event.roomId]; + + if (startEvent) { + isVoiceBroadcastChunk = (startEvent.eventType == MXEventTypeCustom && [startEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]); + } + } + if (isVoiceBroadcastChunk) { + displayText = nil; + } else { + // Make the unknown inbound session id error description more user friendly + errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + } } else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorDuplicateMessageIndexCode) @@ -1749,6 +1765,7 @@ static NSString *const kHTMLATagRegexPattern = @"( if (url.URL) { [str addAttribute:NSLinkAttributeName value:url.URL range:matchRange]; + [str addAttribute:NSForegroundColorAttributeName value:self.linksColor range:matchRange]; } } } @@ -1806,6 +1823,7 @@ static NSString *const kHTMLATagRegexPattern = @"( } html = [self renderReplyTo:html withRoomState:roomState]; + html = [self renderPollEndedReplyTo:html repliedEvent:repliedEvent]; } // Apply the css style that corresponds to the event state @@ -1878,6 +1896,12 @@ static NSString *const kHTMLATagRegexPattern = @"( { MXJSONModelSetString(repliedEventContent, repliedEvent.content[kMXMessageBodyKey]); } + if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollStart) { + repliedEventContent = [MXEventContentPollStart modelFromJSON:repliedEvent.content].question; + } + if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollEnd) { + repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.endedPollMessage; + } } // No message content in a non-redacted event. Formatter should use fallback. @@ -2012,6 +2036,44 @@ static NSString *const kHTMLATagRegexPattern = @"( return html; } +- (NSString*)renderPollEndedReplyTo:(NSString*)htmlString repliedEvent:(MXEvent*)repliedEvent { + static NSRegularExpression *endedPollRegex; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kRepliedTextPattern options:NSRegularExpressionCaseInsensitive error:nil]; + }); + + NSString* finalString = htmlString; + + if (repliedEvent.eventType != MXEventTypePollEnd) { + return finalString; + } + + NSTextCheckingResult* match = [endedPollRegex firstMatchInString:htmlString options:0 range:NSMakeRange(0, htmlString.length)]; + + if (!(match && match.numberOfRanges > 1)) { + // no useful match found + return finalString; + } + + NSRange groupRange = [match rangeAtIndex:1]; + NSString* replacementText; + + if (repliedEvent) { + MXEvent* pollStartedEvent = [mxSession.store eventWithEventId:repliedEvent.relatesTo.eventId inRoom:repliedEvent.roomId]; + replacementText = [MXEventContentPollStart modelFromJSON:pollStartedEvent.content].question; + } + + if (replacementText == nil) { + replacementText = VectorL10n.pollTimelineReplyEndedPoll; + } + + finalString = [htmlString stringByReplacingCharactersInRange:groupRange withString:replacementText]; + + return finalString; +} + - (void)postFormatMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEvent:(MXEvent*)event andRepliedEvent:(MXEvent*)repliedEvent diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 10cf491a6..9ba006a89 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -46,6 +46,7 @@ static NSRegularExpression *eventIdRegex; static NSRegularExpression *httpLinksRegex; // A regex to find all HTML tags static NSRegularExpression *htmlTagsRegex; +static NSDataDetector *linkDetector; @implementation MXKTools @@ -60,7 +61,8 @@ static NSRegularExpression *htmlTagsRegex; eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil]; httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil]; - htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; + htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; + linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; }); } @@ -1037,6 +1039,23 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo { [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex]; } + + // This allows to check for normal url based links (like https://element.io) + // And set back the default link color + NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)]; + if (matches) + { + for (NSTextCheckingResult *match in matches) + { + NSRange matchRange = [match range]; + NSURL *matchUrl = [match URL]; + NSURLComponents *url = [[NSURLComponents new] initWithURL:matchUrl resolvingAgainstBaseURL:NO]; + if (url.URL) + { + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:matchRange]; + } + } + } } + (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex @@ -1083,6 +1102,8 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // If the match is fully in the link, skip it if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length) { + // but before we set the right color + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:linkMatch.range]; hasAlreadyLink = YES; break; } @@ -1097,6 +1118,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo NSString *link = [mutableAttributedString.string substringWithRange:match.range]; link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; [mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:match.range]; } }]; } diff --git a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m index 74132c23f..ca572733a 100644 --- a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m +++ b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m @@ -67,6 +67,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText { + self.linkTextAttributes = @{}; if (@available(iOS 15.0, *)) { [self flushPills]; } diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m index c19d4bfe5..725354b6f 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m @@ -169,7 +169,7 @@ if ((row >= 0) && (row < rooms.count)) { MXRoom* room = [rooms objectAtIndex:row]; - _inputTextField.text = room.summary.displayname; + _inputTextField.text = room.summary.displayName; _addButton.enabled = YES; } @@ -191,7 +191,7 @@ rooms = [_mxSession.rooms sortedArrayUsingComparator:^NSComparisonResult(MXRoom* firstRoom, MXRoom* secondRoom) { // Alphabetic order - return [firstRoom.summary.displayname compare:secondRoom.summary.displayname options:NSCaseInsensitiveSearch]; + return [firstRoom.summary.displayName compare:secondRoom.summary.displayName options:NSCaseInsensitiveSearch]; }]; return rooms.count; @@ -202,7 +202,7 @@ - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { MXRoom* room = [rooms objectAtIndex:row]; - return room.summary.displayname; + return room.summary.displayName; } @end diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m index a65174564..c081f51f6 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m @@ -71,7 +71,7 @@ MXRoom *room = [_mxSession roomWithRoomId:mxPushRule.ruleId]; if (room) { - description = [VectorL10n notificationSettingsRoomRuleTitle:room.summary.displayname]; + description = [VectorL10n notificationSettingsRoomRuleTitle:room.summary.displayName]; } break; } @@ -163,7 +163,7 @@ if (sender == _controlButton) { // Swap enable state - [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled]; + [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled completion:nil]; } else if (sender == _deleteButton) { diff --git a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m index c86fbe0d2..60a378221 100644 --- a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m +++ b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m @@ -82,7 +82,7 @@ if (_mxRoom) { // Replace empty string by nil : avoid having the placeholder 'Room name" when there is no displayname - self.displayNameTextField.text = (_mxRoom.summary.displayname.length) ? _mxRoom.summary.displayname : nil; + self.displayNameTextField.text = (_mxRoom.summary.displayName.length) ? _mxRoom.summary.displayName : nil; } else if (_mxUser) { @@ -189,7 +189,7 @@ if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomName]) { // Only the room name is edited here, update the text field with the room name - textField.text = _mxRoom.summary.displayname; + textField.text = _mxRoom.summary.displayName; textField.backgroundColor = [UIColor whiteColor]; } else @@ -236,7 +236,7 @@ textField.backgroundColor = [UIColor clearColor]; NSString *roomName = textField.text; - if ((roomName.length || _mxRoom.summary.displayname.length) && [roomName isEqualToString:_mxRoom.summary.displayname] == NO) + if ((roomName.length || _mxRoom.summary.displayName.length) && [roomName isEqualToString:_mxRoom.summary.displayName] == NO) { if ([self.delegate respondsToSelector:@selector(roomTitleView:isSaving:)]) { @@ -266,7 +266,7 @@ } // Revert change - textField.text = strongSelf.mxRoom.summary.displayname; + textField.text = strongSelf.mxRoom.summary.displayName; MXLogDebug(@"[MXKRoomTitleView] Rename room failed"); // Notify MatrixKit user NSString *myUserId = strongSelf.mxRoom.mxSession.myUser.userId; @@ -278,7 +278,7 @@ else { // No change on room name, restore title with room displayName - textField.text = _mxRoom.summary.displayname; + textField.text = _mxRoom.summary.displayName; } } } diff --git a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m index b4dc7d4e1..0099053d6 100644 --- a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m +++ b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m @@ -367,7 +367,7 @@ if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomName]) { // Only the room name is edited here, update the text field with the room name - textField.text = self.mxRoom.summary.displayname; + textField.text = self.mxRoom.summary.displayName; textField.backgroundColor = [UIColor whiteColor]; } else diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index bc1696653..168e22154 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -97,8 +97,18 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { // MARK: - Public func start() { + // bwi: (#4394) handling of the case of canceled login during pincode stage. Startign the authentication first is needed for startup to work + if !MXKAccountManager.shared().accounts.isEmpty && !PinCodePreferences.shared.isPinSet && PinCodePreferences.shared.forcePinProtection { + if let session = AppDelegate.theDelegate().mxSessions.first as? MXSession { + + self.beginAuthentication(with: .login) + authenticationCoordinator?.setPresentSecurityScreens(present: true) + authenticationCoordinator?.setSession(session: session) + showPincodePromt(for: session) + } + } // TODO: Manage a separate flow for soft logout that just uses AuthenticationCoordinator - if authenticationService.softLogoutCredentials != nil { + else if authenticationService.softLogoutCredentials != nil { if BWIBuildSettings.shared.showBwiSplashScreen || BWIBuildSettings.shared.authScreenShowRegister { showBwiSplashScreen() } else { @@ -161,8 +171,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { /// Displays the next view in the flow after the splash screen. private func bwiSplashScreenCoordinator(_ coordinator: OnboardingBwiSplashScreenCoordinator, didCompleteWith result: OnboardingBwiSplashScreenViewModelResult) { - if BWIBuildSettings.shared.onboardingEnableNewAuthenticationFlow { - self.showWelcomeExperience() + if BWIBuildSettings.shared.onboardingEnableNewAuthenticationFlow { + // bwi: show app specific welcoming screens if enabled + if BWIBuildSettings.shared.onboardingShowWelcomeScreens { + self.showWelcomeExperience() + } else { + self.beginAuthentication(with: .login) + } } else { showLegacyAuthenticationScreen(forceAsRootModule: true) } diff --git a/Riot/Modules/People/PeopleViewController.h b/Riot/Modules/People/PeopleViewController.h index 514333769..7e03c6313 100644 --- a/Riot/Modules/People/PeopleViewController.h +++ b/Riot/Modules/People/PeopleViewController.h @@ -15,13 +15,13 @@ limitations under the License. */ -#import "RecentsBannerViewController.h" +#import "RecentsViewController.h" #import "ContactsDataSource.h" /** 'PeopleViewController' instance is used to display/filter the direct rooms and a list of contacts. */ -@interface PeopleViewController : RecentsBannerViewController +@interface PeopleViewController : RecentsViewController + (instancetype)instantiate; diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift new file mode 100644 index 000000000..ecf9ca5a0 --- /dev/null +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift @@ -0,0 +1,221 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import ZXingObjC +import Combine + +final class QRCodeReaderView: UIView { + + // MARK: Public + + var didFoundData: (Data) -> Void = { _ in } + + // MARK: Private + + private lazy var zxCapture: ZXCapture = ZXCapture() + private var captureSizeTransform: CGAffineTransform? + private var isScanning: Bool = false + private var isFirstApplyOrientation: Bool = false + + private var rotationObserver: AnyCancellable? + + init() { + super.init(frame: .zero) + setup() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + deinit { +#if !targetEnvironment(simulator) + self.zxCapture.layer.removeFromSuperlayer() + self.zxCapture.hard_stop() +#endif + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview == nil { + stopScanning() + } + } + + // MARK: - Public + + func startScanning() { +#if !targetEnvironment(simulator) + self.zxCapture.start() +#endif + isScanning = true + } + + func stopScanning() { +#if !targetEnvironment(simulator) + self.zxCapture.stop() +#endif + isScanning = false + } + + // MARK: - Private + + override func layoutSubviews() { + super.layoutSubviews() + + guard isFirstApplyOrientation == false else { + return + } + + isFirstApplyOrientation = true + applyOrientation() + } + + private func setup() { + isUserInteractionEnabled = true + clipsToBounds = true + self.setupQRCodeReaderView() + + rotationObserver = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) + .sink(receiveValue: { [weak self] _ in + self?.applyOrientation() + }) + } + + private func setupQRCodeReaderView() { +#if !targetEnvironment(simulator) + zxCapture.delegate = self + zxCapture.camera = zxCapture.back() + zxCapture.layer.frame = self.bounds + self.layer.addSublayer(zxCapture.layer) +#endif + } + + private func applyOrientation() { + + let orientation = UIApplication.shared.statusBarOrientation + let captureRotation: Double + let scanRectRotation: Double + + switch orientation { + case .portrait: + captureRotation = 0 + scanRectRotation = 90 + case .landscapeLeft: + captureRotation = 90 + scanRectRotation = 180 + case .landscapeRight: + captureRotation = 270 + scanRectRotation = 0 + case .portraitUpsideDown: + captureRotation = 180 + scanRectRotation = 270 + default: + captureRotation = 0 + scanRectRotation = 90 + } + + applyRectOfInterest(orientation: orientation) + + let angleRadius = captureRotation / 180.0 * Double.pi + let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) + + zxCapture.transform = captureTranform + zxCapture.rotation = CGFloat(scanRectRotation) + zxCapture.layer.frame = self.bounds + } + + private func applyRectOfInterest(orientation: UIInterfaceOrientation) { + var transformedVideoRect = self.frame + let cameraSessionPreset = zxCapture.sessionPreset + + var scaleVideoX, scaleVideoY: CGFloat + var videoHeight, videoWidth: CGFloat + + // Currently support only for 1920x1080 || 1280x720 + if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { + videoHeight = 1080.0 + videoWidth = 1920.0 + } else { + videoHeight = 720.0 + videoWidth = 1280.0 + } + + if orientation == UIInterfaceOrientation.portrait { + scaleVideoX = self.frame.width / videoHeight + scaleVideoY = self.frame.height / videoWidth + + // Convert CGPoint under portrait mode to map with orientation of image + // because the image will be cropped before rotate + // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 + let realX = transformedVideoRect.origin.y + let realY = self.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x + let realWidth = transformedVideoRect.size.height + let realHeight = transformedVideoRect.size.width + transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) + + } else { + scaleVideoX = self.frame.width / videoWidth + scaleVideoY = self.frame.height / videoHeight + } + + captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) + + guard let _captureSizeTransform = captureSizeTransform else { + return + } + + let transformRect = transformedVideoRect.applying(_captureSizeTransform) + zxCapture.scanRect = transformRect + } +} + + +// MARK: - ZXCaptureDelegate +extension QRCodeReaderView: ZXCaptureDelegate { + + func captureCameraIsReady(_ capture: ZXCapture!) { + isScanning = true + } + + func captureResult(_ capture: ZXCapture!, result: ZXResult!) { + guard let zxResult = result, isScanning == true else { + return + } + + guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { + return + } + + self.stopScanning() + + if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, + let byteArray = bytes.firstObject as? ZXByteArray { + + let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) + + self.didFoundData(data) + } + } +} diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift index 4d35af705..764744613 100644 --- a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift @@ -15,7 +15,6 @@ */ import UIKit -import ZXingObjC protocol QRCodeReaderViewControllerDelegate: AnyObject { func qrCodeReaderViewController(_ viewController: QRCodeReaderViewController, didFound payloadData: Data) @@ -40,10 +39,7 @@ final class QRCodeReaderViewController: UIViewController { private var theme: Theme! private var errorPresenter: MXKErrorPresentation! - private lazy var zxCapture: ZXCapture = ZXCapture() - private var captureSizeTransform: CGAffineTransform? - private var isScanning: Bool = false - private var isFirstApplyOrientation: Bool = false + private var qrCodeReaderView: QRCodeReaderView! // MARK: Public @@ -56,12 +52,7 @@ final class QRCodeReaderViewController: UIViewController { viewController.theme = ThemeService.shared().theme return viewController } - - deinit { - self.zxCapture.layer.removeFromSuperlayer() - self.zxCapture.hard_stop() - } - + // MARK: - Life cycle override func viewDidLoad() { @@ -92,40 +83,14 @@ final class QRCodeReaderViewController: UIViewController { return self.theme.statusBarStyle } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - guard isFirstApplyOrientation == false else { - return - } - - isFirstApplyOrientation = true - applyOrientation() - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate(alongsideTransition: { (context) in - // do nothing - }, completion: { [weak self] (context) in - guard let self = self else { - return - } - self.applyOrientation() - }) - } - // MARK: - Public func startScanning() { - self.zxCapture.start() - isScanning = true + qrCodeReaderView.startScanning() } func stopScanning() { - self.zxCapture.stop() - isScanning = false + qrCodeReaderView.stopScanning() } // MARK: - Private @@ -145,94 +110,15 @@ final class QRCodeReaderViewController: UIViewController { } private func setupViews() { - self.setupQRCodeReaderView() + let qrCodeReaderView = QRCodeReaderView() + qrCodeReaderView.didFoundData = qrCodeReader(didFound:) + self.qrCodeReaderView = qrCodeReaderView + + self.codeReaderContainerView.vc_addSubViewMatchingParent(qrCodeReaderView) } - private func setupQRCodeReaderView() { - zxCapture.delegate = self - zxCapture.camera = zxCapture.back() - - zxCapture.layer.frame = codeReaderContainerView.bounds - codeReaderContainerView.layer.addSublayer(zxCapture.layer) - } - - private func applyOrientation() { - - let orientation = UIApplication.shared.statusBarOrientation - let captureRotation: Double - let scanRectRotation: Double - - switch orientation { - case .portrait: - captureRotation = 0 - scanRectRotation = 90 - case .landscapeLeft: - captureRotation = 90 - scanRectRotation = 180 - case .landscapeRight: - captureRotation = 270 - scanRectRotation = 0 - case .portraitUpsideDown: - captureRotation = 180 - scanRectRotation = 270 - default: - captureRotation = 0 - scanRectRotation = 90 - } - - applyRectOfInterest(orientation: orientation) - - let angleRadius = captureRotation / 180.0 * Double.pi - let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) - - zxCapture.transform = captureTranform - zxCapture.rotation = CGFloat(scanRectRotation) - zxCapture.layer.frame = codeReaderContainerView.frame - } - - private func applyRectOfInterest(orientation: UIInterfaceOrientation) { - guard var transformedVideoRect = codeReaderContainerView?.frame, - let cameraSessionPreset = zxCapture.sessionPreset - else { return } - - var scaleVideoX, scaleVideoY: CGFloat - var videoHeight, videoWidth: CGFloat - - // Currently support only for 1920x1080 || 1280x720 - if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { - videoHeight = 1080.0 - videoWidth = 1920.0 - } else { - videoHeight = 720.0 - videoWidth = 1280.0 - } - - if orientation == UIInterfaceOrientation.portrait { - scaleVideoX = self.view.frame.width / videoHeight - scaleVideoY = self.view.frame.height / videoWidth - - // Convert CGPoint under portrait mode to map with orientation of image - // because the image will be cropped before rotate - // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 - let realX = transformedVideoRect.origin.y - let realY = self.view.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x - let realWidth = transformedVideoRect.size.height - let realHeight = transformedVideoRect.size.width - transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) - - } else { - scaleVideoX = self.view.frame.width / videoWidth - scaleVideoY = self.view.frame.height / videoHeight - } - - captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) - - guard let _captureSizeTransform = captureSizeTransform else { - return - } - - let transformRect = transformedVideoRect.applying(_captureSizeTransform) - zxCapture.scanRect = transformRect + private func qrCodeReader(didFound data: Data) { + self.delegate?.qrCodeReaderViewController(self, didFound: data) } // MARK: - Actions @@ -241,31 +127,3 @@ final class QRCodeReaderViewController: UIViewController { self.delegate?.qrCodeReaderViewControllerDidCancel(self) } } - -// MARK: - ZXCaptureDelegate -extension QRCodeReaderViewController: ZXCaptureDelegate { - - func captureCameraIsReady(_ capture: ZXCapture!) { - isScanning = true - } - - func captureResult(_ capture: ZXCapture!, result: ZXResult!) { - guard let zxResult = result, isScanning == true else { - return - } - - guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { - return - } - - self.stopScanning() - - if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, - let byteArray = bytes.firstObject as? ZXByteArray { - - let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) - - self.delegate?.qrCodeReaderViewController(self, didFound: data) - } - } -} diff --git a/Riot/Modules/Rendezvous/RendezvousService.swift b/Riot/Modules/Rendezvous/RendezvousService.swift index 49c3486bb..ac9a890e1 100644 --- a/Riot/Modules/Rendezvous/RendezvousService.swift +++ b/Riot/Modules/Rendezvous/RendezvousService.swift @@ -30,6 +30,7 @@ enum RendezvousServiceError: Error { /// Algorithm name as per MSC3903 enum RendezvousChannelAlgorithm: String { case ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" + case ECDH_V2 = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256" } /// Allows communication through a secure channel. Based on MSC3886 and MSC3903 @@ -40,17 +41,20 @@ class RendezvousService { private var privateKey: Curve25519.KeyAgreement.PrivateKey! private var interlocutorPublicKey: Curve25519.KeyAgreement.PublicKey? private var symmetricKey: SymmetricKey? + private var algorithm: RendezvousChannelAlgorithm - init(transport: RendezvousTransportProtocol) { + init(transport: RendezvousTransportProtocol, algorithm: RendezvousChannelAlgorithm) { self.transport = transport + self.algorithm = algorithm } /// Creates a new rendezvous endpoint and publishes the creator's public key func createRendezvous() async -> Result { privateKey = Curve25519.KeyAgreement.PrivateKey() + let algorithm = RendezvousChannelAlgorithm.ECDH_V2 - let publicKeyString = privateKey.publicKey.rawRepresentation.base64EncodedString() - let details = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue) + let publicKeyString = encodeBase64(data: privateKey.publicKey.rawRepresentation) + let details = RendezvousDetails(algorithm: algorithm.rawValue) switch await transport.create(body: details) { case .failure(let transportError): @@ -60,7 +64,7 @@ class RendezvousService { return .failure(.transportError(.rendezvousURLInvalid)) } - let fullDetails = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + let fullDetails = RendezvousDetails(algorithm: algorithm.rawValue, transport: RendezvousTransportDetails(type: "org.matrix.msc3886.http.v1", uri: rendezvousURL.absoluteString), key: publicKeyString) @@ -80,7 +84,7 @@ class RendezvousService { } guard let key = response.key, - let interlocutorPublicKeyData = Data(base64Encoded: key), + let interlocutorPublicKeyData = decodeBase64(input: key), let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { return .failure(.invalidInterlocutorKey) } @@ -107,7 +111,7 @@ class RendezvousService { /// Joins an existing rendezvous and publishes the joiner's public key /// At the end of this a symmetric key will be available for encryption func joinRendezvous(withPublicKey publicKey: String) async -> Result { - guard let interlocutorPublicKeyData = Data(base64Encoded: publicKey), + guard let interlocutorPublicKeyData = decodeBase64(input: publicKey), let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { MXLog.debug("[RendezvousService] Invalid interlocutor data") return .failure(.invalidInterlocutorKey) @@ -115,8 +119,8 @@ class RendezvousService { privateKey = Curve25519.KeyAgreement.PrivateKey() - let publicKeyString = privateKey.publicKey.rawRepresentation.base64EncodedString() - let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + let publicKeyString = encodeBase64(data: privateKey.publicKey.rawRepresentation) + let payload = RendezvousDetails(algorithm: algorithm.rawValue, key: publicKeyString) guard case .success = await transport.send(body: payload) else { @@ -142,6 +146,18 @@ class RendezvousService { return .success(validationCode) } + private func encodeBase64(data: Data) -> String { + if algorithm == .ECDH_V2 { + return MXBase64Tools.unpaddedBase64(from: data) + } + return MXBase64Tools.base64(from: data) + } + + private func decodeBase64(input: String) -> Data? { + // MXBase64Tools will decode both padded and unpadded data so we don't need to take account of algorithm here + return MXBase64Tools.data(fromBase64: input) + } + /// Send arbitrary data over the secure channel /// This will use the previously generated symmetric key to AES encrypt the payload /// - Parameter data: the data to be encrypted and sent @@ -162,8 +178,8 @@ class RendezvousService { var ciphertext = sealedBox.ciphertext ciphertext.append(contentsOf: sealedBox.tag) - let body = RendezvousMessage(iv: Data(nonce).base64EncodedString(), - ciphertext: ciphertext.base64EncodedString()) + let body = RendezvousMessage(iv: encodeBase64(data: Data(nonce)), + ciphertext: encodeBase64(data: ciphertext)) switch await transport.send(body: body) { case .failure(let transportError): @@ -191,8 +207,8 @@ class RendezvousService { MXLog.debug("Received rendezvous response: \(response)") - guard let ciphertextData = Data(base64Encoded: response.ciphertext), - let nonceData = Data(base64Encoded: response.iv), + guard let ciphertextData = decodeBase64(input: response.ciphertext), + let nonceData = decodeBase64(input: response.iv), let nonce = try? AES.GCM.Nonce(data: nonceData) else { return .failure(.decodingError) } @@ -243,9 +259,9 @@ class RendezvousService { initiatorPublicKey: Curve25519.KeyAgreement.PublicKey, recipientPublicKey: Curve25519.KeyAgreement.PublicKey, byteCount: Int = SHA256Digest.byteCount) -> SymmetricKey { - guard let sharedInfoData = [RendezvousChannelAlgorithm.ECDH_V1.rawValue, - initiatorPublicKey.rawRepresentation.base64EncodedString(), - recipientPublicKey.rawRepresentation.base64EncodedString()] + guard let sharedInfoData = [algorithm.rawValue, + encodeBase64(data: initiatorPublicKey.rawRepresentation), + encodeBase64(data: recipientPublicKey.rawRepresentation)] .joined(separator: "|") .data(using: .utf8) else { fatalError("[RendezvousService] Failed creating symmetric key shared data") diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 8b3a49a5f..8f71a82e0 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -24,8 +24,8 @@ extern NSString *const URLPreviewDidUpdateNotification; typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) { RoomBubbleCellDataTagMessage = 0, // Default value used for messages - RoomBubbleCellDataTagMembership, - RoomBubbleCellDataTagRoomCreateConfiguration, + RoomBubbleCellDataTagMembership = 1, // bwi: default value for the room membership changes (status messages) + RoomBubbleCellDataTagRoomCreateConfiguration = 2, // bwi: default value for the room configuration (status messages) RoomBubbleCellDataTagRoomCreateWithPredecessor, RoomBubbleCellDataTagKeyVerificationNoDisplay, RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval, diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 219f67481..10b6737ed 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1072,6 +1072,15 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // We do not want to merge room create event cells with other cell types return NO; } + + if (self.tag == RoomBubbleCellDataTagPoll) { + MXEvent* event = self.events.firstObject; + + if (event) { + // m.poll.ended events should always show the sender information + return event.eventType != MXEventTypePollEnd; + } + } if (self.hasThreadRoot || bubbleCellData.hasThreadRoot) { diff --git a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewController.swift b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewController.swift index 43f5a279b..697358479 100644 --- a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewController.swift +++ b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewController.swift @@ -197,7 +197,7 @@ extension RoomCreationEventsModalViewController: UITableViewDataSource { cell.textView.backgroundColor = .clear cell.backgroundColor = theme.backgroundColor cell.contentView.backgroundColor = .clear - cell.tintColor = theme.tintColor + cell.tintColor = ThemeService.shared().theme.tintColor cell.selectionStyle = .none return cell } diff --git a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift index a692559d9..2a78d6ac0 100644 --- a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift +++ b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift @@ -62,7 +62,7 @@ final class RoomCreationEventsModalViewModel: RoomCreationEventsModalViewModelTy guard let summary = session.roomSummary(withRoomId: roomState.roomId) else { return nil } - return summary.displayname + return summary.displayName } var roomInfo: String? { diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index de6a691f4..8d0d2808a 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -370,6 +370,11 @@ [self.roomDataSource.room.summary markAllAsReadLocally]; [self updateCurrentEventIdAtTableBottom:YES]; + + if (!self.isContextPreview) + { + [self.roomDataSource.room resetUnread]; + } } - (void)viewWillDisappear:(BOOL)animated @@ -755,7 +760,7 @@ else { // set default title - self.navigationItem.title = roomDataSource.room.summary.displayname; + self.navigationItem.title = roomDataSource.room.summary.displayName; } // Show input tool bar @@ -775,7 +780,7 @@ } else { - self.navigationItem.title = roomDataSource.room.summary.displayname; + self.navigationItem.title = roomDataSource.room.summary.displayName; } } else @@ -885,7 +890,7 @@ } failure:^(NSError *error) { cancelIndicator(); - MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", self->roomDataSource.room.summary.displayname); + MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", self->roomDataSource.room.summary.displayName); [self processRoomJoinFailureWithError:error completion:completion]; }]; } diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index 765190370..2583e8be0 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -396,11 +396,11 @@ switch (roomPowerLevel) { case RoomPowerLevelAdmin: - self.roomMemberPowerLevelLabel.text = [BWIL10n roomMemberPowerLevelAdminIn:self.mxRoom.summary.displayname]; + self.roomMemberPowerLevelLabel.text = [BWIL10n roomMemberPowerLevelAdminIn:self.mxRoom.summary.displayName]; self.roomMemberPowerLevelContainerView.hidden = NO; break; case RoomPowerLevelModerator: - self.roomMemberPowerLevelLabel.text = [BWIL10n roomMemberPowerLevelModeratorIn:self.mxRoom.summary.displayname]; + self.roomMemberPowerLevelLabel.text = [BWIL10n roomMemberPowerLevelModeratorIn:self.mxRoom.summary.displayName]; self.roomMemberPowerLevelContainerView.hidden = NO; break; default: @@ -447,7 +447,7 @@ NSString* userLabel = [self tmpUserLabel]; if (userLabel) { - self.roomMemberUserIdLabel.text = [BWIL10n bwiRoomMemberDetailsUserlabel:self.mxRoom.summary.displayname :userLabel]; + self.roomMemberUserIdLabel.text = [BWIL10n bwiRoomMemberDetailsUserlabel:self.mxRoom.summary.displayName :userLabel]; } else { self.roomMemberUserIdLabel.text = nil; } @@ -812,7 +812,7 @@ } else if (section == bwiUserLabelsIndex) { - return [BWIL10n bwiRoomMemberSectionUserlabels: self.mxRoom.summary.displayname]; + return [BWIL10n bwiRoomMemberSectionUserlabels: self.mxRoom.summary.displayName]; } return nil; @@ -887,7 +887,6 @@ title = @"bwi functions desc"; break; case MXKRoomMemberDetailsActionBwiUserLabel: - // $$$ string translation does not exist title = @""; break; default: diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m index 4a22b0693..45df17607 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -155,13 +155,15 @@ [self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"]; - if (_showInviteUserFab) - { - // Add invite members button programmatically - [self vc_addFABWithImage:AssetImages.addMemberFloatingAction.image - target:self - action:@selector(onAddParticipantButtonPressed)]; - } + // bwi: too early to tell if button should be added -> we need mxRoom +// if (_showInviteUserFab) +// { +// // Add invite members button programmatically +// [self vc_addFABWithImage:AssetImages.addMemberFloatingAction.image +// target:self +// action:@selector(onAddParticipantButtonPressed)]; +// } + // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -1199,7 +1201,7 @@ if (section == invitedSection) { - headerLabel.text = [VectorL10n roomParticipantsInvitedSection]; + headerLabel.text = BWIL10n.bwiRoomParticipantsSectionInvite; } else if (section == adminSection) { headerLabel.text = BWIL10n.bwiRoomParticipantsSectionAdmin; diff --git a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsFooter.swift b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsFooter.swift index db5285192..fac2ff186 100644 --- a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsFooter.swift +++ b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsFooter.swift @@ -44,8 +44,11 @@ class RoomNotificationSettingsFooter: UITableViewHeaderFooterView { // let linkRange = (footer0.string as NSString).range(of: linkStr) // footer0.addAttribute(NSAttributedString.Key.link, value: Constants.linkToAccountSettings, range: linkRange) - label.text = footerState.showEncryptedNotice ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : nil - + if BWIBuildSettings.shared.notificationSettingsLikeAndroidAndWeb { + label.text = "" + } else { + label.text = footerState.showEncryptedNotice ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : nil + } } } diff --git a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsViewController.swift b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsViewController.swift index 87445c38e..77229499f 100644 --- a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsViewController.swift +++ b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsViewController.swift @@ -161,7 +161,7 @@ final class RoomNotificationSettingsViewController: UIViewController { }) }) let footerState = RoomNotificationSettingsFooter.State(showEncryptedNotice: viewState.roomEncrypted, showAccountLink: false) - let section0 = Section(title: VectorL10n.roomNotifsSettingsNotifyMeFor, rows: rows, footerState: footerState) + let section0 = Section(title: BWIL10n.roomNotifsSettingsNotifyMeFor, rows: rows, footerState: footerState) sections = [ section0 ] diff --git a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationsSettingsCell.swift b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationsSettingsCell.swift index 782e08150..2625b24e1 100644 --- a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationsSettingsCell.swift +++ b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationsSettingsCell.swift @@ -36,7 +36,7 @@ extension RoomNotificationSettingsCell: Themable { textLabel?.textColor = theme.textPrimaryColor backgroundColor = theme.backgroundColor contentView.backgroundColor = .clear - tintColor = theme.tintColor + tintColor = ThemeService.shared().theme.tintColor selectedBackgroundView = UIView() selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index eee4a3e87..46f9ae452 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -32,6 +32,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { private let parentSpaceId: String? private let initialSection: RoomInfoSection private let dismissOnCancel: Bool + private let canAddParticipants: Bool private weak var roomSettingsViewController: RoomSettingsViewController? private lazy var segmentedViewController: SegmentedViewController = { @@ -44,6 +45,8 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { participants.parentSpaceId = self.parentSpaceId participants.delegate = self participants.screenTracker = AnalyticsScreenTracker(screen: .roomMembers) + participants.showInviteUserFab = self.canAddParticipants + let files = RoomFilesViewController() files.finalizeInit() @@ -106,6 +109,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { self.room = parameters.room self.parentSpaceId = parameters.parentSpaceId self.initialSection = parameters.initialSection + self.canAddParticipants = parameters.canAddParticipants self.dismissOnCancel = parameters.dismissOnCancel } @@ -188,6 +192,14 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { coordinator.start() self.add(childCoordinator: coordinator) self.navigationRouter.push(coordinator, animated: true, popCompletion: nil) + case .pollHistory: + let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: room, navigationRouter: navigationRouter)) + coordinator.start() + coordinator.completion = { [weak self] event in + guard let self else { return } + self.delegate?.roomInfoCoordinator(self, viewEventInTimeline: event) + } + push(coordinator: coordinator) default: guard let tabIndex = target.tabIndex else { fatalError("No settings tab index for this target.") @@ -201,6 +213,13 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { navigationRouter.push(segmentedViewController, animated: animated, popCompletion: nil) } } + + private func push(coordinator: Coordinator & Presentable, animated: Bool = true) { + self.add(childCoordinator: coordinator) + navigationRouter.push(coordinator, animated: animated) { + self.remove(childCoordinator: coordinator) + } + } } // MARK: - RoomInfoListCoordinatorDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index b8db2a66a..39e740bfc 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -17,12 +17,14 @@ */ import Foundation +import MatrixSDK @objc protocol RoomInfoCoordinatorBridgePresenterDelegate { func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember) func roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String) + func roomInfoCoordinatorBridgePresenter(_ coordinator: RoomInfoCoordinatorBridgePresenter, viewEventInTimeline event: MXEvent) } /// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller. @@ -129,6 +131,9 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate { func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) { self.delegate?.roomInfoCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId) } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) { + self.delegate?.roomInfoCoordinatorBridgePresenter(self, viewEventInTimeline: event) + } } // MARK: - UIAdaptivePresentationControllerDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift index c1c46fab4..2478922d6 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift @@ -33,12 +33,14 @@ class RoomInfoCoordinatorParameters: NSObject { let parentSpaceId: String? let initialSection: RoomInfoSection let dismissOnCancel: Bool + let canAddParticipants: Bool - init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, dismissOnCancel: Bool) { + init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool = true, dismissOnCancel: Bool) { self.session = session self.room = room self.parentSpaceId = parentSpaceId self.initialSection = initialSection + self.canAddParticipants = canAddParticipants self.dismissOnCancel = dismissOnCancel super.init() } @@ -50,4 +52,8 @@ class RoomInfoCoordinatorParameters: NSObject { convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) { self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, dismissOnCancel: false) } + + convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool) { + self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, canAddParticipants: canAddParticipants, dismissOnCancel: false) + } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift index 80f696b0b..2122ddf1d 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift @@ -17,12 +17,14 @@ */ import Foundation +import MatrixSDK protocol RoomInfoCoordinatorDelegate: AnyObject { func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType) func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType) func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) } /// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift index 3cc108208..d9825853e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift @@ -28,6 +28,7 @@ enum RoomInfoListTarget: Equatable { case search case permalink case notifications + case pollHistory var tabIndex: UInt? { switch self { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index d7c549748..c9b3703d8 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -207,6 +207,9 @@ final class RoomInfoListViewController: UIViewController { let rowUploads = Row(type: .default, icon: Asset.Images.scrollup.image, text: BWIL10n.roomDetailsFiles, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .uploads)) } + let rowPollHistory = Row(type: .default, icon: Asset.Images.pollHistory.image, text: VectorL10n.roomDetailsPolls, accessoryType: .disclosureIndicator) { + self.viewModel.process(viewAction: .navigate(target: .pollHistory)) + } let rowSearch = Row(type: .default, icon: Asset.Images.searchIcon.image, text: VectorL10n.roomDetailsSearch, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .search)) } @@ -231,6 +234,11 @@ final class RoomInfoListViewController: UIViewController { rows.append(rowIntegrations) } rows.append(rowMembers) + + if BuildSettings.pollsEnabled { + rows.append(rowPollHistory) + } + rows.append(rowUploads) if BWIBuildSettings.shared.bwiShowRoomSearch { @@ -416,7 +424,7 @@ extension RoomInfoListViewController: UITableViewDataSource { } cell.backgroundColor = theme.backgroundColor cell.contentView.backgroundColor = .clear - cell.tintColor = theme.tintColor + cell.tintColor = ThemeService.shared().theme.tintColor return cell } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift index 3010c5148..932f7fddd 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift @@ -40,7 +40,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { let basicInfoViewData = RoomInfoBasicViewData(avatarUrl: room.summary.avatar, mediaManager: session.mediaManager, roomId: room.roomId, - roomDisplayName: room.summary.displayname, + roomDisplayName: room.summary.displayName, mainRoomAlias: room.summary.aliases?.first, roomTopic: room.summary.topic, encryptionImage: encryptionImage, diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 14bab4c05..b0c482917 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -35,6 +35,7 @@ @class LiveLocationSharingBannerView; @class VoiceBroadcastService; @class ComposerLinkActionBridgePresenter; +@class PerformanceProfile; NS_ASSUME_NONNULL_BEGIN @@ -125,6 +126,8 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; @property (nonatomic, strong, nullable) ComposerLinkActionBridgePresenter *composerLinkActionBridgePresenter; +@property (weak, nonatomic, nullable) UIViewController *waitingOtherParticipantViewController; +@property (nonatomic) BOOL isWaitingForOtherParticipants; /** Retrieve the live data source in cases where the timeline is not live. @@ -189,6 +192,9 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; */ + (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration; +// bwi: measure performance +- (void) finishTextMessageProfil:(PerformanceProfile*)profile; + @end /** diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 9053a7439..8ac4bad5b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -188,6 +188,9 @@ static CGSize kThreadListBarButtonItemImageSize; // Time to display notification content in the timeline MXTaskProfile *notificationTaskProfile; + + // Observe kMXEventTypeStringRoomMember events + __weak id roomMemberEventListener; } @property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView; @@ -236,6 +239,9 @@ static CGSize kThreadListBarButtonItemImageSize; // scroll state just before the layout change, and restore it after the layout. @property (nonatomic) BOOL wasScrollAtBottomBeforeLayout; +// Check if we should wait for other participants +@property (nonatomic, readonly) BOOL shouldWaitForOtherParticipants; + @end @implementation RoomViewController @@ -332,6 +338,7 @@ static CGSize kThreadListBarButtonItemImageSize; _showMissedDiscussionsBadge = YES; _scrollToBottomHidden = YES; + _isWaitingForOtherParticipants = NO; // Listen to the event sent state changes [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeSentState:) name:kMXEventDidChangeSentStateNotification object:nil]; @@ -381,7 +388,10 @@ static CGSize kThreadListBarButtonItemImageSize; // Prepare missed dicussion badge (if any) self.showMissedDiscussionsBadge = _showMissedDiscussionsBadge; - + + // Refresh the waiting for other participants state + [self refreshWaitForOtherParticipantsState]; + // Set up the room title view according to the data source (if any) [self refreshRoomTitle]; @@ -397,6 +407,12 @@ static CGSize kThreadListBarButtonItemImageSize; self.jumpToLastUnreadLabel.text = [VectorL10n roomJumpToFirstUnread]; + if(BWIBuildSettings.shared.bwiEnableBuMUI) { + self.jumpToLastUnreadImageView.image = [UIImage imageNamed:@"room_scroll_up_bum"]; + } else { + self.jumpToLastUnreadImageView.image = [UIImage imageNamed:@"room_scroll_up"]; + } + MXWeakify(self); // Observe user interface theme change. @@ -1208,9 +1224,9 @@ static CGSize kThreadListBarButtonItemImageSize; BOOL canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsMessage:kMXEventTypeStringRoomMessage]); BOOL isRoomObsolete = self.roomDataSource.roomState.isObsolete; - BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded]; + BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded]; - if (isRoomObsolete || isResourceLimitExceeded) + if (isRoomObsolete || isResourceLimitExceeded || _isWaitingForOtherParticipants) { roomInputToolbarViewClass = nil; shouldDismissContextualMenu = YES; @@ -1377,6 +1393,10 @@ static CGSize kThreadListBarButtonItemImageSize; MXStrongifyAndReturnIfNil(self); if (readyToSend) { + // bwi: evaluate send message performance + PerformanceProfile *sendTextMessageProfile = [[PerformanceProfile alloc] initWithThreshold:BWIBuildSettings.shared.sendMessageThreshold]; + [sendTextMessageProfile startMeasurement]; + // The event modified is always fetch from the actual data source MXEvent *eventModified = [self.roomDataSource eventWithEventId:self.customizedRoomDataSource.selectedEventId]; @@ -1385,25 +1405,36 @@ static CGSize kThreadListBarButtonItemImageSize; [self setupRoomDataSourceToResolveEvent:^(MXKRoomDataSource *roomDataSource) { if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeReply && eventModified) { - [roomDataSource sendReplyToEvent:eventModified withTextMessage:msgTxt success:nil failure:^(NSError *error) { + [roomDataSource sendReplyToEvent:eventModified withTextMessage:msgTxt success:^(NSString *string) + { + [self finishTextMessageProfil:sendTextMessageProfile]; + } failure:^(NSError *error) { // Just log the error. The message will be displayed in red in the room history MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed."); + [sendTextMessageProfile abortMeasurement]; }]; } else if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeEdit && eventModified) { - [roomDataSource replaceTextMessageForEvent:eventModified withTextMessage:msgTxt success:nil failure:^(NSError *error) { - // Just log the error. The message will be displayed in red + [roomDataSource replaceTextMessageForEvent:eventModified withTextMessage:msgTxt success:^(NSString *string) + { + [self finishTextMessageProfil:sendTextMessageProfile]; + } failure:^(NSError *error) { + // Just log the error. The message will be displayed in red in the room history MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed."); + [sendTextMessageProfile abortMeasurement]; }]; } else { // Let the datasource send it and manage the local echo - [roomDataSource sendTextMessage:msgTxt success:nil failure:^(NSError *error) + [roomDataSource sendTextMessage:msgTxt success:^(NSString *string) { + [self finishTextMessageProfil:sendTextMessageProfile]; + } failure:^(NSError *error) { // Just log the error. The message will be displayed in red in the room history MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed."); + [sendTextMessageProfile abortMeasurement]; }]; } @@ -1546,6 +1577,8 @@ static CGSize kThreadListBarButtonItemImageSize; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil]; + [self waitForOtherParticipant:NO]; + [super destroy]; } @@ -1652,6 +1685,57 @@ static CGSize kThreadListBarButtonItemImageSize; return self.customizedRoomDataSource.isCurrentUserSharingActiveLocation; } +#pragma mark - Wait for 3rd party invitee + +- (void)setIsWaitingForOtherParticipants:(BOOL)isWaitingForOtherParticipants +{ + if (_isWaitingForOtherParticipants == isWaitingForOtherParticipants) + { + return; + } + + _isWaitingForOtherParticipants = isWaitingForOtherParticipants; + [self updateRoomInputToolbarViewClassIfNeeded]; + + if (_isWaitingForOtherParticipants) + { + if (self->roomMemberEventListener == nil) + { + MXWeakify(self); + self->roomMemberEventListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) { + MXStrongifyAndReturnIfNil(self); + if (direction != MXTimelineDirectionForwards) + { + return; + } + [self refreshWaitForOtherParticipantsState]; + }]; + } + } + else + { + if (self->roomMemberEventListener != nil) + { + [self.roomDataSource.room removeListener:self->roomMemberEventListener]; + self->roomMemberEventListener = nil; + } + } +} + +- (BOOL)shouldWaitForOtherParticipants +{ + MXRoomState *roomState = self.roomDataSource.roomState; + BOOL isDirect = self.roomDataSource.room.isDirect; + + // Wait for the other participant only if it is a direct encrypted room with only one member waiting for a third party guest. + return (isDirect && roomState.isEncrypted && roomState.membersCount.members == 1 && roomState.thirdPartyInvites.count > 0); +} + +- (void)refreshWaitForOtherParticipantsState +{ + [self waitForOtherParticipant:self.shouldWaitForOtherParticipants]; +} + #pragma mark - Internals - (UIBarButtonItem *)videoCallBarButtonItem @@ -1970,7 +2054,7 @@ static CGSize kThreadListBarButtonItemImageSize; [self refreshMissedDiscussionsCount:YES]; // bwi: check if threads are enabled - if (self.showThreadOption) + if (self.showThreadOption && !_isWaitingForOtherParticipants) { if (self.roomDataSource.threadId) { @@ -2283,8 +2367,8 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated { - RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection]; - + RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection canAddParticipants: !self.isWaitingForOtherParticipants]; + self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters]; self.roomInfoCoordinatorBridgePresenter.delegate = self; @@ -2477,13 +2561,19 @@ static CGSize kThreadListBarButtonItemImageSize; // Prevents listening a VB when recording a new one [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + // Check connectivity + if ([AppDelegate theDelegate].isOffline) + { + [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]]; + return; + } + // Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room [session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) { if (voiceBroadcastService) { - [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { - - } failure:^(NSError * _Nonnull error) { - + [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { } failure:^(NSError * _Nonnull error) { + [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]]; + [session tearDownVoiceBroadcastService]; }]; } else @@ -4330,8 +4420,14 @@ static CGSize kThreadListBarButtonItemImageSize; [self startActivityIndicator]; + NSArray* relationTypes = nil; + // If it's a voice broadcast, delete the selected event and all related events. + if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { + relationTypes = @[MXEventRelationTypeReference]; + } + MXWeakify(self); - [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ + [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:relationTypes reason:nil success:^{ MXStrongifyAndReturnIfNil(self); [self stopActivityIndicator]; } failure:^(NSError *error) { @@ -5221,7 +5317,14 @@ static CGSize kThreadListBarButtonItemImageSize; - (IBAction)onVoiceCallPressed:(id)sender { - if (self.isCallActive) + // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + + if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) { + [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle + message:VectorL10n.voiceBroadcastVoipCannotStartDescription]; + } + else if (self.isCallActive) { [self hangupCall]; } @@ -5233,7 +5336,15 @@ static CGSize kThreadListBarButtonItemImageSize; - (IBAction)onVideoCallPressed:(id)sender { - [self placeCallWithVideo:YES]; + // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + + if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) { + [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle + message:VectorL10n.voiceBroadcastVoipCannotStartDescription]; + } else { + [self placeCallWithVideo:YES]; + } } - (IBAction)onThreadListTapped:(id)sender @@ -5264,25 +5375,9 @@ static CGSize kThreadListBarButtonItemImageSize; { // Dismiss potential keyboard. [self dismissKeyboard]; - - // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. - MXWeakify(self); - [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId - initialEventId:self.roomDataSource.room.accountData.readMarkerEventId - threadId:self.roomDataSource.threadId - andMatrixSession:self.mainSession - onComplete:^(id roomDataSource) { - MXStrongifyAndReturnIfNil(self); - - [roomDataSource finalizeInitialization]; - - // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view. - self.centerBubblesTableViewContentOnTheInitialEventBottom = YES; - [self displayRoom:roomDataSource]; - - // Give the data source ownership to the room view controller. - self.hasRoomDataSourceOwnership = YES; - }]; + NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId; + NSString *threadId = self.roomDataSource.threadId; + [self reloadRoomWihtEventId:eventId threadId:threadId]; } else if (sender == self.resetReadMarkerButton) { @@ -7492,7 +7587,7 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service { - if (!service) + if (!service || _isWaitingForOtherParticipants) { return; } @@ -7905,6 +8000,35 @@ static CGSize kThreadListBarButtonItemImageSize; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } } +- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator + viewEventInTimeline:(MXEvent *)event +{ + [self.navigationController popToViewController:self animated:true]; + [self reloadRoomWihtEventId:event.eventId threadId:event.threadId]; +} + +-(void)reloadRoomWihtEventId:(NSString *)eventId + threadId:(NSString *)threadId +{ + // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. + MXWeakify(self); + [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:eventId + threadId:threadId + andMatrixSession:self.mainSession + onComplete:^(id roomDataSource) { + MXStrongifyAndReturnIfNil(self); + + [roomDataSource finalizeInitialization]; + + // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view. + self.centerBubblesTableViewContentOnTheInitialEventBottom = YES; + [self displayRoom:roomDataSource]; + + // Give the data source ownership to the room view controller. + self.hasRoomDataSourceOwnership = YES; + }]; +} #pragma mark - RemoveJitsiWidgetViewDelegate @@ -7972,7 +8096,7 @@ static CGSize kThreadListBarButtonItemImageSize; samples:(NSArray *)samples completion:(void (^)(BOOL))completion { - [self.roomDataSource sendVoiceMessage:url mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { + [self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); completion(YES); } failure:^(NSError *error) { diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 561cc382a..ef1c3e828 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -106,6 +106,11 @@ extension RoomViewController { /// /// - Parameter attributedTextMsg: the attributed text message @objc func sendAttributedTextMessage(_ attributedTextMsg: NSAttributedString) { + + // bwi: evaluate send message performance + let sendTextMessageProfile = PerformanceProfile(threshold: BWIBuildSettings.shared.sendMessageThreshold) + sendTextMessageProfile.startMeasurement() + let eventModified = self.roomDataSource.event(withEventId: customizedRoomDataSource?.selectedEventId) self.setupRoomDataSource { roomDataSource in guard let roomDataSource = roomDataSource as? RoomDataSource else { return } @@ -115,11 +120,12 @@ extension RoomViewController { withAttributedTextMessage: attributedTextMsg) { response in switch response { case .success: - break + self.finishTextMessageProfil(sendTextMessageProfile) case .failure: MXLog.error("[RoomViewController] sendAttributedTextMessage failed while updating event", context: [ "event_id": eventModified.eventId ]) + sendTextMessageProfile.abortMeasurement() } } } else if self.inputToolbar?.sendMode == .edit, let eventModified = eventModified { @@ -127,20 +133,22 @@ extension RoomViewController { for: eventModified, withAttributedTextMessage: attributedTextMsg, success: { _ in - // + self.finishTextMessageProfil(sendTextMessageProfile) }, failure: { _ in MXLog.error("[RoomViewController] sendAttributedTextMessage failed while updating event", context: [ "event_id": eventModified.eventId ]) + sendTextMessageProfile.abortMeasurement() }) } else { roomDataSource.sendAttributedTextMessage(attributedTextMsg) { response in switch response { case .success: - break + self.finishTextMessageProfil(sendTextMessageProfile) case .failure: MXLog.error("[RoomViewController] sendAttributedTextMessage failed") + sendTextMessageProfile.abortMeasurement() } } } @@ -251,6 +259,50 @@ extension RoomViewController { composerLinkActionBridgePresenter = presenter presenter.present(from: self, animated: true) } + + @objc func showWaitingOtherParticipantHeader() { + let controller = VectorHostingController(rootView: RoomWaitingForMembers()) + guard let headerView = controller.view else { + return + } + self.waitingOtherParticipantViewController = controller + self.addChild(controller) + + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + headerView.translatesAutoresizingMaskIntoConstraints = false + containerView.vc_addSubViewMatchingParent(headerView, withInsets: UIEdgeInsets(top: 9, left: 9, bottom: -9, right: -9)) + + self.bubblesTableView.tableHeaderView = containerView + NSLayoutConstraint.activate([ + containerView.centerXAnchor.constraint(equalTo: self.bubblesTableView.centerXAnchor), + containerView.widthAnchor.constraint(equalTo: self.bubblesTableView.widthAnchor), + containerView.topAnchor.constraint(equalTo: self.bubblesTableView.topAnchor) + ]) + controller.didMove(toParent: self) + + self.bubblesTableView.tableHeaderView?.layoutIfNeeded() + } + + @objc func hideWaitingOtherParticipantHeader() { + guard let waitingOtherParticipantViewController else { + return + } + waitingOtherParticipantViewController.removeFromParent() + self.bubblesTableView.tableHeaderView = nil + waitingOtherParticipantViewController.didMove(toParent: nil) + self.waitingOtherParticipantViewController = nil + } + + @objc func waitForOtherParticipant(_ wait: Bool) { + self.isWaitingForOtherParticipants = wait + if wait { + showWaitingOtherParticipantHeader() + } else { + hideWaitingOtherParticipantHeader() + } + } + } // MARK: - Private Helpers diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 515af3545..baccdff0c 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -540,7 +540,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS]; } - [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE]; [tmpSections addObject:sectionMain]; if (!BWIBuildSettings.shared.roomSettingsScreenRemoveLeave) { @@ -2392,22 +2391,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti cell = favoriteCell; } } - else if (row == ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE) - { - MXKTableViewCellWithButton *leaveCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier] forIndexPath:indexPath]; - - NSString* title = [VectorL10n leave]; - - [leaveCell.mxkButton setTitle:title forState:UIControlStateNormal]; - [leaveCell.mxkButton setTitle:title forState:UIControlStateHighlighted]; - [leaveCell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; - leaveCell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; - - [leaveCell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; - [leaveCell.mxkButton addTarget:self action:@selector(onLeave:) forControlEvents:UIControlEventTouchUpInside]; - - cell = leaveCell; - } } else if (section == SECTION_TAG_ACCESS) { @@ -3266,76 +3249,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti #pragma mark - actions -- (void)onLeave:(id)sender -{ - // Prompt user before leaving the room - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - NSString *title, *message; - if ([self.mainSession roomWithRoomId:self.roomId].isDirect) - { - title = [VectorL10n roomParticipantsLeavePromptTitleForDm]; - message = [VectorL10n roomParticipantsLeavePromptMsgForDm]; - } - else - { - title = [VectorL10n roomParticipantsLeavePromptTitle]; - message = [VectorL10n roomParticipantsLeavePromptMsg]; - } - - currentAlert = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n leave] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - [self startActivityIndicator]; - [self->mxRoom leave:^{ - - if (self.delegate) { - [self.delegate roomSettingsViewControllerDidLeaveRoom:self]; - } else { - [[LegacyAppDelegate theDelegate] restoreInitialDisplay:nil]; - } - - } failure:^(NSError *error) { - - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomSettingsViewController] Leave room failed"); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"RoomSettingsVCLeaveAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; -} - - (void)onRoomAvatarTap:(UITapGestureRecognizer *)recognizer { if (!mxRoom.isDirect || BWIBuildSettings.shared.showUnrelatedRoomSettingsForDirectMessages) { diff --git a/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift b/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift index 30b444829..3ff48f568 100644 --- a/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift +++ b/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift @@ -237,13 +237,13 @@ class RoomGroupCallStatusCell: RoomCallBaseCell { TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions { if JitsiService.shared.isWidgetDeclined(withId: widgetId) { - innerContentView.callerNameLabel.text = room.summary.displayname + innerContentView.callerNameLabel.text = room.summary.displayName room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) viewState = .declined statusText = VectorL10n.eventFormatterCallYouDeclined } else { - innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayname) + innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayName) innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl, withType: nil, @@ -257,7 +257,7 @@ class RoomGroupCallStatusCell: RoomCallBaseCell { statusText = nil } } else { - innerContentView.callerNameLabel.text = room.summary.displayname + innerContentView.callerNameLabel.text = room.summary.displayName room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) } diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m index d5d9f08ed..20a1fde2c 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m @@ -895,7 +895,8 @@ static BOOL _disableLongPressGestureOnEvent; id cellLayoutUpdater = timelineConfiguration.currentStyle.cellLayoutUpdater; // Handle updated text view layout if needed - if (cellLayoutUpdater) + // bwi: don't use cellLayoutUpdater for status messages (bubbleData.tag / RoomBubbleCellDataTag == 1 and 2), fixes cell sizing problem (status messages getting cut off) + if (cellLayoutUpdater && bubbleData.tag != 2 && bubbleData.tag != 1) { maxTextViewWidth = [cellLayoutUpdater maximumTextViewWidthFor:cell cellData:cellData maximumCellWidth:maxWidth]; } diff --git a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m index c06b34016..34e292dc3 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m @@ -26,8 +26,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift index 9b1ad925f..f490950df 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift @@ -192,7 +192,7 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell { discussionType = .room(topic: roomSummary.topic, canInvitePeople: bubbleData.canInvitePeople) } - let displayName = roomSummary.displayname ?? "" + let displayName = roomSummary.displayName ?? "" let roomAvatarViewData = RoomAvatarViewData(roomId: roomId, displayName: displayName, diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m index 2fe69ff5f..21ae1b2ad 100644 --- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m @@ -37,13 +37,6 @@ xibPictureViewTopConstraintConstant = self.pictureViewTopConstraint.constant; } -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)prepareForReuse { [super prepareForReuse]; diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m index eac1df923..558ef7050 100644 --- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m @@ -26,13 +26,6 @@ @implementation RoomMembershipCollapsedBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)layoutSubviews { [super layoutSubviews]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift index 993b606c5..f39abcd40 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift @@ -50,7 +50,7 @@ class PollBaseBubbleCell: PollPlainCell { return } - self.addBubbleBackgroundView( messageBubbleBackgroundView, to: pollView) + self.addBubbleBackgroundView(messageBubbleBackgroundView, to: pollView) messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift index 5a7d8984b..d38b5cb3c 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift @@ -51,14 +51,6 @@ class TextMessageBaseBubbleCell: SizableBaseRoomCell, RoomCellURLPreviewDisplaya override func setupMessageTextViewLongPressGesture() { // Do nothing, otherwise default setup prevent link tap } - - override func update(theme: Theme) { - super.update(theme: theme) - - if let messageTextView = self.messageTextView { - messageTextView.tintColor = theme.tintColor - } - } } // MARK: - RoomCellTimestampDisplayable diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m index 3dd06a1d8..6da4b9db1 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m index 2a7a96788..d40aea21c 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m index 30b096e91..2ca481673 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m @@ -23,13 +23,6 @@ @implementation RoomIncomingAttachmentWithoutSenderInfoBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - + (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth { CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m index 456305e9c..716e15c31 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m index d78362a65..4cb4f3761 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m index 46ea41861..57cf00691 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m @@ -22,11 +22,5 @@ @implementation RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m index 7021df5f6..ec91ee33d 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m @@ -24,13 +24,6 @@ @implementation RoomOutgoingAttachmentWithoutSenderInfoBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)render:(MXKCellData *)cellData { [super render:cellData]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m index 28bbdb19c..288e54d0a 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index c66531dce..9e7bc37c3 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m index ae4126049..86862a0d0 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m @@ -22,11 +22,4 @@ @implementation RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index db4370dc5..245b17e77 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m index 8678dd894..3f5d73475 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m index a31a52029..fa50aa35f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 0bc9f1de3..dfafa3df6 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 9abfde421..c2826e3b9 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -82,6 +82,9 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; inputAccessoryViewForKeyboard = [[UIView alloc] initWithFrame:CGRectZero]; self.textView.inputAccessoryView = inputAccessoryViewForKeyboard; + + UIImage* buttonImage = BWIBuildSettings.shared.bwiEnableBuMUI ? AssetImages.sendIconBum.image : AssetImages.sendIcon.image; + [self.rightInputToolbarButton setImage:buttonImage forState:UIControlStateNormal]; } #pragma mark - Override MXKView @@ -219,7 +222,7 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; switch (_sendMode) { case RoomInputToolbarViewSendModeReply: - buttonImage = AssetImages.sendIcon.image; + buttonImage = BWIBuildSettings.shared.bwiEnableBuMUI ? AssetImages.sendIconBum.image : AssetImages.sendIcon.image; self.inputContextImageView.image = AssetImages.inputReplyIcon.image; self.inputContextLabel.text = [VectorL10n roomMessageReplyingTo:self.eventSenderDisplayName]; @@ -237,11 +240,11 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; self.textView.maxHeight -= kContextBarHeight; break; case RoomInputToolbarViewSendModeCreateDM: - buttonImage = AssetImages.sendIcon.image; + buttonImage = BWIBuildSettings.shared.bwiEnableBuMUI ? AssetImages.sendIconBum.image : AssetImages.sendIcon.image; self.inputContextViewHeightConstraint.constant = 0; break; default: - buttonImage = AssetImages.sendIcon.image; + buttonImage = BWIBuildSettings.shared.bwiEnableBuMUI ? AssetImages.sendIconBum.image : AssetImages.sendIcon.image; if (previousMode != _sendMode) { diff --git a/Riot/Modules/Room/Views/Title/RoomTitleView.m b/Riot/Modules/Room/Views/Title/RoomTitleView.m index b07f928d9..48cbb5a3b 100644 --- a/Riot/Modules/Room/Views/Title/RoomTitleView.m +++ b/Riot/Modules/Room/Views/Title/RoomTitleView.m @@ -85,7 +85,7 @@ [super customizeViewRendering]; self.backgroundColor = UIColor.clearColor; - self.displayNameTextField.textColor = (self.mxRoom.summary.displayname.length ? ThemeService.shared.theme.textPrimaryColor : ThemeService.shared.theme.textSecondaryColor); + self.displayNameTextField.textColor = (self.mxRoom.summary.displayName.length ? ThemeService.shared.theme.textPrimaryColor : ThemeService.shared.theme.textSecondaryColor); self.typingLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.dotView.backgroundColor = ThemeService.shared.theme.warningColor; self.missedDiscussionsBadgeLabel.textColor = ThemeService.shared.theme.tintColor; @@ -121,7 +121,7 @@ [self.presenceIndicatorView stopListeningPresenceUpdates]; } - self.displayNameTextField.text = self.mxRoom.summary.displayname; + self.displayNameTextField.text = self.mxRoom.summary.displayName; if (!self.displayNameTextField.text.length) { self.displayNameTextField.text = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 7956ad107..db6cc8193 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -17,6 +17,7 @@ import Foundation import Reusable import WysiwygComposer +import HTMLParser import SwiftUI import Combine import UIKit @@ -43,10 +44,26 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel( - textColor: ThemeService.shared().theme.colors.primaryContent, - linkColor: ThemeService.shared().theme.colors.accent, - codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor + parserStyle: WysiwygInputToolbarView.parserStyle ) + /// Compute current HTML parser style for composer. + private static var parserStyle: HTMLParserStyle { + return HTMLParserStyle( + textColor: ThemeService.shared().theme.colors.primaryContent, + linkColor: ThemeService.shared().theme.colors.links, + codeBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + borderColor: ThemeService.shared().theme.textQuinaryColor, + borderWidth: 1.0, + cornerRadius: 4.0, + padding: .init(horizontal: 10.0, vertical: 12.0), + type: .background), + quoteBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + borderColor: ThemeService.shared().theme.selectedBackgroundColor, + borderWidth: 0.0, + cornerRadius: 0.0, + padding: .init(horizontal: 25.0, vertical: 12.0), + type: .side(offset: 5, width: 4))) + } private var viewModel: ComposerViewModelProtocol! private var isLandscapePhone: Bool { @@ -298,9 +315,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background - wysiwygViewModel.textColor = theme.colors.primaryContent - wysiwygViewModel.linkColor = theme.colors.accent - wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor + wysiwygViewModel.parserStyle = WysiwygInputToolbarView.parserStyle } private func updateTextViewHeight() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 4a242a91c..920e0aeb4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -53,7 +53,7 @@ class VoiceMessageAudioPlayer: NSObject { return false } - return (audioPlayer.rate > 0) + return audioPlayer.currentItem != nil && (audioPlayer.rate > 0) } var duration: TimeInterval { @@ -118,6 +118,13 @@ class VoiceMessageAudioPlayer: NSObject { } } + func reloadContentIfNeeded() { + if let url, let audioPlayer, audioPlayer.currentItem == nil { + self.url = nil + loadContentFromURL(url) + } + } + func removeAllPlayerItems() { audioPlayer?.removeAllItems() } @@ -130,6 +137,8 @@ class VoiceMessageAudioPlayer: NSObject { func play() { isStopped = false + reloadContentIfNeeded() + do { try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) try AVAudioSession.sharedInstance().setActive(true) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index d82200109..9ab618e97 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -73,15 +73,18 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { } } - func stopRecording() { + func stopRecording(releaseAudioSession: Bool = true) { audioRecorder?.stop() - do { - try AVAudioSession.sharedInstance().setActive(false) - } catch { - delegateContainer.notifyDelegatesWithBlock { delegate in - (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } + + if releaseAudioSession { + MXLog.debug("[VoiceMessageAudioRecorder] stopRecording() - releasing audio session") + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } + } } - } func peakPowerForChannelNumber(_ channelNumber: Int) -> Float { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index fa5058a3f..bd2ef8bf6 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -187,7 +187,10 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioPlayer?.stop() audioRecorder?.stopRecording() - sendRecordingAtURL(temporaryFileURL) + // As we only use a single temporary file, we have to rename it, otherwise it will be deleted once the file is sent and if another recording has been started meanwhile, it will fail. + if let finalFileURL = finalizeRecordingAtURL(temporaryFileURL) { + sendRecordingAtURL(finalFileURL) + } isInLockedMode = false updateUI() @@ -196,15 +199,25 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + guard self.audioRecorder === audioRecorder else { + return + } notifiedRemainingTime = false updateUI() } func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + guard self.audioRecorder === audioRecorder else { + return + } updateUI() } func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) { + guard self.audioRecorder === audioRecorder else { + MXLog.error("[VoiceMessageController] audioRecorder failed but it's not the current one.") + return + } isInLockedMode = false updateUI() @@ -214,20 +227,34 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } audioPlayer.seekToTime(0.0) { [weak self] _ in self?.updateUI() + // Reload its content if necessary, otherwise the seek won't work + self?.audioPlayer?.reloadContentIfNeeded() } } @@ -260,8 +287,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioRecorder?.stopRecording() guard isInLockedMode else { - if recordDuration ?? 0 >= Constants.minimumRecordingDuration { - sendRecordingAtURL(temporaryFileURL) + if recordDuration ?? 0 >= Constants.minimumRecordingDuration, let finalRecordingURL = finalizeRecordingAtURL(temporaryFileURL) { + sendRecordingAtURL(finalRecordingURL) } else { cancelRecording() } @@ -277,7 +304,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, isInLockedMode = false audioPlayer?.stop() - audioRecorder?.stopRecording() + + // Check if we are recording before stopping the recording, because it will try to pause the audio session and it can be problematic if another player or recorder is running + if let audioRecorder, audioRecorder.isRecording { + audioRecorder.stopRecording() + } + // Also, we can release it now, which will prevent the service provider from trying to manage an old audio recorder. + audioRecorder = nil deleteRecordingAtURL(temporaryFileURL) @@ -371,6 +404,23 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } } + private func finalizeRecordingAtURL(_ url: URL?) -> URL? { + guard let url = url, FileManager.default.fileExists(atPath: url.path) else { + return nil + } + + // We rename the file to something unique, so that we can start a new recording without having to wait for this record to be sent. + let newPath = url.deletingPathExtension().path + "-\(UUID().uuidString)" + let destinationUrl = URL(fileURLWithPath: newPath).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.moveItem(at: url, to: destinationUrl) + } catch { + MXLog.error("[VoiceMessageController] finalizeRecordingAtURL:", context: error) + return nil + } + return destinationUrl + } + private func deleteRecordingAtURL(_ url: URL?) { // Fix: use url.path instead of url.absoluteString when using FileManager otherwise the url seems to be percent encoded and the file is not found. guard let url = url, FileManager.default.fileExists(atPath: url.path) else { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 54262f828..79d96b548 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -28,6 +28,7 @@ import MediaPlayer private var roomAvatarLoader: MXMediaLoader? private let audioPlayers: NSMapTable private let audioRecorders: NSHashTable + private let nowPlayingInfoDelegates: NSMapTable private var displayLink: CADisplayLink! @@ -47,7 +48,7 @@ import MediaPlayer didSet { // set avatar placeholder for now roomAvatar = AvatarGenerator.generateAvatar(forMatrixItem: currentRoomSummary?.roomId, - withDisplayName: currentRoomSummary?.displayname, + withDisplayName: currentRoomSummary?.displayName, size: Constants.roomAvatarImageSize.width, andFontSize: Constants.roomAvatarFontSize) @@ -93,6 +94,7 @@ import MediaPlayer private override init() { audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) + nowPlayingInfoDelegates = NSMapTable(keyOptions: .weakMemory, valueOptions: .weakMemory) activeAudioPlayers = Set() super.init() @@ -123,27 +125,54 @@ import MediaPlayer pauseAllServicesExcept(nil) } + func registerNowPlayingInfoDelegate(_ delegate: VoiceMessageNowPlayingInfoDelegate, forPlayer player: VoiceMessageAudioPlayer) { + nowPlayingInfoDelegates.setObject(delegate, forKey: player) + } + + func deregisterNowPlayingInfoDelegate(forPlayer player: VoiceMessageAudioPlayer) { + nowPlayingInfoDelegates.removeObject(forKey: player) + } + // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { currentlyPlayingAudioPlayer = audioPlayer activeAudioPlayers.insert(audioPlayer) - setUpRemoteCommandCenter() + + let shouldSetupRemoteCommandCenter = nowPlayingInfoDelegates.object(forKey: audioPlayer)?.shouldSetupRemoteCommandCenter(audioPlayer: audioPlayer) ?? true + if shouldSetupRemoteCommandCenter { + setUpRemoteCommandCenter() + } else { + // clean up the remote command center + tearDownRemoteCommandCenter() + } pauseAllServicesExcept(audioPlayer) } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if currentlyPlayingAudioPlayer == audioPlayer { - currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter() + // If we have a NowPlayingInfoDelegate for this player + let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) + + // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) + if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { + currentlyPlayingAudioPlayer = nil + tearDownRemoteCommandCenter() + } } activeAudioPlayers.remove(audioPlayer) } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if currentlyPlayingAudioPlayer == audioPlayer { - currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter() + // If we have a NowPlayingInfoDelegate for this player + let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) + + // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) + if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { + currentlyPlayingAudioPlayer = nil + tearDownRemoteCommandCenter() + } } activeAudioPlayers.remove(audioPlayer) } @@ -162,7 +191,9 @@ import MediaPlayer continue } - audioRecorder.stopRecording() + // We should release the audio session only if we want to pause all services + let shouldReleaseAudioSession = (service == nil) + audioRecorder.stopRecording(releaseAudioSession: shouldReleaseAudioSession) } guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else { @@ -249,6 +280,17 @@ import MediaPlayer let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() nowPlayingInfoCenter.nowPlayingInfo = nil + nowPlayingInfoCenter.playbackState = .stopped + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = false + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.isEnabled = false + commandCenter.pauseCommand.removeTarget(nil) + commandCenter.skipForwardCommand.isEnabled = false + commandCenter.skipForwardCommand.removeTarget(nil) + commandCenter.skipBackwardCommand.isEnabled = false + commandCenter.skipBackwardCommand.removeTarget(nil) } private func updateNowPlayingInfoCenter() { @@ -256,9 +298,14 @@ import MediaPlayer return } - let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() - nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder, - MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any, - MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any] + // Checks if we have a delegate for this player, or if we should update the NowPlayingInfoCenter ourselves + if let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) { + nowPlayingInfoDelegate.updateNowPlayingInfoCenter(forPlayer: audioPlayer) + } else { + let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder, + MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any, + MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any] + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift new file mode 100644 index 000000000..959f8ca91 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift @@ -0,0 +1,26 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc protocol VoiceMessageNowPlayingInfoDelegate { + + func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) + + func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool + + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: VoiceMessageAudioPlayer) -> Bool +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index eb4f9b1c0..d20819aed 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -149,6 +149,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess audioPlayer.seekToTime(0.0) { [weak self] _ in guard let self = self else { return } self.state = .stopped + // Reload its content if necessary, otherwise the seek won't work + self.audioPlayer?.reloadContentIfNeeded() } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index b78d1df76..e26439230 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -183,6 +183,14 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture func update(theme: Theme) { currentTheme = theme playbackView.update(theme: theme) + + if BWIBuildSettings.shared.bwiEnableBuMUI { + let microphoneImage = Asset.Images.voiceMessageRecordButtonRecordingBum.image + secondaryRecordButton.setImage(microphoneImage, for: .normal) + } else { + let microphoneImage = Asset.Images.voiceMessageRecordButtonRecording.image + secondaryRecordButton.setImage(microphoneImage, for: .normal) + } } // MARK: - UIGestureRecognizerDelegate diff --git a/Riot/Modules/Rooms/RoomsViewController.h b/Riot/Modules/Rooms/RoomsViewController.h index 68e79ea69..2f665ef65 100644 --- a/Riot/Modules/Rooms/RoomsViewController.h +++ b/Riot/Modules/Rooms/RoomsViewController.h @@ -15,12 +15,12 @@ limitations under the License. */ -#import "RecentsBannerViewController.h" +#import "RecentsViewController.h" /** The `RoomsViewController` screen is the view controller displayed when `Rooms` tab is selected. */ -@interface RoomsViewController : RecentsBannerViewController +@interface RoomsViewController : RecentsViewController + (instancetype)instantiate; diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift index 6395b6cdc..595de3adb 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift @@ -242,7 +242,7 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { } private func roomCellViewModel(with room: MXRoom) -> DirectoryRoomTableViewCellVM { - let displayName = room.summary.displayname + let displayName = room.summary.displayName let joinedMembersCount = Int(room.summary.membersCount.joined) let topic = MXTools.stripNewlineCharacters(room.summary.topic) let isJoined = room.summary.membership == .join || room.summary.membershipTransitionState == .joined diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift index 462462291..9457f2d7f 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift @@ -190,6 +190,15 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { self.recoverButton.isEnabled = self.viewModel.isFormValid } + // bwi: MESSENGER-3948 + private func updateShowHidePassphraseButton() { + if self.passphraseTextField.isSecureTextEntry { + passphraseVisibilityButton.setImage(Asset.Images.revealPasswordButton.image, for: .normal) + } else { + passphraseVisibilityButton.setImage(Asset.Images.hidePasswordButton.image, for: .normal) + } + } + private func render(viewState: SecretsRecoveryWithPassphraseViewState) { switch viewState { case .loading: @@ -232,6 +241,7 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { @IBAction private func passphraseVisibilityButtonAction(_ sender: Any) { self.passphraseTextField.isSecureTextEntry = !self.passphraseTextField.isSecureTextEntry + self.updateShowHidePassphraseButton() // bwi: MESSENGER-3948 } @objc private func passphraseTextFieldDidChange(_ textField: UITextField) { diff --git a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift index 6c72ebe5d..402dab935 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift @@ -64,7 +64,7 @@ final class SecretsResetCoordinator: SecretsResetCoordinatorType { let reauthenticationCoordinatorParameters = ReauthenticationCoordinatorParameters(session: self.session, presenter: self.toPresentable(), title: nil, - message: VectorL10n.secretsResetAuthenticationMessage, + message: BWIL10n.secretsResetAuthenticationMessage, authenticatedEndpointRequest: request) let coordinator = ReauthenticationCoordinator(parameters: reauthenticationCoordinatorParameters) diff --git a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift index 7c93e85af..3dd135927 100644 --- a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift +++ b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift @@ -303,6 +303,7 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController, UITe return } self.passphraseTextField.isSecureTextEntry.toggle() + self.updateShowHidePassphraseButton() // bwi: MESSENGER-3948 } @objc private func textFieldDidChange(_ textField: UITextField) { @@ -312,6 +313,15 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController, UITe self.viewModel.process(viewAction: .updatePassphrase(textField.text)) } + // bwi: MESSENGER-3948 + private func updateShowHidePassphraseButton() { + if self.passphraseTextField.isSecureTextEntry { + passphraseVisibilityButton.setImage(Asset.Images.revealPasswordButton.image, for: .normal) + } else { + passphraseVisibilityButton.setImage(Asset.Images.hidePasswordButton.image, for: .normal) + } + } + @IBAction private func validateButtonAction(_ sender: Any) { self.viewModel.process(viewAction: .validate) } diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift index 91139a6c5..09a1eae4d 100644 --- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift +++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift @@ -102,18 +102,6 @@ final class SecureBackupSetupIntroViewController: UIViewController { self.title = BWIL10n.secureKeyBackupSetupIntroTitle self.informationLabel.text = BWIL10n.secureKeyBackupSetupIntroInfo - if let session = AppDelegate.theDelegate().mxSessions.first as? MXSession { - if !session.homeserverWellknown.backupRequired() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - guard let self = self else { - return - } - self.delegate?.secureBackupSetupIntroViewControllerDidCancel(self, showSkipAlert: true) - } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - } - } - self.secureKeyCell.fill(title: VectorL10n.secureKeyBackupSetupIntroUseSecurityKeyTitle, information: VectorL10n.secureKeyBackupSetupIntroUseSecurityKeyInfo, image: Asset.Images.secretsSetupKey.image) diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index f575d7ac3..4edbe05f0 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -332,6 +332,7 @@ TableViewSectionsDelegate> Section *analyticsSection = [Section sectionWithTag:SECTION_ANALYTICS]; [analyticsSection addRowWithTag:ANALYTICS_ENABLED]; analyticsSection.headerTitle = BWIL10n.bwiSettingsAnalyticsSectionHeader; + analyticsSection.footerTitle = BWIL10n.bwiSettingsAnalyticsSectionFooter; [sections addObject:analyticsSection]; } @@ -1306,7 +1307,7 @@ TableViewSectionsDelegate> if (rowTag == ANALYTICS_ENABLED) { MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - labelAndSwitchCell.mxkLabel.text = BWIL10n.matomoSettingsSendDiagnosticData; + labelAndSwitchCell.mxkLabel.text = BWIL10n.bwiSettingsAnalyticsSwitchText; labelAndSwitchCell.mxkSwitch.on = BWIAnalytics.sharedTracker.running; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; labelAndSwitchCell.mxkSwitch.enabled = YES; @@ -1916,6 +1917,10 @@ TableViewSectionsDelegate> BOOL isOn = sender.on; BWIAnalytics.sharedTracker.running = isOn; + + if (isOn) { + [BWIAnalytics.sharedTracker trackBwiValue:@0 :@"General" :@"ConsentGiven" :@"settings"]; + } } @end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 084bcad01..30cf90b59 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -81,6 +81,7 @@ typedef NS_ENUM(NSUInteger, USER_SETTINGS_INDEX) USER_SETTINGS_DISPLAYNAME_INDEX, USER_SETTINGS_PERSONAL_STATE_INDEX, USER_SETTINGS_CHANGE_PASSWORD_INDEX, + USER_SETTINGS_MY_QR_CODE_INDEX, USER_SETTINGS_FIRST_NAME_INDEX, USER_SETTINGS_SURNAME_INDEX, USER_SETTINGS_ADD_EMAIL_INDEX, @@ -226,7 +227,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_NEW_SESSION_MANAGER, LABS_ENABLE_NEW_CLIENT_INFO_FEATURE, LABS_ENABLE_WYSIWYG_COMPOSER, - LABS_ENABLE_VOICE_BROADCAST + LABS_ENABLE_VOICE_BROADCAST, + LABS_ENABLE_CRYPTO_SDK }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -272,6 +274,9 @@ ChangePasswordCoordinatorBridgePresenterDelegate> __weak id notificationCenterDidUpdateObserver; __weak id notificationCenterDidFailObserver; + // bwi: 4203 reference to profile image view + __weak MXKImageView* bwiProfileImage; + // profile updates // avatar UIImage* newAvatarImage; @@ -420,6 +425,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionUserSettings addRowWithTag:USER_SETTINGS_CHANGE_PASSWORD_INDEX]; } + if (BWIBuildSettings.shared.showMyQRCode) + { + [sectionUserSettings addRowWithTag:USER_SETTINGS_MY_QR_CODE_INDEX]; + } if (BuildSettings.settingsScreenShowUserFirstName) { [sectionUserSettings addRowWithTag:USER_SETTINGS_FIRST_NAME_INDEX]; @@ -739,6 +748,12 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BWIBuildSettings.shared.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; + /* bwi: disabled for our apps + if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId]) + { + [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; + } + [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; @@ -746,13 +761,18 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING]; } + */ [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; + /* bwi: disabled for our apps [sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE]; + */ if (@available(iOS 15.0, *)) { [sectionLabs addRowWithTag:LABS_ENABLE_WYSIWYG_COMPOSER]; } + /* bwi: disabled for our apps [sectionLabs addRowWithTag:LABS_ENABLE_VOICE_BROADCAST]; + */ sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) { @@ -1912,6 +1932,9 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { MXKTableViewCellWithLabelAndMXKImageView *profileCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithLabelAndMXKImageView defaultReuseIdentifier] forIndexPath:indexPath]; + // bwi: keep reference to cell image view + bwiProfileImage = profileCell.mxkImageView; + profileCell.mxkLabelLeadingConstraint.constant = tableView.vc_separatorInset.left; profileCell.mxkImageViewTrailingConstraint.constant = 10; @@ -2172,6 +2195,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = passwordCell; } + else if (row == USER_SETTINGS_MY_QR_CODE_INDEX) + { + MXKTableViewCell *showMyQRCodeCell = [self getDefaultTableViewCell:tableView]; + showMyQRCodeCell.textLabel.text = BWIL10n.showMyQrSettingsTitle; + [showMyQRCodeCell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; + cell = showMyQRCodeCell; + } } else if (section == SECTION_TAG_NEW_FEATURES) { @@ -2904,6 +2934,18 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; + } + else if (row == LABS_ENABLE_CRYPTO_SDK) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; + labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; + labelAndSwitchCell.mxkSwitch.on = isEnabled; + [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; } } @@ -2960,7 +3002,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> downtimeCell.textLabel.text = [self bwiDowntimeCellText]; downtimeCell.textLabel.textColor = [self bwiDowntimeTextColor]; downtimeCell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; - downtimeCell.textLabel.numberOfLines = 5; + // bwi: with descriptions coming soon we need to do this dynamically + if ([[[ServerDowntimeDefaultService alloc] init] isSameDay]) { + downtimeCell.textLabel.numberOfLines = 6; + } else { + downtimeCell.textLabel.numberOfLines = 7; + } + cell = downtimeCell; } @@ -3315,6 +3363,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [self displayPasswordAlert]; } + else if (row == USER_SETTINGS_MY_QR_CODE_INDEX) + { + [self showMyQRCode]; + } else if (row == USER_SETTINGS_ADD_EMAIL_INDEX) { if (!self.newEmailEditingEnabled) @@ -3390,7 +3442,8 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { if (row == SHOW_NEW_FEATURES) { - NSString *htmlFile = [[NSBundle mainBundle] pathForResource:BWIBuildSettings.shared.newFeaturesHTML ofType:@"html" inDirectory:nil]; + // bwi: replace link and show webview + NSString *htmlFile = BWIBuildSettings.shared.bwiFeatureHistoryFilePath; WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithLocalHTMLFile: htmlFile]; webViewViewController.title = BWIL10n.bwiSettingsNewFeaturesHeader; [self pushViewController:webViewViewController]; @@ -3780,6 +3833,30 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } +- (void)enableCryptoSDKFeature:(UISwitch *)sender +{ + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk + message:VectorL10n.settingsLabsConfirmCryptoSdk + preferredStyle:UIAlertControllerStyleAlert]; + + MXWeakify(self); + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + + [sender setOn:NO animated:YES]; + }]]; + + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + [CryptoSDKFeature.shared enable]; + [[AppDelegate theDelegate] reloadMatrixSessions:YES]; + }]]; + + [self presentViewController:confirmationAlert animated:YES completion:nil]; + currentAlert = confirmationAlert; +} + - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; @@ -4604,6 +4681,24 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self.changePasswordBridgePresenter presentFrom:self animated:YES]; } +// bwi: 4203 +- (void)showMyQRCode +{ + MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; + MXMyUser* myUser = account.mxSession.myUser; + UIImage* avatarImage = [bwiProfileImage image]; + + UIViewController *myQRCodeViewController; + if( avatarImage ) { + myQRCodeViewController = [MyQRCodeViewController createFromSwiftUIViewWithMatrixID: myUser.userId displayName: myUser.displayname avatarUIImage: bwiProfileImage.image]; + } else { + myQRCodeViewController = [MyQRCodeViewController createFromSwiftUIViewWithMatrixID: myUser.userId displayName: myUser.displayname]; + } + + [[self.navigationController navigationBar] setPrefersLargeTitles:NO]; + [self pushViewController:myQRCodeViewController]; +} + #pragma mark - MXKCountryPickerViewControllerDelegate - (void)countryPickerViewController:(MXKCountryPickerViewController *)countryPickerViewController didSelectCountry:(NSString *)isoCountryCode diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift index 21140abb5..362bfe14d 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift @@ -102,7 +102,7 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { } let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, - displayName: summary.displayname, + displayName: summary.displayName, topic: summary.topic, avatarUrl: summary.avatar, joinRule: nil, @@ -130,7 +130,7 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { }) let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, - displayName: summary.displayname, + displayName: summary.displayName, topic: summary.topic, avatarUrl: summary.avatar, joinRule: joinRule, diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index 428cf8302..762593db7 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -248,9 +248,9 @@ final class SpaceListViewModel: SpaceListViewModelType { var invites: [SpaceListItemViewData] = [] var spaces: [SpaceListItemViewData] = [] session.spaceService.rootSpaceSummaries.forEach { summary in - let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayname, avatarUrl: summary.avatar, mediaManager: session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayname)) + let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayName, avatarUrl: summary.avatar, mediaManager: session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayName)) let notificationState = session.spaceService.notificationCounter.notificationState(forSpaceWithId: summary.roomId) - let viewData = SpaceListItemViewData(spaceId: summary.roomId, title: summary.displayname, + let viewData = SpaceListItemViewData(spaceId: summary.roomId, title: summary.displayName, avatarViewData: avatarViewData, isInvite: summary.membership == .invite, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index ef3290ec7..91a96197b 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -142,7 +142,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { private func renderLoaded(space: MXSpace) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.mxRoom = space.room - if let spaceName = space.summary?.displayname { + if let spaceName = space.summary?.displayName { self.titleView.breadcrumbView.breadcrumbs = [spaceName] } else { self.titleView.breadcrumbView.breadcrumbs = [] diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift index bf4e03aa6..53e463dc4 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift @@ -101,7 +101,7 @@ class SpaceMenuPresenter: NSObject { } private func showLeaveSpace() { - let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayname ?? VectorL10n.spaceTag + let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayName ?? VectorL10n.spaceTag let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 1d2a54eb6..4caabe2a9 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -140,9 +140,9 @@ class SpaceMenuViewController: UIViewController { return } - let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayname, avatarUrl: summary.avatar, mediaManager: self.session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayname)) + let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayName, avatarUrl: summary.avatar, mediaManager: self.session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayName)) - self.titleLabel.text = space.summary?.displayname + self.titleLabel.text = space.summary?.displayName // TODO: display members instead once done on android // self.subtitleLabel.text = space.membersId.count == 1 ? VectorL10n.roomTitleOneMember : // VectorL10n.roomTitleMembers("\(space.membersId.count)") diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index edf5218fb..d445f29af 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -17,6 +17,7 @@ */ import UIKit +import MatrixSDK @objcMembers final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { @@ -59,7 +60,7 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { func start() { - let rootCoordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: self.spaceId, spaceName: self.session.spaceService.getSpace(withId: self.spaceId)?.summary?.displayname) + let rootCoordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: self.spaceId, spaceName: self.session.spaceService.getSpace(withId: self.spaceId)?.summary?.displayName) rootCoordinator.start() @@ -519,5 +520,8 @@ extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate { self.remove(childCoordinator: coordinator) } } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) { + + } } diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 48e363fee..055ff1986 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -36,6 +36,9 @@ UIBarButtonItem *cancelBarButtonItem; UIBarButtonItem *createBarButtonItem; + // SearchBar text + NSString *currentSearch; + // HTTP Request MXHTTPOperation *roomCreationRequest; @@ -46,10 +49,13 @@ @property (weak, nonatomic) IBOutlet UIView *searchBarHeader; @property (weak, nonatomic) IBOutlet UISearchBar *searchBarView; @property (weak, nonatomic) IBOutlet UIView *searchBarHeaderBorder; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchBarHeaderHeightConstraint; @property (nonatomic, strong) InviteFriendsPresenter *inviteFriendsPresenter; @property (nonatomic, weak) InviteFriendsHeaderView *inviteFriendsHeaderView; +@property (nonatomic, weak) UIView *onlyOneEmailInvitationView; + @end @implementation StartChatViewController @@ -131,6 +137,11 @@ - (void)setupInviteFriendsHeaderView { + if (self.inviteFriendsHeaderView) + { + return; + } + if (!RiotSettings.shared.allowInviteExernalUsers) { self.contactsTableView.tableHeaderView = nil; @@ -157,7 +168,7 @@ [self setupInviteFriendsHeaderView]; } } - else + else if (self.inviteFriendsHeaderView != nil) { self.contactsTableView.tableHeaderView = nil; } @@ -309,6 +320,101 @@ contactsDataSource.ignoredContactsByMatrixId[self.mainSession.myUser.userId] = userContact; } } + + // hide the search bar if a participant is already invited by email + [self hideSearchBar: [self participantsAlreadyContainAnEmail]]; + +} + +- (BOOL)participantsAlreadyContainAnEmail +{ + for (MXKContact* participant in participants) + { + // if it is not a matrix contact or a local contact with a MatrixID + if (participant.matrixIdentifiers.count == 0 && ![MXTools isMatrixUserIdentifier:participant.displayName]) + { + return YES; + } + } + return NO; +} + +- (BOOL)canAddParticipant: (MXKContact*) contact +{ + if (!contact) + { + return YES; + } + + // The following rules will be applied only if the resulting room is going to be encrypted + if (![self.mainSession vc_homeserverConfiguration].encryption.isE2EEByDefaultEnabled) + { + return YES; + } + + // If we have already invited an email, we cannot add another participant + if ([self participantsAlreadyContainAnEmail]) + { + return NO; + } + + // if it is not a matrix contact, nor a local contact with a MatrixID, and if there is already at least one participant, another participant cannot be added. + // bwi: dms can only be created with one participant + if ((contact.matrixIdentifiers.count == 0 && ![MXTools isMatrixUserIdentifier:contact.displayName]) || participants.count > 0) + { + return NO; + } + + // Otherwise, we should be able to add this participant + return YES; +} + +- (void)showAllowOnlyOneInvitByEmailAllowedHeaderView:(BOOL)visible +{ + if (visible) + { + if (!self.onlyOneEmailInvitationView) + { + UIView *headerView = [[UIView alloc] initWithFrame: CGRectZero]; + headerView.translatesAutoresizingMaskIntoConstraints = NO; + + UILabel *label = [[UILabel alloc] initWithFrame: CGRectZero]; + label.numberOfLines = 0; + label.textColor = ThemeService.shared.theme.textSecondaryColor; + label.font = [UIFont systemFontOfSize:14 weight:UIFontWeightLight]; + label.adjustsFontSizeToFitWidth = YES; + + label.text = VectorL10n.roomCreationOnlyOneEmailInvite; + label.translatesAutoresizingMaskIntoConstraints = NO; + [headerView addSubview:label]; + + [NSLayoutConstraint activateConstraints:@[ + [label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:16], + [label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-16], + [label.topAnchor constraintEqualToAnchor:headerView.topAnchor constant:8], + [label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-8], + ]]; + [label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [headerView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + + self.onlyOneEmailInvitationView = headerView; + self.contactsTableView.tableHeaderView = self.onlyOneEmailInvitationView; + + [NSLayoutConstraint activateConstraints:@[ + [headerView.leadingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.leadingAnchor], + [headerView.trailingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.trailingAnchor] + ]]; + [self.contactsTableView.tableHeaderView layoutIfNeeded]; + } + } + else if (self.onlyOneEmailInvitationView != nil) + { + if (self.contactsTableView.tableHeaderView == self.onlyOneEmailInvitationView) + { + self.contactsTableView.tableHeaderView = nil; + } + self.onlyOneEmailInvitationView = nil; + } } - (void)showInviteFriendsFromSourceView:(UIView*)sourceView @@ -372,6 +478,14 @@ if (_isAddParticipantSearchBarEditing) { cell = [contactsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; + MXKContact* contact = [contactsDataSource contactAtIndexPath:indexPath]; + if (![self canAddParticipant:contact]) + { + // Prevent to add it + cell.contentView.alpha = 0.5; + cell.userInteractionEnabled = NO; + cell.accessoryView = nil; + } } else if (indexPath.section == participantsSection) { @@ -538,7 +652,7 @@ // Prepare the invited participant data NSMutableArray *inviteArray = [NSMutableArray array]; - NSMutableArray *invite3PIDArray = [NSMutableArray array]; + NSMutableArray *invite3PIDArray = [NSMutableArray array]; // Check whether some users must be invited for (MXKContact *contact in participants) @@ -599,7 +713,7 @@ // Is it a direct chat? BOOL isDirect = ((inviteArray.count + invite3PIDArray.count == 1) ? YES : NO); - + // In case of a direct chat with only one user id, we open the first available direct chat // or creates a new one (if it doesn't exist). if (isDirect && inviteArray.count) @@ -611,6 +725,19 @@ } else { + // We don't want to create a new direct room for a 3rd party invite if we already have one + NSString *first3rdPartyInvitee = invite3PIDArray.firstObject.address; + if (isDirect && first3rdPartyInvitee) + { + MXRoom *existingRoom = [self.mainSession directJoinedRoomWithUserId:first3rdPartyInvitee]; + if (existingRoom) + { + [self stopActivityIndicator]; + [[AppDelegate theDelegate] showRoom:existingRoom.roomId andEventId:nil withMatrixSession:self.mainSession]; + return; + } + } + // Ensure direct chat are created with equal ops on both sides (the trusted_private_chat preset) MXRoomPreset preset = (isDirect ? kMXRoomPresetTrustedPrivateChat : nil); @@ -640,7 +767,7 @@ roomCreationParameters.isDirect = isDirect; roomCreationParameters.preset = preset; - if (canEnableE2E && roomCreationParameters.invite3PIDArray == nil) + if (canEnableE2E) { roomCreationParameters.initialStateEvents = @[ [MXRoomCreationParameters initialStateEventForEncryptionWithAlgorithm:kMXCryptoMegolmAlgorithm @@ -649,6 +776,9 @@ self->roomCreationRequest = [self.mainSession createRoomWithParameters:roomCreationParameters success:^(MXRoom *room) { + // Update the room summary + [room.summary resetRoomStateData]; + self->roomCreationRequest = nil; [self stopActivityIndicator]; @@ -713,7 +843,29 @@ [contactsDataSource searchWithPattern:searchText forceReset:NO]; } + // $$$ 1.10.5 has the floowing lines +// self->currentSearch = searchText; +// if (searchText != nil && searchText.length > 0) +// { +// MXKContact *contact = nil; +// if ([MXTools isMatrixUserIdentifier:searchText]) +// { +// contact = [[MXKContact alloc] initMatrixContactWithDisplayName:searchText andMatrixID:searchText]; +// +// } +// else if ([MXTools isEmailAddress:searchText]) +// { +// contact = [[MXKContact alloc] initContactWithDisplayName:searchText emails:nil phoneNumbers:nil andThumbnail:nil]; +// } +// +// [self showAllowOnlyOneInvitByEmailAllowedHeaderView: ![self canAddParticipant:contact]]; +// } +// else +// { +// [self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO]; +// } + [contactsDataSource searchWithPattern:searchText forceReset:NO]; self.contactsAreFilteredWithSearch = searchText.length ? YES : NO; } @@ -728,6 +880,7 @@ - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { searchBar.text = nil; + self->currentSearch = nil; self.isAddParticipantSearchBarEditing = NO; // Reset filtering @@ -735,6 +888,8 @@ // Leave search [searchBar resignFirstResponder]; + + [self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO]; } #pragma mark - ContactsTableViewControllerDelegate @@ -773,14 +928,19 @@ } } - if (contact) + if ([self canAddParticipant:contact]) { // Update here the mutable list of participants [participants addObject:contact]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:_searchBarView]; + + // bwi: if there is already at least one participant, the search bar should be hidden + if (participants.count > 0) { + [self hideSearchBar:true]; + } } - - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:_searchBarView]; } #pragma mark - InviteFriendsHeaderViewDelegate @@ -791,4 +951,15 @@ [self showInviteFriendsFromSourceView:button]; } + +#pragma mark - bwi: hideSearchBar +- (void)hideSearchBar:(BOOL)shouldHide +{ + self.searchBarHeader.alpha = shouldHide ? 0.0f : 1.0f; + self.searchBarHeaderHeightConstraint.constant = shouldHide ? 0.0f : 50.0f; + [UIView animateWithDuration:0.2f animations:^{ + [self.view layoutIfNeeded]; + }]; +} + @end diff --git a/Riot/Modules/StartChat/StartChatViewController.xib b/Riot/Modules/StartChat/StartChatViewController.xib index 958aaf904..eb4ea3a6a 100644 --- a/Riot/Modules/StartChat/StartChatViewController.xib +++ b/Riot/Modules/StartChat/StartChatViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -13,6 +13,7 @@ + diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index edc143d91..dbe8d8866 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -57,7 +57,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { // Remove a matrix session. - (void)removeMatrixSession:(MXSession*)mxSession; -// $$$ bwi: which can be deleted /** Display the default onboarding flow. */ diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 62ff5a0f5..938107f76 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -114,10 +114,6 @@ [self updateSideMenuNotifcationIcon]; }]; - // replace link - if (BWIBuildSettings.shared.bwiReplaceFeatureLink) { - [[[BwiNewFeatureHelper alloc] init] replaceFeatureHistoryLink]; - } } - (void)userInterfaceThemeDidChange @@ -627,14 +623,14 @@ inMatrixSession:(MXSession*)mxSession { if (roomParentId) { - NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayname; + NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayName; NSMutableArray *breadcrumbs = [[NSMutableArray alloc] initWithObjects:parentName, nil]; MXSpace *firstRootAncestor = roomParentId ? [mxSession.spaceService firstRootAncestorForRoomWithId:roomParentId] : nil; NSString *rootName = nil; if (firstRootAncestor) { - rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayname; + rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayName; [breadcrumbs insertObject:rootName atIndex:0]; } titleView.breadcrumbView.breadcrumbs = breadcrumbs; @@ -1036,14 +1032,11 @@ switch (tabItemTag) { case TABBAR_FAVOURITES_INDEX: - [self.favouritesViewController setupTopBanner]; [self.favouritesViewController checkAppVersionOutdated]; break; case TABBAR_PEOPLE_INDEX: - [self.peopleViewController setupTopBanner]; break; case TABBAR_ROOMS_INDEX: - [self.roomsViewController setupTopBanner]; break; default: break; @@ -1123,9 +1116,7 @@ [alert addAction:[UIAlertAction actionWithTitle:BWIL10n.bwiAnalyticsAlertOkButton style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { BWIAnalytics.sharedTracker.running = YES; }]]; - [self presentViewController:alert animated:YES completion:^() { - [BWIAnalytics.sharedTracker setPromtShown:YES]; - }]; + [self presentViewController:alert animated:YES completion:nil]; } @end diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index f9afbadc1..4522df16c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -35,6 +35,7 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject { func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) } /** @@ -53,9 +54,12 @@ public class VoiceBroadcastAggregator { private var voiceBroadcastInfoStartEventContent: VoiceBroadcastInfo! private var voiceBroadcastSenderId: String! + public private(set) var voiceBroadcastLastChunkSequence: Int = 0 + private var referenceEventsListener: Any? private var events: [MXEvent] = [] + private var undecryptableEvents: Set = [] public private(set) var voiceBroadcast: VoiceBroadcast! { didSet { @@ -82,7 +86,7 @@ public class VoiceBroadcastAggregator { try buildVoiceBroadcastStartContent() } - + private func buildVoiceBroadcastStartContent() throws { guard let event = session.store.event(withEventId: voiceBroadcastStartEventId, inRoom: room.roomId), let eventContent = VoiceBroadcastInfo(fromJSON: event.content), @@ -116,7 +120,11 @@ public class VoiceBroadcastAggregator { @objc private func eventDidDecrypt(sender: Notification) { guard let event = sender.object as? MXEvent else { return } - + + if undecryptableEvents.remove(event) != nil { + delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: undecryptableEvents) + } + self.handleEvent(event: event) } @@ -136,8 +144,19 @@ public class VoiceBroadcastAggregator { private func updateVoiceBroadcast(event: MXEvent) { guard event.sender == self.voiceBroadcastSenderId, let relatedEventId = event.relatesTo?.eventId, - relatedEventId == self.voiceBroadcastStartEventId, - event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { + relatedEventId == self.voiceBroadcastStartEventId else { + return + } + + // Handle decryption errors + if event.decryptionError != nil { + self.undecryptableEvents.insert(event) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + + return + } + + guard event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { return } @@ -168,7 +187,10 @@ public class VoiceBroadcastAggregator { let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else { return } - + // For .pause and .stopped, update the last chunk sequence + if [.stopped, .paused].contains(state) { + self.voiceBroadcastLastChunkSequence = voiceBroadcastInfo.lastChunkSequence + } self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state) } } @@ -187,14 +209,22 @@ public class VoiceBroadcastAggregator { } self.events.removeAll() + self.undecryptableEvents.removeAll() + self.voiceBroadcastLastChunkSequence = 0 let filteredChunk = response.chunk.filter { event in event.sender == self.voiceBroadcastSenderId && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil } - self.events.append(contentsOf: filteredChunk) - + + let decryptionFailure = response.chunk.filter { event in + event.sender == self.voiceBroadcastSenderId && + event.decryptionError != nil + } + self.undecryptableEvents.formUnion(decryptionFailure) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage] self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes, onEvent: { [weak self] event, direction, roomState in self?.handleEvent(event: event, direction: direction, roomState: roomState) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index 8c707eab2..66bd6dd57 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -186,7 +186,7 @@ public class VoiceBroadcastService: NSObject { return } - self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), + let httpOperation = self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), content: stateEventContent, stateKey: stateKey) { [weak self] response in guard let self = self else { return } @@ -199,6 +199,9 @@ public class VoiceBroadcastService: NSObject { } taskCompleted() } + + // No retry to send the request + httpOperation.maxNumberOfTries = 0 } } } @@ -295,7 +298,7 @@ extension MXRoom { threadId: String? = nil, sequence: UInt, success: @escaping ((String?) -> Void), - failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? { + failure: @escaping ((Swift.Error?) -> Void)) -> MXHTTPOperation? { guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference, eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else { failure(VoiceBroadcastServiceError.unknown) diff --git a/Riot/Utils/EventFormatter+DTCoreTextFix.h b/Riot/Utils/EventFormatter+DTCoreTextFix.h deleted file mode 100644 index 90be432c4..000000000 --- a/Riot/Utils/EventFormatter+DTCoreTextFix.h +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -@import Foundation; - -#import "EventFormatter.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface EventFormatter(DTCoreTextFix) - -// Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168) -+ (void)fixDTCoreTextFont; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Riot/Utils/EventFormatter+DTCoreTextFix.m b/Riot/Utils/EventFormatter+DTCoreTextFix.m deleted file mode 100644 index 110d036fc..000000000 --- a/Riot/Utils/EventFormatter+DTCoreTextFix.m +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#import "EventFormatter+DTCoreTextFix.h" - -@import UIKit; -@import CoreText; -@import ObjectiveC; - -#pragma mark - UIFont DTCoreText fix - -@interface UIFont (vc_DTCoreTextFix) - -+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont; - -@end - -@implementation UIFont (vc_DTCoreTextFix) - -+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont { - NSString *fontName = (__bridge_transfer NSString *)CTFontCopyName(ctFont, kCTFontPostScriptNameKey); - - CGFloat fontSize = CTFontGetSize(ctFont); - UIFont *font = [UIFont fontWithName:fontName size:fontSize]; - - // On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI" - // In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits. - if ([font.familyName.lowercaseString containsString:@"times"]) - { - UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont); - - UIFontDescriptor *systemFontDescriptor = [UIFont systemFontOfSize:fontSize].fontDescriptor; - - UIFontDescriptor *finalFontDescriptor = [systemFontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; - font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize]; - } - - return font; -} - -@end - -#pragma mark - Implementation - -@implementation EventFormatter(DTCoreTextFix) - -// DTCoreText iOS 13 fix. See issue and comment here: https://github.com/Cocoanetics/DTCoreText/issues/1168#issuecomment-583541514 -+ (void)fixDTCoreTextFont -{ - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - Class originalClass = object_getClass([UIFont class]); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" - SEL originalSelector = @selector(fontWithCTFont:); // DTCoreText method we're overriding - SEL ourSelector = @selector(vc_fixedFontWithCTFont:); // Use custom implementation -#pragma clang diagnostic pop - - Method originalMethod = class_getClassMethod(originalClass, originalSelector); - Method swizzledMethod = class_getClassMethod(originalClass, ourSelector); - - method_exchangeImplementations(originalMethod, swizzledMethod); - }); -} - -@end diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 874d462d5..321f8d23e 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -26,7 +26,6 @@ #import "MXDecryptionResult.h" #import "DecryptionFailureTracker.h" -#import "EventFormatter+DTCoreTextFix.h" #import #pragma mark - Constants definitions @@ -54,11 +53,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; @implementation EventFormatter -+ (void)load -{ - [self fixDTCoreTextFont]; -} - - (void)initDateTimeFormatters { [super initDateTimeFormatters]; @@ -113,6 +107,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; @"event_id": event.eventId ?: @"unknown" }); string = [[NSAttributedString alloc] initWithString:[VectorL10n noticeErrorUnformattableEvent] attributes:@{ + NSForegroundColorAttributeName: self.sendingTextColor, NSFontAttributeName: [self encryptedMessagesTextFont] }]; } @@ -365,7 +360,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; attributes:@{ NSLinkAttributeName: linkActionString, NSForegroundColorAttributeName: self.sendingTextColor, - NSFontAttributeName: self.encryptedMessagesTextFont + NSFontAttributeName: self.encryptedMessagesTextFont, + NSUnderlineStyleAttributeName: [NSNumber numberWithInt:NSUnderlineStyleSingle] }]]; [attributedStringWithRerequestMessage appendAttributedString: @@ -390,8 +386,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", [VectorL10n eventFormatterMessageEditedMention]] attributes:@{ NSLinkAttributeName: linkActionString, - // NOTE: Color is curretly overidden by UIText.tintColor as we use `NSLinkAttributeName`. - // If we use UITextView.linkTextAttributes to set link color we will also have the issue that color will be the same for all kind of links. NSForegroundColorAttributeName: self.editionMentionTextColor, NSFontAttributeName: self.editionMentionTextFont }]]; @@ -493,6 +487,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; self.bingTextColor = ThemeService.shared.theme.noticeColor; self.encryptingTextColor = ThemeService.shared.theme.textPrimaryColor; self.sendingTextColor = ThemeService.shared.theme.textPrimaryColor; + self.linksColor = ThemeService.shared.theme.colors.links; self.errorTextColor = ThemeService.shared.theme.textPrimaryColor; self.showEditionMention = YES; self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor; @@ -554,8 +549,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } #pragma mark - MXRoomSummaryUpdating -- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState { - +- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState +{ // Do not display voice broadcast chunk in last message. if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType]) { @@ -563,31 +558,81 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } // Update last message if we have a voice broadcast in the room. - if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) + MXEvent *lastVoiceBroadcastInfoEvent = [self lastVoiceBroadcastInfoEventWithEvent:event roomState:roomState]; + if (lastVoiceBroadcastInfoEvent != nil) { - return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:event roomState:roomState]; - } - else - { - MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; - if (stateEvent && ![VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]) + MXEvent *voiceBroadcastInfoStartedEvent = [self voiceBroadcastInfoStartedEventWithEvent:lastVoiceBroadcastInfoEvent + roomId:summary.roomId + session:session]; + if (voiceBroadcastInfoStartedEvent != nil + && !(voiceBroadcastInfoStartedEvent.isRedactedEvent || [voiceBroadcastInfoStartedEvent.eventId isEqualToString:event.redacts])) { - return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:stateEvent roomState:roomState]; + return [self session:session + updateRoomSummary:summary +withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent + voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent + roomState:roomState]; } } BOOL updated = [super session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState]; - if (updated) { + if (updated) + { // Force the default text color for the last message (cancel highlighted message color) NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText]; - [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor range:NSMakeRange(0, lastEventDescription.length)]; + [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor + range:NSMakeRange(0, lastEventDescription.length)]; summary.lastMessage.attributedText = lastEventDescription; } return updated; } + +- (MXEvent *)lastVoiceBroadcastInfoEventWithEvent:(MXEvent *)event roomState:(MXRoomState *)roomState +{ + MXEvent *voiceBroadcastInfoEvent = nil; + VoiceBroadcastInfo *info = nil; + if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) + { + info = [VoiceBroadcastInfo modelFromJSON: event.content]; + + if (info != nil) + { + voiceBroadcastInfoEvent = event; + } + } + else + { + MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; + if (stateEvent != nil) + { + info = [VoiceBroadcastInfo modelFromJSON: stateEvent.content]; + if (info != nil && ![VoiceBroadcastInfo isStoppedFor:info.state]) + { + voiceBroadcastInfoEvent = stateEvent; + } + } + } + + return voiceBroadcastInfoEvent; +} + +- (MXEvent *)voiceBroadcastInfoStartedEventWithEvent:(MXEvent *)voiceBroadcastInfoEvent roomId:(NSString *)roomId session:(MXSession *)session +{ + VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: voiceBroadcastInfoEvent.content]; + if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state]) + { + return voiceBroadcastInfoEvent; + } + else + { + // Search for the event only in the store to avoid network calls while updating the room summary (this a synchronous process and we cannot delay it). + return [mxSession.store eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId]; + } +} + - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray *)stateEvents roomState:(MXRoomState *)roomState { BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState]; @@ -611,18 +656,29 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; return updated; } -- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent roomState:(MXRoomState *)roomState +- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent voiceBroadcastInfoStartedEvent:(MXEvent *)voiceBroadcastInfoStartedEvent roomState:(MXRoomState *)roomState { - [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:stateEvent]]; - if (summary.lastMessage.others == nil) + BOOL isStoppedVoiceBroadcast = [VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]; + + if ([summary.lastMessage.eventId isEqualToString:voiceBroadcastInfoStartedEvent.eventId]) { - summary.lastMessage.others = [NSMutableDictionary dictionary]; + if (!isStoppedVoiceBroadcast) + { + return NO; + } + } + else + { + [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:voiceBroadcastInfoStartedEvent]]; + if (summary.lastMessage.others == nil) + { + summary.lastMessage.others = [NSMutableDictionary dictionary]; + } } - summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES]; NSAttributedString *attachmentString = nil; UIColor *textColor; - if ([VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]) + if (isStoppedVoiceBroadcast) { textColor = ThemeService.shared.theme.textSecondaryColor; NSString *senderDisplayName; @@ -635,6 +691,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; senderDisplayName = [self senderDisplayNameForEvent:stateEvent withRoomState:roomState]; summary.lastMessage.text = [VectorL10n noticeVoiceBroadcastEnded:senderDisplayName]; } + summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES]; } else { @@ -646,6 +703,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastLive; + summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:voiceBroadcastInfoStartedEvent withTime:YES]; } // Compute the attribute text message diff --git a/Riot/Utils/HTMLFormatter.swift b/Riot/Utils/HTMLFormatter.swift index 0338cda07..8eb30e555 100644 --- a/Riot/Utils/HTMLFormatter.swift +++ b/Riot/Utils/HTMLFormatter.swift @@ -47,10 +47,9 @@ class HTMLFormatter: NSObject { var options: [AnyHashable: Any] = [ DTUseiOS6Attributes: true, - DTDefaultFontFamily: font.familyName, - DTDefaultFontName: font.fontName, - DTDefaultFontSize: font.pointSize, + DTDefaultFontDescriptor: font.fontDescriptor, DTDefaultLinkDecoration: false, + DTDefaultLinkColor: ThemeService.shared().theme.colors.links, DTWillFlushBlockCallBack: sanitizeCallback ] options.merge(extraOptions) { (_, new) in new } diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index d71531f6f..2c23d7b3f 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -39,7 +39,11 @@ presenceText = [VectorL10n roomParticipantsIdle]; break; - case MXPresenceUnknown: // Do like matrix-js-sdk + case MXPresenceUnknown: + // Fix https://github.com/vector-im/element-ios/issues/6597 + // Return nil because we don't want to display anything if the status is unknown + return nil; + case MXPresenceOffline: presenceText = [VectorL10n roomParticipantsOffline]; break; diff --git a/Riot/target-bum-beta.yml b/Riot/target-bum-beta.yml index a178ed1c0..1351d3fa0 100644 --- a/Riot/target-bum-beta.yml +++ b/Riot/target-bum-beta.yml @@ -42,6 +42,7 @@ targets: - target: RiotNSE - target: DesignKit - target: CommonKit + - package: AnalyticsEvents - package: Mapbox - package: OrderedCollections - package: SwiftOGG diff --git a/Riot/target-messenger.yml b/Riot/target-messenger.yml index a5466cd9e..e12959cef 100644 --- a/Riot/target-messenger.yml +++ b/Riot/target-messenger.yml @@ -42,6 +42,7 @@ targets: - target: RiotNSE - target: DesignKit - target: CommonKit + - package: AnalyticsEvents - package: Mapbox - package: OrderedCollections - package: SwiftOGG diff --git a/Riot/target.yml b/Riot/target.yml index 228883f55..48f4a6ebf 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -39,12 +39,14 @@ targets: - target: RiotNSE - target: DesignKit - target: CommonKit + - package: AnalyticsEvents - package: Mapbox - package: OrderedCollections - package: SwiftOGG - package: Lottie - package: WysiwygComposer - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index e71651d03..8a68f5b4d 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -42,6 +42,7 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? + private var isCryptoSDKEnabled = false /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -268,14 +269,18 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() - NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in - MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) - }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in - userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion) - }) + + NotificationService.backgroundSyncService = MXBackgroundSyncService( + withCredentials: userAccount.mxCredentials, + isCryptoSDKEnabled: isCryptoSDKEnabled, + persistTokenDataHandler: { persistTokenDataHandler in + MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) + }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in + userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion) + }) MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: AFTER") self.logMemory() } @@ -287,6 +292,16 @@ class NotificationService: UNNotificationServiceExtension { } } + /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require + /// rebuilding `MXBackgroundSyncService` + private func hasChangedCryptoSDK() -> Bool { + guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else { + return false + } + isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK + return true + } + /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content @@ -299,7 +314,7 @@ class NotificationService: UNNotificationServiceExtension { // If a room summary is available, use the displayname for the best attempt title. guard let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId) else { return } - guard let roomDisplayName = roomSummary.displayname else { return } + guard let roomDisplayName = roomSummary.displayName else { return } bestAttemptContents[eventId]?.title = roomDisplayName // At this stage we don't know the message type, so leave the body as set in didReceive. @@ -441,9 +456,15 @@ class NotificationService: UNNotificationServiceExtension { var ignoreBadgeUpdate = false var threadIdentifier: String? = roomId let currentUserId = account.mxCredentials.userId - let roomDisplayName = roomSummary?.displayname + let roomDisplayName = roomSummary?.displayName let pushRule = NotificationService.backgroundSyncService.pushRule(matching: event, roomState: roomState) - + + // if the push rule must not be notified we complete and return + if pushRule?.dontNotify == true { + onComplete(nil, false) + return + } + switch event.eventType { case .callInvite: let offer = event.content["offer"] as? [AnyHashable: Any] @@ -989,3 +1010,10 @@ class NotificationService: UNNotificationServiceExtension { return String(format: format, locale: locale, arguments: args) } } + +private extension MXPushRule { + var dontNotify: Bool { + let actions = (actions as? [MXPushRuleAction]) ?? [] + return actions.contains { $0.actionType == MXPushRuleActionTypeDontNotify } + } +} diff --git a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h index 6409af92f..5db20c745 100644 --- a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h +++ b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h @@ -23,4 +23,6 @@ #import "BuildInfo.h" +#import "ThemeService.h" + #endif /* RiotNSE_Bridging_Header_h */ diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index a0c042646..82507f6f9 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -32,7 +32,9 @@ targets: type: app-extension dependencies: + - package: AnalyticsEvents - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig @@ -48,6 +50,9 @@ targets: - path: ../bwi/NotificationTimes/NotificationTimesRoomSettings.swift - path: ../bwi/NotificationTimes/NotificationTimesWeekday.swift - path: ../bwi/NotificationTimes/Date+fromHourMinute.swift + - path: ../bwi/MatomoAnalytics/BWIAnalytics.swift + - path: ../bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift + - path: ../bwi/MatomoAnalytics/E2EEError.swift - path: ../Riot/Managers/Settings/RiotSettings.swift - path: ../Config/BuildSettings.swift - path: ../Config/BWIBuildSettings.swift @@ -55,6 +60,7 @@ targets: - path: ../bwi/UserAgent/UserAgentService.swift - path: ../Riot/Utils/DataProtectionHelper.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Riot/Managers/PushNotification/PushNotificationStore.swift - path: ../Riot/Modules/SetPinCode/PinCodePreferences.swift - path: ../Riot/Modules/SetPinCode/SetupBiometrics/BiometricsAuthenticationPresenter.swift @@ -62,6 +68,8 @@ targets: - path: ../Riot/Managers/Locale/LocaleProvider.swift - path: ../Riot/Managers/Locale/LocaleProviderType.swift - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift + - path: ../Riot/Managers/Serialization/SerializationService.swift + - path: ../Riot/Managers/Serialization/SerializationServiceType.swift - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Categories/MXEvent.swift - path: ../Riot/Generated/Strings.swift @@ -88,3 +96,5 @@ targets: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK + - path: ../Riot/Managers/Theme + - path: ../Riot/Categories/UIColor.swift diff --git a/RiotShareExtension/Shared/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m index e097b5da7..8133e67f1 100644 --- a/RiotShareExtension/Shared/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -141,7 +141,7 @@ { for (NSString* pattern in patternsList) { - if (cellData.roomSummary.displayname && [cellData.roomSummary.displayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) + if (cellData.roomSummary.displayName && [cellData.roomSummary.displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) { [self.visibleRoomCellDatas addObject:cellData]; break; diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 614df9398..cc5c6853c 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -32,7 +32,9 @@ targets: type: app-extension dependencies: + - package: AnalyticsEvents - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig @@ -57,6 +59,7 @@ targets: - path: ../Riot/Categories/MXRoom+Riot.m - path: ../Config/Configurable.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Riot/Utils/UserNameColorGenerator.swift - path: ../Riot/Utils/HTMLFormatter.swift - path: ../Riot/Categories/MXRoomSummary+Riot.m diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 7d222aa31..559c67d17 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -47,7 +47,7 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { return ( [promptType, viewModel], AnyView(AnalyticsPrompt(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift index 204eddd57..dac56608d 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift @@ -33,6 +33,8 @@ enum AuthenticationLoginViewModelResult: CustomStringConvertible { case fallback /// Continue with QR login case qrLogin + /// bwi: register info + case register /// A string representation of the result, ignoring any associated values that could leak PII. var description: String { @@ -51,6 +53,8 @@ enum AuthenticationLoginViewModelResult: CustomStringConvertible { return "fallback" case .qrLogin: return "qrLogin" + case .register: + return "register" } } } @@ -112,6 +116,8 @@ enum AuthenticationLoginViewAction { case continueWithSSO(SSOIdentityProvider) /// Continue using QR login case qrLogin + /// bwi: register info + case register } enum AuthenticationLoginErrorType: Hashable { diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift index f1180c1d1..64ee83363 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift @@ -52,6 +52,8 @@ class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, Authentica Task { await callback?(.continueWithSSO(provider)) } case .qrLogin: Task { await callback?(.qrLogin) } + case .register: + Task { await callback?(.register) } } } @@ -78,4 +80,20 @@ class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, Authentica state.bindings.alertInfo = AlertInfo(id: type) } } + + // bwi: show custom alert + @MainActor func displayInfoAlert(_ type: AuthenticationLoginViewAction) { + switch type { + case .forgotPassword: + state.bindings.alertInfo = AlertInfo(id: .unknown, + title: BWIL10n.authForgotPassword, + message: BWIL10n.bwiAuthForgotPasswordAlertText) + case .register: + state.bindings.alertInfo = AlertInfo(id: .unknown, + title: BWIL10n.bwiAuthRegisterAlertTitle, + message: BWIL10n.bwiAuthRegisterAlertText) + default: + return + } + } } diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift index 87ef46fce..bbd8f6a7b 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift @@ -31,4 +31,8 @@ protocol AuthenticationLoginViewModelProtocol { /// Display an error to the user. /// - Parameter type: The type of error to be displayed. @MainActor func displayError(_ type: AuthenticationLoginErrorType) + + /// bwi: Display an info alert. + /// - Parameter type: The type of the message to be displayed. + @MainActor func displayInfoAlert(_ type: AuthenticationLoginViewAction) } diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index a1b899ad6..f71f6c0c9 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -123,7 +123,12 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { case .parseUsername(let username): self.parseUsername(username) case .forgotPassword: - self.showForgotPasswordScreen() + // bwi: show info alert + if BWIBuildSettings.shared.forgotPasswordInformationAlert { + self.authenticationLoginViewModel.displayInfoAlert(.forgotPassword) + } else { + self.showForgotPasswordScreen() + } case .login(let username, let password): self.login(username: username, password: password) case .continueWithSSO(let identityProvider): @@ -132,6 +137,9 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { self.callback?(.fallback) case .qrLogin: self.showQRLoginScreen() + case .register: + // bwi: show info alert + self.authenticationLoginViewModel.displayInfoAlert(.register) } } } diff --git a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift index ff09c7624..662c26383 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift @@ -20,6 +20,9 @@ struct AuthenticationLoginScreen: View { // MARK: - Properties // MARK: Private + private enum CustomText { + case username, submit + } @Environment(\.theme) private var theme: ThemeSwiftUI @@ -35,8 +38,8 @@ struct AuthenticationLoginScreen: View { VStack { ScrollView { VStack(spacing: 0) { - if BWIBuildSettings.shared.bumLoginFlowLayout { - ServerIcon(image: Asset.SharedImages.loginFlowLogo, size: OnboardingMetrics.iconSize) + if BWIBuildSettings.shared.bumLoginFlowLayout || BWIBuildSettings.shared.bwiLoginFlowLayout { + ServerIcon(image: nil, size: OnboardingMetrics.iconSize) .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) .padding(.bottom, 16) } else { @@ -45,14 +48,19 @@ struct AuthenticationLoginScreen: View { .padding(.bottom, 28) } - serverInfo - .padding(.leading, 12) - .padding(.bottom, 16) + if !BWIBuildSettings.shared.bwiLoginFlowLayout { + serverInfo + .padding(.leading, 12) + .padding(.bottom, 16) - Rectangle() - .fill(theme.colors.quinaryContent) - .frame(height: 1) - .padding(.bottom, 22) + Rectangle() + .fill(theme.colors.quinaryContent) + .frame(height: 1) + .padding(.bottom, 22) + } else { + // bwi: show cutom header + authLoginHeaderlineText + } if BWIBuildSettings.shared.bumLoginFlowLayout { loginDescription @@ -115,7 +123,7 @@ struct AuthenticationLoginScreen: View { /// The form with text fields for username and password, along with a submit button. var loginForm: some View { VStack(spacing: 14) { - RoundedBorderTextField(placeHolder: BWIL10n.authenticationLoginUsername, + RoundedBorderTextField(placeHolder: getCustomText(text: .username), text: $viewModel.username, isFirstResponder: false, configuration: UIKitTextInputConfiguration(returnKeyType: .next, @@ -134,8 +142,8 @@ struct AuthenticationLoginScreen: View { onEditingChanged: passwordEditingChanged, onCommit: submit) .accessibilityIdentifier("passwordTextField") - - if !BWIBuildSettings.shared.bumLoginFlowLayout { + // bwi: hide nv forgot password button + if !BWIBuildSettings.shared.bumLoginFlowLayout && !BWIBuildSettings.shared.bwiLoginFlowLayout { Button { viewModel.send(viewAction: .forgotPassword) } label: { Text(VectorL10n.authenticationLoginForgotPassword) .font(theme.fonts.body) @@ -145,11 +153,20 @@ struct AuthenticationLoginScreen: View { } Button(action: submit) { - Text(VectorL10n.next) + Text(getCustomText(text: .submit)) } .buttonStyle(PrimaryActionButtonStyle()) .disabled(!viewModel.viewState.canSubmit) .accessibilityIdentifier("nextButton") + .padding([.vertical], BWIBuildSettings.shared.bwiLoginFlowLayout ? 36 : 0) + + + if BWIBuildSettings.shared.authScreenShowForgotPassword { + forgotPasswordButton + } + if BWIBuildSettings.shared.bwiEnableRegisterInfo { + registerButton + } } } @@ -239,6 +256,55 @@ struct AuthenticationLoginScreen: View { func qrLogin() { viewModel.send(viewAction: .qrLogin) } + + // bwi: custom forgot password button + var forgotPasswordButton: some View { + Button { + viewModel.send(viewAction: .forgotPassword) + } label: { + Text(BWIL10n.authForgotPassword) + .font(theme.fonts.body) + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 8) + } + + // bwi: custom register button + var registerButton: some View { + Button { + viewModel.send(viewAction: .register) + } label: { + Text(BWIL10n.bwiAuthRegisterButtonTitle) + .font(theme.fonts.body) + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 8) + } + + // bwi: custom header + var authLoginHeaderlineText: some View { + VStack(alignment: .leading) { + Text(BWIL10n.authLoginHeadlineText) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + Text(BWIL10n.authLoginSubheadlineText) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.secondaryContent) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding([.vertical], 36) + } + + + // bwi: get app specific text + private func getCustomText(text: CustomText) -> String { + switch text { + case .submit: + return BWIBuildSettings.shared.bwiLoginFlowLayout ? BWIL10n.authenticationServerSelectionSubmitButtonTitle : VectorL10n.next + case .username: + return BWIBuildSettings.shared.bwiLoginFlowLayout ? BWIL10n.authUserIdPlaceholder : BWIL10n.authenticationLoginUsername + } + } } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index fa743fd2b..05506b4bd 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -171,8 +171,16 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { @MainActor private func processQRLoginCode(_ code: QRLoginCode) async { MXLog.debug("[QRLoginService] processQRLoginCode: \(code)") - state = .connectingToDevice - + + // we check these first so that we can show a more specific error message + guard code.rendezvous.transport?.type == "org.matrix.msc3886.http.v1", + let algorithm = RendezvousChannelAlgorithm(rawValue: code.rendezvous.algorithm) else { + MXLog.error("[QRLoginService] Unsupported algorithm or transport") + state = .failed(error: .deviceNotSupported) + return + } + + // so, this is of an expected algorithm so any bad data can be considered an invalid QR code guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue, let uri = code.rendezvous.transport?.uri, let rendezvousURL = URL(string: uri), @@ -182,9 +190,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { return } + state = .connectingToDevice + let transport = RendezvousTransport(baseURL: BuildSettings.rendezvousServerBaseURL, rendezvousURL: rendezvousURL) - let rendezvousService = RendezvousService(transport: transport) + let rendezvousService = RendezvousService(transport: transport, algorithm: algorithm) self.rendezvousService = rendezvousService MXLog.debug("[QRLoginService] Joining the rendezvous at \(rendezvousURL)") diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift index 823a4983c..c5b48a74f 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift @@ -30,6 +30,7 @@ enum QRLoginServiceMode { enum QRLoginServiceError: Error, Equatable { case noCameraAccess case noCameraAvailable + case deviceNotSupported case invalidQR case requestDenied case requestTimedOut diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift index 0e363e549..656ab5bb4 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift @@ -55,6 +55,9 @@ class AuthenticationQRLoginFailureViewModel: AuthenticationQRLoginFailureViewMod case .invalidQR: self.state.failureText = VectorL10n.authenticationQrLoginFailureInvalidQr self.state.retryButtonVisible = true + case .deviceNotSupported: + self.state.failureText = VectorL10n.authenticationQrLoginFailureDeviceNotSupported + self.state.retryButtonVisible = true case .requestDenied: self.state.failureText = VectorL10n.authenticationQrLoginFailureRequestDenied self.state.retryButtonVisible = false diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift index 5747c86bb..f55bf6d96 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift @@ -24,6 +24,7 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable // with specific, minimal associated data that will allow you // mock that screen. case invalidQR + case deviceNotSupported case requestDenied case requestTimedOut @@ -35,7 +36,7 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable /// A list of screen state definitions static var allCases: [MockAuthenticationQRLoginFailureScreenState] { // Each of the presence statuses - [.invalidQR, .requestDenied, .requestTimedOut] + [.invalidQR, .deviceNotSupported, .requestDenied, .requestTimedOut] } /// Generate the view struct for the screen state. @@ -45,6 +46,8 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable switch self { case .invalidQR: viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .invalidQR))) + case .deviceNotSupported: + viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .deviceNotSupported))) case .requestDenied: viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .requestDenied))) case .requestTimedOut: diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift index 829349d78..33c82c949 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift @@ -32,6 +32,20 @@ class AuthenticationQRLoginFailureUITests: MockScreenTestCase { XCTAssertTrue(cancelButton.isEnabled) } + func testDeviceNotSupported() { + app.goToScreenWithIdentifier(MockAuthenticationQRLoginFailureScreenState.deviceNotSupported.title) + + XCTAssertTrue(app.staticTexts["failureLabel"].exists) + + let retryButton = app.buttons["retryButton"] + XCTAssertTrue(retryButton.exists) + XCTAssertTrue(retryButton.isEnabled) + + let cancelButton = app.buttons["cancelButton"] + XCTAssertTrue(cancelButton.exists) + XCTAssertTrue(cancelButton.isEnabled) + } + func testRequestDenied() { app.goToScreenWithIdentifier(MockAuthenticationQRLoginFailureScreenState.requestDenied.title) diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift index 7390909c8..d029c4a35 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift @@ -48,7 +48,7 @@ struct AuthenticationServerSelectionScreen: View { .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) .padding(.bottom, 16) - if BWIBuildSettings.shared.scanServerQRCode { + if BWIBuildSettings.shared.allowScanServerQRCode { scanButton .alert(isPresented: $showAlertForMissingCameraAuthorization) { Alert( @@ -67,6 +67,9 @@ struct AuthenticationServerSelectionScreen: View { message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage), dismissButton: .default(Text(VectorL10n.ok))) } + if BWIBuildSettings.shared.authScreenShowTestServerOptions { + serverSelectionButton + } } .readableFrame() .padding(.horizontal, 16) @@ -93,8 +96,8 @@ struct AuthenticationServerSelectionScreen: View { /// The title, message and icon at the top of the screen. var header: some View { VStack(spacing: 8) { - if BWIBuildSettings.shared.bumLoginFlowLayout { - ServerIcon(image: Asset.SharedImages.loginFlowLogo, size: OnboardingMetrics.iconSize) + if BWIBuildSettings.shared.bumLoginFlowLayout || BWIBuildSettings.shared.bwiLoginFlowLayout { + ServerIcon(image: nil, size: OnboardingMetrics.iconSize) .padding(.bottom, 16) } else { OnboardingIconImage(image: Asset.Images.welcomeExperience1) @@ -221,6 +224,24 @@ struct AuthenticationServerSelectionScreen: View { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } + + // bwi: show server selection button + var serverSelectionButton: some View { + VStack() { + Menu(content: { + ForEach(ServerURLHelper.shared.serverSettings, id: \.self) { server in + Button(server.name, action: { + viewModel.homeserverAddress = server.serverUrl + }) + } + }, label: { + Button(action: { return }) { + Text(BWIL10n.bwiAuthBetaSelectionButtonTitle) + } + .buttonStyle(PrimaryActionButtonStyle()) + }) + } + } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift index 02369980c..ea4488244 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift @@ -32,10 +32,6 @@ class AvatarService: AvatarServiceProtocol { private let mediaManager: MXMediaManager - static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol { - AvatarService(mediaManager: mediaManager) - } - init(mediaManager: MXMediaManager) { self.mediaManager = mediaManager } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index 4f51f574a..3ddd4d472 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -19,17 +19,18 @@ import SwiftUI struct AvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI - @Environment(\.dependencies) var dependencies: DependencyContainer - @StateObject var viewModel = AvatarViewModel() + @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? var matrixItemId: String var displayName: String? var size: AvatarSize + @State private var avatar: AvatarViewState = .empty + var body: some View { Group { - switch viewModel.viewState { + switch avatar { case .empty: ProgressView() case .placeholder(let firstCharacter, let colorIndex): @@ -43,14 +44,16 @@ struct AvatarImage: View { .frame(maxWidth: CGFloat(size.rawValue), maxHeight: CGFloat(size.rawValue)) .clipShape(Circle()) .onAppear { - viewModel.inject(dependencies: dependencies) - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: displayName, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) + avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count) + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size ) { newState in + avatar = newState + } } } } @@ -95,7 +98,7 @@ struct AvatarImage_Previews: PreviewProvider { AvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge) } } - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index 8e967fe18..708c26a2b 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -19,17 +19,18 @@ import SwiftUI struct SpaceAvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI - @Environment(\.dependencies) var dependencies: DependencyContainer - @StateObject var viewModel = AvatarViewModel() + @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? var matrixItemId: String var displayName: String? var size: AvatarSize + @State private var avatar: AvatarViewState = .empty + var body: some View { Group { - switch viewModel.viewState { + switch avatar { case .empty: ProgressView() case .placeholder(let firstCharacter, let colorIndex): @@ -49,24 +50,27 @@ struct SpaceAvatarImage: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } } - .onChange(of: displayName, perform: { value in - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: value, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) - }) + .onChange(of: displayName) { value in + guard case .placeholder = avatar else { return } + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: value, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size) { newState in + avatar = newState + } + } .onAppear { - viewModel.inject(dependencies: dependencies) - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: displayName, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) + avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count) + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size) { newState in + avatar = newState + } } } } @@ -99,7 +103,7 @@ struct LiveAvatarImage_Previews: PreviewProvider { SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge) } } - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 433fb9cba..68037f552 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -19,13 +19,25 @@ import DesignKit import Foundation /// Simple ViewModel that supports loading an avatar image -class AvatarViewModel: InjectableObject, ObservableObject { - @Inject var avatarService: AvatarServiceProtocol +final class AvatarViewModel: ObservableObject { + private let avatarService: AvatarServiceProtocol - @Published private(set) var viewState = AvatarViewState.empty + init(avatarService: AvatarServiceProtocol) { + self.avatarService = avatarService + } private var cancellables = Set() + func placeholderAvatar(matrixItemId: String, + displayName: String?, + colorCount: Int) -> AvatarViewState { + let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, + matrixItemId: matrixItemId, + colorCount: colorCount) + + return .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) + } + /// Load an avatar /// - Parameters: /// - mxContentUri: The matrix content URI of the avatar. @@ -37,14 +49,10 @@ class AvatarViewModel: InjectableObject, ObservableObject { matrixItemId: String, displayName: String?, colorCount: Int, - avatarSize: AvatarSize) { - let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, - matrixItemId: matrixItemId, - colorCount: colorCount) - - viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) - + avatarSize: AvatarSize, + avatarCompletion: @escaping (AvatarViewState) -> Void) { guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else { + avatarCompletion(placeholderAvatar(matrixItemId: matrixItemId, displayName: displayName, colorCount: colorCount)) return } @@ -52,9 +60,16 @@ class AvatarViewModel: InjectableObject, ObservableObject { .sink { completion in guard case let .failure(error) = completion else { return } UILog.error("[AvatarService] Failed to retrieve avatar", context: error) + // No need to call the completion, there's nothing we can do and the error is logged. } receiveValue: { image in - self.viewState = .avatar(image) + avatarCompletion(.avatar(image)) } .store(in: &cancellables) } } + +extension AvatarViewModel { + static func withMockedServices() -> AvatarViewModel { + .init(avatarService: MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift deleted file mode 100644 index d09fa87f4..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// Used for storing and resolving dependencies at runtime. -struct DependencyContainer { - // Stores the dependencies with type information removed. - private var dependencyStore: [String: Any] = [:] - - /// Resolve a dependency by type. - /// - /// Given a particular `Type` (Inferred from return type), - /// generate a key and retrieve from storage. - /// - /// - Returns: The resolved dependency. - func resolve() -> T { - let key = String(describing: T.self) - guard let t = dependencyStore[key] as? T else { - fatalError("No provider registered for type \(T.self)") - } - return t - } - - /// Register a dependency. - /// - /// Given a dependency, generate a key from it's `Type` and save in storage. - /// - Parameter dependency: The dependency to register. - mutating func register(dependency: T) { - let key = String(describing: T.self) - dependencyStore[key] = dependency - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift deleted file mode 100644 index 4bde8956e..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import SwiftUI - -/// An Environment Key for retrieving runtime dependencies. -/// -/// Dependencies are to be injected into `ObservableObjects` -/// that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View). -private struct DependencyContainerKey: EnvironmentKey { - static let defaultValue = DependencyContainer() -} - -extension EnvironmentValues { - var dependencies: DependencyContainer { - get { self[DependencyContainerKey.self] } - set { self[DependencyContainerKey.self] = newValue } - } -} - -extension View { - /// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container. - /// - /// Important: When adding a dependency to cast it to the type in which it will be injected. - /// So if adding `MockDependency` but type at injection is `Dependency` remember to cast - /// to `Dependency` first. - /// - Parameter dependency: The dependency to add. - /// - Returns: The wrapped view that now includes the dependency. - func addDependency(_ dependency: T) -> some View { - transformEnvironment(\.dependencies) { container in - container.register(dependency: dependency) - } - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift deleted file mode 100644 index d45907eeb..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// A property wrapped used to inject from the dependency container on the instance, to instance properties. -/// -/// ``` -/// @Inject var someClass: SomeClass -/// ``` -@propertyWrapper struct Inject { - static subscript(_enclosingInstance instance: T, - wrapped wrappedKeyPath: ReferenceWritableKeyPath, - storage storageKeyPath: ReferenceWritableKeyPath) -> Value { - get { - // Resolve dependencies from enclosing instance's `dependencies` property - let v: Value = instance.dependencies.resolve() - return v - } - set { - fatalError("Only subscript get is supported for injection") - } - } - - @available(*, unavailable, message: "This property wrapper can only be applied to classes") - var wrappedValue: Value { - get { fatalError("wrappedValue get not used") } - set { fatalError("wrappedValue set not used. \(newValue)") } - } -} diff --git a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift index ac7a7ebbc..60589f9d8 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift @@ -25,6 +25,6 @@ extension MXRoom: Avatarable { } var displayName: String? { - summary.displayname + summary.displayName } } diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift index 62d86a681..b34c744c3 100644 --- a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift +++ b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift @@ -51,7 +51,7 @@ enum MockInfoSheetScreenState: MockScreenState, CaseIterable { return ( [model, viewModel], AnyView(InfoSheet(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index eaee22e73..91bf25a51 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -72,6 +72,8 @@ enum MockAppScreens { MockComposerScreenState.self, MockComposerCreateActionListScreenState.self, MockComposerLinkActionScreenState.self, - MockVoiceBroadcastPlaybackScreenState.self + MockVoiceBroadcastPlaybackScreenState.self, + MockPollHistoryScreenState.self, + MockPollHistoryDetailScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift index 08e1712ab..d0bc91c3e 100644 --- a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift @@ -52,7 +52,7 @@ struct MultilineTextField: View { private var borderColor: Color { if isEditing { - return theme.colors.accent + return Color(ThemeService.shared().theme.tintColor) } return theme.colors.quarterlyContent diff --git a/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift index 0e0f38586..3b2710006 100644 --- a/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift @@ -28,7 +28,7 @@ struct PasswordButtonModifier: ViewModifier { // MARK: - Private @Environment(\.theme) private var theme: ThemeSwiftUI - @ScaledMetric private var iconSize = 16 + @ScaledMetric private var iconSize = BWIBuildSettings.shared.bwiEnableBuMUI ? 22 : 16 // MARK: - Public @@ -38,9 +38,11 @@ struct PasswordButtonModifier: ViewModifier { if !text.isEmpty { Button { isSecureTextVisible.toggle() } label: { - Image(Asset.Images.authenticationRevealPassword.name) + // bwi: MESSENGER-3948 + Image(isSecureTextVisible ? Asset.Images.hidePasswordButton.name : Asset.Images.revealPasswordButton.name) .renderingMode(.template) .resizable() + .aspectRatio(contentMode: .fit) .frame(width: iconSize, height: iconSize) .foregroundColor(theme.colors.secondaryContent) } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift index fc5c6b87a..4bd1f7c52 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift @@ -53,7 +53,7 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { service: service ) let view = LiveLocationSharingViewer(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) liveLocationSharingViewerViewModel = viewModel liveLocationSharingViewerHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift index 5c4f8a841..7c0adedd1 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -42,6 +42,13 @@ struct LiveLocationSharingViewerViewState: BindableState { /// Live location list items var listItemsViewData: [LiveLocationListItemViewData] + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide + + var isCurrentUserShared: Bool { + listItemsViewData.contains { $0.isCurrentUser } + } + var showLoadingIndicator = false var shareButtonEnabled: Bool { @@ -75,4 +82,5 @@ enum LiveLocationSharingViewerViewAction { case tapListItem(_ userId: String) case share(_ annotation: UserLocationAnnotation) case mapCreditsDidTap + case showUserLocation } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift index ebfd06220..6a3931b58 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -72,6 +72,8 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType completion?(.share(userLocationAnnotation.coordinate)) case .mapCreditsDidTap: state.bindings.showMapCreditsSheet.toggle() + case .showUserLocation: + showsCurrentUserLocation() } } @@ -209,6 +211,14 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType return } + /* + if the map is currently following the current user's location, + we want to switch back to only showing the marker, + so the the highlighted shared location can be centered + */ + if state.showsUserLocationMode == .follow { + state.showsUserLocationMode = .show + } state.highlightedAnnotation = foundUserAnnotation } @@ -229,4 +239,12 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType } } } + + private func showsCurrentUserLocation() { + if liveLocationSharingViewerService.requestAuthorizationIfNeeded() { + state.showsUserLocationMode = .follow + } else { + state.errorSubject.send(.invalidLocationAuthorization) + } + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift index cb9e107a5..864fa080f 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift @@ -33,4 +33,6 @@ protocol LiveLocationSharingViewerServiceProtocol { /// Stop current user location sharing func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) + + func requestAuthorizationIfNeeded() -> Bool } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift index 9b646a1bf..58c07c942 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift @@ -19,11 +19,13 @@ import Foundation import MatrixSDK class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + // MARK: - Properties private(set) var usersLiveLocation: [UserLiveLocation] = [] private let roomId: String private var beaconInfoSummaryListener: Any? + private let locationManager = CLLocationManager() // MARK: Private @@ -74,6 +76,10 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol } } + func requestAuthorizationIfNeeded() -> Bool { + locationManager.requestAuthorizationIfNeeded() + } + // MARK: - Private private func updateUsersLiveLocation(notifyUpdate: Bool) { diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift index cfb34ef04..64b32dfb2 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift @@ -27,12 +27,17 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt // MARK: Setup - init(generateRandomUsers: Bool = false) { - let firstUserLiveLocation = createFirstUserLiveLocation() + init(generateRandomUsers: Bool = false, currentUserSharingLocation: Bool = true) { + let firstUserLiveLocation: UserLiveLocation? + if currentUserSharingLocation { + firstUserLiveLocation = createFirstUserLiveLocation() + } else { + firstUserLiveLocation = nil + } let secondUserLiveLocation = createSecondUserLiveLocation() - var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation] + var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation].compactMap { $0 } if generateRandomUsers { for _ in 1...20 { @@ -56,6 +61,10 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { } + func requestAuthorizationIfNeeded() -> Bool { + return true + } + // MARK: Private private func createFirstUserLiveLocation() -> UserLiveLocation { diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift index cefe1c2ed..c20df0e96 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -15,6 +15,7 @@ // import Combine +import CoreLocation import XCTest @testable import RiotSwiftUI @@ -30,4 +31,19 @@ class LiveLocationSharingViewerViewModelTests: XCTestCase { viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service) context = viewModel.context } + + func testIsUserBeingShared() { + XCTAssertTrue(context.viewState.isCurrentUserShared) + } + + func testToggleShowUserLocation() { + let service = MockLiveLocationSharingViewerService(currentUserSharingLocation: false) + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service) + XCTAssertFalse(viewModel.context.viewState.isCurrentUserShared) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .hide) + viewModel.context.send(viewAction: .showUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .follow) + viewModel.context.send(viewAction: .tapListItem("@bob:matrix.org")) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .show) + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 9678028b8..e0ac8ac76 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -34,23 +34,35 @@ struct LiveLocationSharingViewer: View { @ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context + var mapView: LocationSharingMapView { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: viewModel.viewState.annotations, + highlightedAnnotation: viewModel.viewState.highlightedAnnotation, + userAvatarData: nil, + showsUserLocationMode: viewModel.viewState.showsUserLocationMode, + userAnnotationCanShowCallout: true, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + onCalloutTap: { annotation in + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + viewModel.send(viewAction: .share(userLocationAnnotation)) + } + }, + errorSubject: viewModel.viewState.errorSubject) + } + var body: some View { ZStack(alignment: .bottom) { if !viewModel.viewState.showMapLoadingError { - LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, - annotations: viewModel.viewState.annotations, - highlightedAnnotation: viewModel.viewState.highlightedAnnotation, - userAvatarData: nil, - showsUserLocation: false, - userAnnotationCanShowCallout: true, - userLocation: Binding.constant(nil), - mapCenterCoordinate: Binding.constant(nil), - onCalloutTap: { annotation in - if let userLocationAnnotation = annotation as? UserLocationAnnotation { - viewModel.send(viewAction: .share(userLocationAnnotation)) - } - }, - errorSubject: viewModel.viewState.errorSubject) + + if !viewModel.viewState.isCurrentUserShared { + mapView + .overlay(CenterToUserLocationButton(action: { + viewModel.send(viewAction: .showUserLocation) + }).offset(x: -11.0, y: 52), alignment: .topTrailing) + } else { + mapView + } // Show map credits above collapsed bottom sheet height if bottom sheet is visible if viewModel.viewState.isBottomSheetVisible { @@ -178,3 +190,27 @@ struct LiveLocationSharingViewer_Previews: PreviewProvider { } } } + +struct CenterToUserLocationButton: View { + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var action: () -> Void + + var body: some View { + Button { + action() + } label: { + Image(uiImage: Asset.Images.locationCenterMapIcon.image) + .foregroundColor(theme.colors.accent) + } + .padding(8.0) + .background(theme.colors.background) + .clipShape(Circle()) + .shadow(radius: 2.0) + } +} diff --git a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift index fa02eef50..fe7ad1521 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift @@ -32,7 +32,7 @@ struct MapViewErrorAlertInfoBuilder { case .invalidLocationAuthorization: alertInfo = AlertInfo(id: .authorizationError, title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, {}), secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) default: alertInfo = nil diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift index 5bfc1daf2..7a357b911 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift @@ -18,6 +18,15 @@ import Combine import Mapbox import SwiftUI +/* + Behavior mode of the current user's location, can be hidden, only shown and shown following the user + */ +enum ShowUserLocationMode { + case follow + case show + case hide +} + struct LocationSharingMapView: UIViewRepresentable { // MARK: - Constants @@ -39,8 +48,8 @@ struct LocationSharingMapView: UIViewRepresentable { /// Current user avatar data, used to replace current location annotation view with the user avatar let userAvatarData: AvatarInputProtocol? - /// True to indicate to show and follow current user location - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide /// True to indicate that a touch on user annotation can show a callout var userAnnotationCanShowCallout = false @@ -75,14 +84,23 @@ struct LocationSharingMapView: UIViewRepresentable { mapView.vc_removeAllAnnotations() mapView.addAnnotations(annotations) - if let highlightedAnnotation = highlightedAnnotation { - mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false) + /* + if there is an highlighted annotation, + and the current user's location it's either hidden or only shown, + we can center to the highlighted annotation + */ + if let highlightedAnnotation = highlightedAnnotation, showsUserLocationMode != .follow { + mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: true) } - if showsUserLocation { + switch showsUserLocationMode { + case .follow: mapView.showsUserLocation = true mapView.userTrackingMode = .follow - } else { + case .show: + mapView.showsUserLocation = true + mapView.userTrackingMode = .none + case .hide: mapView.showsUserLocation = false mapView.userTrackingMode = .none } @@ -125,11 +143,14 @@ extension LocationSharingMapView { return LocationAnnotationView(userLocationAnnotation: userLocationAnnotation) } else if let pinLocationAnnotation = annotation as? PinLocationAnnotation { return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation) - } else if annotation is MGLUserLocation, let currentUserAvatarData = locationSharingMapView.userAvatarData { - // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location - return LocationAnnotationView(avatarData: currentUserAvatarData) + } else if annotation is MGLUserLocation { + if let currentUserAvatarData = locationSharingMapView.userAvatarData { + // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location + return LocationAnnotationView(avatarData: currentUserAvatarData) + } else { + return LocationAnnotationView(userPinLocationAnnotation: annotation) + } } - return nil } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift index 25a5b9848..7915760c8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift @@ -48,7 +48,11 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { addUserMarkerView(with: userLocationAnnotation.avatarData) } - + convenience init(userPinLocationAnnotation: MGLAnnotation) { + self.init(annotation: userPinLocationAnnotation, reuseIdentifier: "userPinLocation") + + addPinView() + } convenience init(pinLocationAnnotation: PinLocationAnnotation) { // TODO: Use a reuseIdentifier self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) @@ -74,11 +78,27 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { addMarkerView(avatarMarkerView) } + private func addPinView() { + guard let pinView = UIHostingController(rootView: Image(uiImage: Asset.Images.locationMarkerIcon.image) + .resizable() + .foregroundColor(theme.colors.accent)).view else { + return + } + + addMarkerView(pinView) + } + private func addPinMarkerView() { guard let pinMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { - Image(uiImage: Asset.Images.locationPinIcon.image) - .resizable() - .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: Asset.Images.locationPinIconBum.image) + .resizable() + .shapedBorder(color: Color(ThemeService.shared().theme.tintColor), borderWidth: 3, shape: Circle()) + } else { + Image(uiImage: Asset.Images.locationPinIcon.image) + .resizable() + .shapedBorder(color: Color(ThemeService.shared().theme.tintColor), borderWidth: 3, shape: Circle()) + } }).view else { return } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift index 97cf99d09..911804d5f 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -87,7 +87,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { let view = LocationSharingView(context: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) locationSharingViewModel = viewModel locationSharingHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift index 3178bcb87..0c623230c 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift @@ -80,8 +80,8 @@ struct LocationSharingViewState: BindableState { var showLoadingIndicator = false - /// True to indicate to show and follow current user location - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide /// Used to hide live location sharing features var isLiveLocationSharingEnabled = false diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift index 3b86f4e89..c59e0bf4b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift @@ -34,6 +34,6 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { isLiveLocationSharingEnabled: true, service: locationSharingService) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) - .addDependency(MockAvatarService.example))) + .environmentObject(AvatarViewModel.withMockedServices()))) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift index 5f66e6146..8c8d7a078 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift @@ -40,7 +40,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie userAvatarData: avatarData, annotations: [], highlightedAnnotation: nil, - showsUserLocation: true, + showsUserLocationMode: .follow, isLiveLocationSharingEnabled: isLiveLocationSharingEnabled) super.init(initialViewState: viewState) @@ -78,7 +78,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie if state.bindings.userLocation == nil { showMissingLocationError() } else { - state.showsUserLocation = true + state.showsUserLocationMode = .follow state.isPinDropSharing = false } case .startLiveSharing: @@ -87,7 +87,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie state.bindings.showingTimerSelector = false completion?(.shareLiveLocation(timeout: timeout.rawValue)) case .userDidPan: - state.showsUserLocation = false + state.showsUserLocationMode = .hide state.isPinDropSharing = true case .mapCreditsDidTap: state.bindings.showMapCreditsSheet.toggle() diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift index 556f410f4..07d0f8af5 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift @@ -52,8 +52,13 @@ struct LocationSharingOptionButton_Previews: PreviewProvider { .resizable() } LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) { } buttonIcon: { - Image(uiImage: Asset.Images.locationPinIcon.image) - .resizable() + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: Asset.Images.locationPinIconBum.image) + .resizable() + } else { + Image(uiImage: Asset.Images.locationPinIcon.image) + .resizable() + } } } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift index a2acb30f1..47f2483cc 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift @@ -62,9 +62,12 @@ struct LocationSharingView: View { .background(theme.colors.background.ignoresSafeArea()) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(VectorL10n.cancel, action: { + Button { context.send(viewAction: .cancel) - }) + } label: { + Text(VectorL10n.cancel) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } } ToolbarItem(placement: .principal) { Text(VectorL10n.locationSharingTitle) @@ -92,7 +95,7 @@ struct LocationSharingView: View { annotations: context.viewState.annotations, highlightedAnnotation: context.viewState.highlightedAnnotation, userAvatarData: context.viewState.userAvatarData, - showsUserLocation: context.viewState.showsUserLocation, + showsUserLocationMode: context.viewState.showsUserLocationMode, userLocation: $context.userLocation, mapCenterCoordinate: $context.pinLocation, errorSubject: context.viewState.errorSubject, @@ -101,9 +104,15 @@ struct LocationSharingView: View { }) if context.viewState.isPinDropSharing { LocationSharingMarkerView(backgroundColor: theme.colors.accent) { - Image(uiImage: Asset.Images.locationPinIcon.image) - .resizable() - .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: Asset.Images.locationPinIconBum.image) + .resizable() + .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) + } else { + Image(uiImage: Asset.Images.locationPinIcon.image) + .resizable() + .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) + } } } } @@ -111,7 +120,7 @@ struct LocationSharingView: View { context.send(viewAction: .goToUserLocation) } label: { Image(uiImage: Asset.Images.locationCenterMapIcon.image) - .foregroundColor(theme.colors.accent) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } .padding(6.0) .background(theme.colors.background) @@ -145,8 +154,13 @@ struct LocationSharingView: View { LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) { context.send(viewAction: .sharePinLocation) } buttonIcon: { - Image(uiImage: Asset.Images.locationPinIcon.image) - .resizable() + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: Asset.Images.locationPinIconBum.image) + .resizable() + } else { + Image(uiImage: Asset.Images.locationPinIcon.image) + .resizable() + } } .disabled(!context.viewState.shareButtonEnabled) } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift index b125fbcd6..09ed605df 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift @@ -53,10 +53,12 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL, avatarData: parameters.avatarData, location: parameters.location, - coordinateType: parameters.coordinateType + coordinateType: parameters.coordinateType, + service: StaticLocationSharingViewerService() ) let view = StaticLocationView(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) + staticLocationViewingViewModel = viewModel staticLocationViewingHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift index 2ea0f0aec..d2d1ecdf7 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift @@ -46,10 +46,11 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable { let viewModel = StaticLocationViewingViewModel(mapStyleURL: mapStyleURL, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"), location: location, - coordinateType: coordinateType) + coordinateType: coordinateType, + service: MockStaticLocationSharingViewerService()) return ([viewModel], AnyView(StaticLocationView(viewModel: viewModel.context) - .addDependency(MockAvatarService.example))) + .environmentObject(AvatarViewModel.withMockedServices()))) } } diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift similarity index 56% rename from RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift rename to RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift index b05b966e4..62be10936 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift @@ -1,5 +1,5 @@ -// -// Copyright 2021 New Vector Ltd +// +// Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,17 +14,19 @@ // limitations under the License. // +import CoreLocation import Foundation -/// A protocol for classes that can be injected with a dependency container -protocol Injectable: AnyObject { - var dependencies: DependencyContainer! { get set } -} - -extension Injectable { - /// Used to inject the dependency container into an Injectable. - /// - Parameter dependencies: The `DependencyContainer` to inject. - func inject(dependencies: DependencyContainer) { - self.dependencies = dependencies +class StaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol { + + // MARK: Private + + private let locationManager = CLLocationManager() + + // MARK: Public + + func requestAuthorizationIfNeeded() -> Bool { + locationManager.requestAuthorizationIfNeeded() } } + diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift new file mode 100644 index 000000000..e792b8a73 --- /dev/null +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift @@ -0,0 +1,25 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockStaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol { + + func requestAuthorizationIfNeeded() -> Bool { + return true + } + +} diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift new file mode 100644 index 000000000..06a99e1bf --- /dev/null +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol StaticLocationSharingViewerServiceProtocol { + + func requestAuthorizationIfNeeded() -> Bool +} diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift index 2d00d2548..c91487950 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift @@ -24,6 +24,7 @@ enum StaticLocationViewingViewAction { case close case share case mapCreditsDidTap + case showUserLocation } enum StaticLocationViewingViewModelResult { @@ -43,6 +44,9 @@ struct StaticLocationViewingViewState: BindableState { /// Shared annotation to display existing location let sharedAnnotation: LocationAnnotation + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide + var showLoadingIndicator = false var shareButtonEnabled: Bool { diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index 0806fcd55..cd998665b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -24,6 +24,7 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static // MARK: Private + private var staticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder // MARK: Public @@ -32,7 +33,10 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static // MARK: - Setup - init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType) { + init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType, service: StaticLocationSharingViewerServiceProtocol) { + + staticLocationSharingViewerService = service + let sharedAnnotation: LocationAnnotation switch coordinateType { case .user: @@ -65,6 +69,8 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static completion?(.share(state.sharedAnnotation.coordinate)) case .mapCreditsDidTap: state.bindings.showMapCreditsSheet.toggle() + case .showUserLocation: + showsCurrentUserLocation() } } @@ -91,4 +97,12 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static state.bindings.alertInfo = alertInfo } + + private func showsCurrentUserLocation() { + if staticLocationSharingViewerService.requestAuthorizationIfNeeded() { + state.showsUserLocationMode = .follow + } else { + state.errorSubject.send(.invalidLocationAuthorization) + } + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift index f9aa24324..7a2f30da4 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift @@ -79,10 +79,18 @@ class StaticLocationViewingViewModelTests: XCTestCase { waitForExpectations(timeout: 3) } + func testToggleShowUserLocation() { + let viewModel = buildViewModel() + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .hide) + viewModel.context.send(viewAction: .showUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .follow) + } + private func buildViewModel() -> StaticLocationViewingViewModel { StaticLocationViewingViewModel(mapStyleURL: URL(string: "http://empty.com")!, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), location: CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096), - coordinateType: .user) + coordinateType: .user, + service: MockStaticLocationSharingViewerService()) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift index 722519adf..f50741884 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -31,6 +31,17 @@ struct StaticLocationView: View { // MARK: Views + var mapView: LocationSharingMapView { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: [viewModel.viewState.sharedAnnotation], + highlightedAnnotation: viewModel.viewState.sharedAnnotation, + userAvatarData: nil, + showsUserLocationMode: viewModel.viewState.showsUserLocationMode, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + errorSubject: viewModel.viewState.errorSubject) + } + var body: some View { NavigationView { ZStack(alignment: .bottom) { @@ -38,7 +49,7 @@ struct StaticLocationView: View { annotations: [viewModel.viewState.sharedAnnotation], highlightedAnnotation: viewModel.viewState.sharedAnnotation, userAvatarData: viewModel.viewState.userAvatarData, - showsUserLocation: false, + showsUserLocationMode: ShowUserLocationMode.hide, userLocation: Binding.constant(nil), mapCenterCoordinate: Binding.constant(nil), errorSubject: viewModel.viewState.errorSubject) @@ -52,12 +63,19 @@ struct StaticLocationView: View { }).sheet } } + .overlay(CenterToUserLocationButton(action: { + viewModel.send(viewAction: .showUserLocation) + }).offset(x: -11.0, y: 52), alignment: .topTrailing) .ignoresSafeArea(.all, edges: [.bottom]) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(VectorL10n.cancel, action: { + Button { viewModel.send(viewAction: .close) - }) + } label: { + Text(VectorL10n.cancel) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } + } ToolbarItem(placement: .principal) { Text(VectorL10n.locationSharingTitle) @@ -69,6 +87,7 @@ struct StaticLocationView: View { viewModel.send(viewAction: .share) } label: { Image(uiImage: Asset.Images.locationShareIcon.image) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } .disabled(!viewModel.viewState.shareButtonEnabled) .accessibilityIdentifier("shareButton") diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift index 3c972b602..73fb8e668 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift @@ -57,7 +57,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingAvatarScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift index d911f8249..e0bca18c1 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift @@ -39,7 +39,7 @@ enum MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift index ed580b834..c1ac46265 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift @@ -45,7 +45,7 @@ enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift deleted file mode 100644 index 0ba19872a..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Combine -import SwiftUI - -typealias AllChatsOnboardingViewModelType = StateStoreViewModel - -class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol { - // MARK: - Properties - - // MARK: Private - - // MARK: Public - - var completion: ((AllChatsOnboardingViewModelResult) -> Void)? - - // MARK: - Setup - - static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol { - AllChatsOnboardingViewModel() - } - - private init() { - super.init(initialViewState: Self.defaultState()) - } - - private static func defaultState() -> AllChatsOnboardingViewState { - AllChatsOnboardingViewState(pages: [ - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding1.image, - title: VectorL10n.allChatsOnboardingPageTitle1, - message: VectorL10n.allChatsOnboardingPageMessage1), - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding2.image, - title: VectorL10n.allChatsOnboardingPageTitle2, - message: VectorL10n.allChatsOnboardingPageMessage2), - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding3.image, - title: VectorL10n.allChatsOnboardingPageTitle3, - message: VectorL10n.allChatsOnboardingPageMessage3) - ]) - } - - // MARK: - Public - - override func process(viewAction: AllChatsOnboardingViewAction) { - switch viewAction { - case .cancel: - completion?(.cancel) - } - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift deleted file mode 100644 index df189c144..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import CommonKit -import SwiftUI - -/// All Chats onboarding screen -final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - - private let hostingController: UIViewController - private var viewModel: AllChatsOnboardingViewModelProtocol - - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? - - // MARK: - Setup - - override init() { - let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel() - let view = AllChatsOnboarding(viewModel: viewModel.context) - self.viewModel = viewModel - hostingController = VectorHostingController(rootView: view) - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) - - super.init() - - hostingController.presentationController?.delegate = self - } - - // MARK: - Public - - func start() { - MXLog.debug("[AllChatsOnboardingCoordinator] did start.") - viewModel.completion = { [weak self] result in - guard let self = self else { return } - MXLog.debug("[AllChatsOnboardingCoordinator] AllChatsOnboardingViewModel did complete with result: \(result).") - switch result { - case .cancel: - self.completion?() - } - } - } - - func toPresentable() -> UIViewController { - hostingController - } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - loadingIndicator = nil - } -} - -// MARK: - UIAdaptivePresentationControllerDelegate - -extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - completion?() - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift deleted file mode 100644 index 75977054d..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -@objc protocol AllChatsOnboardingCoordinatorBridgePresenterDelegate { - func allChatsOnboardingCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter) -} - -/// `AllChatsOnboardingCoordinatorBridgePresenter` enables to start `AllChatsOnboardingCoordinator` from a view controller. -/// This bridge is used while waiting for global usage of coordinator pattern. -/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). -/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. -@objcMembers -final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { - // MARK: - Properties - - // MARK: Private - - private var coordinator: AllChatsOnboardingCoordinator? - - // MARK: Public - - var completion: (() -> Void)? - - // MARK: - Public - - func present(from viewController: UIViewController, animated: Bool) { - let coordinator = AllChatsOnboardingCoordinator() - coordinator.completion = { [weak self] in - guard let self = self else { return } - self.completion?() - } - let presentable = coordinator.toPresentable() - viewController.present(presentable, animated: animated, completion: nil) - coordinator.start() - - self.coordinator = coordinator - } - - func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = coordinator else { - return - } - coordinator.toPresentable().dismiss(animated: animated) { - self.coordinator = nil - completion?() - } - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift deleted file mode 100644 index 513cc55a3..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AllChatsOnboarding: View { - // MARK: - Properties - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - @State private var selectedTab = 0 - - // MARK: Public - - @ObservedObject var viewModel: AllChatsOnboardingViewModel.Context - - var body: some View { - VStack { - Text(VectorL10n.allChatsOnboardingTitle) - .font(theme.fonts.title3SB) - .foregroundColor(theme.colors.primaryContent) - .padding() - TabView(selection: $selectedTab) { - ForEach(viewModel.viewState.pages.indices, id: \.self) { index in - let page = viewModel.viewState.pages[index] - AllChatsOnboardingPage(image: page.image, - title: page.title, - message: page.message) - .tag(index) - } - } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) - .indexViewStyle(.page(backgroundDisplayMode: .always)) - - Button { onCallToAction() } label: { - Text(selectedTab == viewModel.viewState.pages.count - 1 ? VectorL10n.allChatsOnboardingTryIt : VectorL10n.next) - .animation(nil) - } - .buttonStyle(PrimaryActionButtonStyle()) - .padding() - } - .background(theme.colors.background.ignoresSafeArea()) - .frame(maxHeight: .infinity) - } - - // MARK: - Private - - private func onCallToAction() { - if selectedTab == viewModel.viewState.pages.count - 1 { - viewModel.send(viewAction: .cancel) - } else { - withAnimation { - selectedTab += 1 - } - } - } -} - -// MARK: - Previews - -struct AllChatsOnboarding_Previews: PreviewProvider { - static var previews: some View { - AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.light).preferredColorScheme(.light) - AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.dark).preferredColorScheme(.dark) - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift deleted file mode 100644 index c6a5f06fa..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AllChatsOnboardingPage: View { - // MARK: - Properties - - let image: UIImage - let title: String - let message: String - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - - var body: some View { - VStack { - Spacer() - Image(uiImage: image) - Spacer() - Text(title) - .font(theme.fonts.title2B) - .foregroundColor(theme.colors.primaryContent) - .padding(.bottom, 16) - Text(message) - .multilineTextAlignment(.center) - .font(theme.fonts.callout) - .foregroundColor(theme.colors.primaryContent) - Spacer() - } - .padding(.horizontal) - } -} - -// MARK: - Previews - -struct AllChatsOnboardingPage_Previews: PreviewProvider { - static var previews: some View { - preview.theme(.light).preferredColorScheme(.light) - preview.theme(.dark).preferredColorScheme(.dark) - } - - private static var preview: some View { - AllChatsOnboardingPage(image: Asset.Images.allChatsOnboarding1.image, - title: VectorL10n.allChatsOnboardingPageTitle1, - message: VectorL10n.allChatsOnboardingPageMessage1) - } -} diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift index b47c5bcd5..fdf92cab5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift @@ -59,20 +59,31 @@ extension ComposerLinkActionViewState { } var isSaveButtonDisabled: Bool { - guard isValidLink else { return true } + guard !bindings.linkUrl.isEmpty else { return true } switch linkAction { case .createWithText: return bindings.text.isEmpty - default: return false + case .create: return false + case .edit: return !bindings.hasEditedUrl } } - - private var isValidLink: Bool { - guard let url = URL(string: bindings.linkUrl) else { return false } - return UIApplication.shared.canOpenURL(url) - } } struct ComposerLinkActionBindings { var text: String - var linkUrl: String + + private let initialLinkUrl: String + fileprivate var hasEditedUrl = false + var linkUrl: String { + didSet { + if !hasEditedUrl && linkUrl != initialLinkUrl { + hasEditedUrl = true + } + } + } + + init(text: String, linkUrl: String) { + self.text = text + self.linkUrl = linkUrl + self.initialLinkUrl = linkUrl + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift index f30dacf30..c18405951 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift @@ -29,9 +29,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase { let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) linkTextField.tap() - linkTextField.typeText("invalid url") - XCTAssertFalse(saveButton.isEnabled) - linkTextField.clearAndTypeText("https://element.io") + linkTextField.clearAndTypeText("element.io") XCTAssertTrue(saveButton.isEnabled) } @@ -47,7 +45,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase { let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) linkTextField.tap() - linkTextField.typeText("https://element.io") + linkTextField.typeText("element.io") XCTAssertFalse(saveButton.isEnabled) textTextField.tap() textTextField.typeText("test") @@ -60,13 +58,15 @@ final class ComposerLinkActionUITests: MockScreenTestCase { XCTAssertTrue(app.buttons[VectorL10n.cancel].exists) let saveButton = app.buttons[VectorL10n.save] XCTAssertTrue(saveButton.exists) - XCTAssertTrue(saveButton.isEnabled) + XCTAssertFalse(saveButton.isEnabled) XCTAssertFalse(app.textFields["textTextField"].exists) let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) let value = linkTextField.value as? String XCTAssertEqual(value, "https://element.io") - linkTextField.clearAndTypeText("invalid url") + linkTextField.clearAndTypeText("") XCTAssertFalse(saveButton.isEnabled) + linkTextField.clearAndTypeText("matrix.org") + XCTAssertTrue(saveButton.isEnabled) } } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift index 40ad27358..2407eccc4 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift @@ -53,29 +53,20 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testEditDefaultState() { - let link = "https://element.io" + let link = "element.io" setUp(with: .edit(link: link)) XCTAssertEqual(context.viewState.bindings.text, "") XCTAssertEqual(context.viewState.bindings.linkUrl, link) - XCTAssertFalse(context.viewState.isSaveButtonDisabled) + XCTAssertTrue(context.viewState.isSaveButtonDisabled) XCTAssertTrue(context.viewState.shouldDisplayRemoveButton) XCTAssertFalse(context.viewState.shouldDisplayTextField) XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionEditTitle) } - func testUrlValidityCheck() { - setUp(with: .create) - XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "invalid url" - XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "https://element.io" - XCTAssertFalse(context.viewState.isSaveButtonDisabled) - } - func testTextNotEmptyCheck() { setUp(with: .createWithText) XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "https://element.io" + context.linkUrl = "element.io" XCTAssertTrue(context.viewState.isSaveButtonDisabled) context.text = "text" XCTAssertFalse(context.viewState.isSaveButtonDisabled) @@ -92,7 +83,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testRemoveAction() { - setUp(with: .edit(link: "https://element.io")) + setUp(with: .edit(link: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value @@ -107,7 +98,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { viewModel.callback = { value in result = value } - let link = "https://element.io" + let link = "element.io" context.linkUrl = link context.send(viewAction: .save) XCTAssertEqual(result, .performOperation(.setLink(urlString: link))) @@ -119,7 +110,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { viewModel.callback = { value in result = value } - let link = "https://element.io" + let link = "element.io" context.linkUrl = link let text = "test" context.text = text @@ -128,13 +119,15 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testSaveActionForEdit() { - setUp(with: .edit(link: "https://element.io")) + setUp(with: .edit(link: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value } - let link = "https://matrix.org" + XCTAssertTrue(context.viewState.isSaveButtonDisabled) + let link = "matrix.org" context.linkUrl = link + XCTAssertFalse(context.viewState.isSaveButtonDisabled) context.send(viewAction: .save) XCTAssertEqual(result, .performOperation(.setLink(urlString: link))) } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/View/ComposerLinkAction.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/View/ComposerLinkAction.swift index 1dbdee3ae..e017ad619 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/View/ComposerLinkAction.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/View/ComposerLinkAction.swift @@ -100,9 +100,12 @@ struct ComposerLinkAction: View { .padding(.horizontal, 16.0) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(VectorL10n.cancel, action: { + Button { viewModel.send(viewAction: .cancel) - }) + } label: { + Text(VectorL10n.cancel) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } } ToolbarItem(placement: .principal) { Text(viewModel.viewState.title) @@ -114,7 +117,7 @@ struct ComposerLinkAction: View { .introspectNavigationController { navigationController in ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar) } - .accentColor(theme.colors.accent) + .accentColor(Color(ThemeService.shared().theme.tintColor)) .navigationViewStyle(StackNavigationViewStyle()) } } diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index bc2e8771d..98d7febf6 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -34,7 +34,13 @@ enum FormatType { case italic case underline case strikethrough + case unorderedList + case orderedList + case indent + case unindent case inlineCode + case codeBlock + case quote case link } @@ -42,6 +48,18 @@ extension FormatType: CaseIterable, Identifiable { var id: Self { self } } +extension FormatType { + /// Return true if the format type is an indentation action. + var isIndentType: Bool { + switch self { + case .indent, .unindent: + return true + default: + return false + } + } +} + extension FormatItem: Identifiable { var id: FormatType { type } } @@ -54,14 +72,26 @@ extension FormatItem { return Asset.Images.bold.name case .italic: return Asset.Images.italic.name - case .strikethrough: - return Asset.Images.strikethrough.name case .underline: return Asset.Images.underlined.name - case .link: - return Asset.Images.link.name + case .strikethrough: + return Asset.Images.strikethrough.name + case .unorderedList: + return Asset.Images.bulletList.name + case .orderedList: + return Asset.Images.numberedList.name + case .indent: + return Asset.Images.indentIncrease.name + case .unindent: + return Asset.Images.indentDecrease.name case .inlineCode: return Asset.Images.code.name + case .codeBlock: + return Asset.Images.codeBlock.name + case .quote: + return Asset.Images.quote.name + case .link: + return Asset.Images.link.name } } @@ -71,14 +101,26 @@ extension FormatItem { return "boldButton" case .italic: return "italicButton" - case .strikethrough: - return "strikethroughButton" case .underline: return "underlineButton" - case .link: - return "linkButton" + case .strikethrough: + return "strikethroughButton" + case .unorderedList: + return "unorderedListButton" + case .orderedList: + return "orderedListButton" + case .indent: + return "indentListButton" + case .unindent: + return "unIndentButton" case .inlineCode: return "inlineCodeButton" + case .codeBlock: + return "codeBlockButton" + case .quote: + return "quoteButton" + case .link: + return "linkButton" } } @@ -88,14 +130,26 @@ extension FormatItem { return VectorL10n.wysiwygComposerFormatActionBold case .italic: return VectorL10n.wysiwygComposerFormatActionItalic - case .strikethrough: - return VectorL10n.wysiwygComposerFormatActionStrikethrough case .underline: return VectorL10n.wysiwygComposerFormatActionUnderline - case .link: - return VectorL10n.wysiwygComposerFormatActionLink + case .strikethrough: + return VectorL10n.wysiwygComposerFormatActionStrikethrough + case .unorderedList: + return VectorL10n.wysiwygComposerFormatActionUnorderedList + case .orderedList: + return VectorL10n.wysiwygComposerFormatActionOrderedList + case .indent: + return VectorL10n.wysiwygComposerFormatActionIndent + case .unindent: + return VectorL10n.wysiwygComposerFormatActionUnIndent case .inlineCode: return VectorL10n.wysiwygComposerFormatActionInlineCode + case .codeBlock: + return VectorL10n.wysiwygComposerFormatActionCodeBlock + case .quote: + return VectorL10n.wysiwygComposerFormatActionQuote + case .link: + return VectorL10n.wysiwygComposerFormatActionLink } } } @@ -108,14 +162,26 @@ extension FormatType { return .bold case .italic: return .italic - case .strikethrough: - return .strikeThrough case .underline: return .underline - case .link: - return .link + case .strikethrough: + return .strikeThrough + case .unorderedList: + return .unorderedList + case .orderedList: + return .orderedList + case .indent: + return .indent + case .unindent: + return .unindent case .inlineCode: return .inlineCode + case .codeBlock: + return .codeBlock + case .quote: + return .quote + case .link: + return .link } } @@ -127,14 +193,26 @@ extension FormatType { return .bold case .italic: return .italic - case .strikethrough: - return .strikeThrough case .underline: return .underline - case .link: - return .link + case .strikethrough: + return .strikeThrough + case .unorderedList: + return .unorderedList + case .orderedList: + return .orderedList + case .indent: + return .indent + case .unindent: + return .unindent case .inlineCode: return .inlineCode + case .codeBlock: + return .codeBlock + case .quote: + return .quote + case .link: + return .link } } } @@ -167,5 +245,3 @@ final class LinkActionWrapper: NSObject { super.init() } } - - diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift index aae6e1682..72a2fb2ad 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift @@ -158,4 +158,32 @@ final class ComposerUITests: MockScreenTestCase { XCTAssertFalse(minimiseButton.exists) XCTAssertTrue(maximiseButton.exists) } + + func testCreatingListDisplaysIndentButtons() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + // Create a list. + composerToolbarButton(in: app, for: .orderedList).tap() + XCTAssertTrue(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertTrue(composerToolbarButton(in: app, for: .indent).exists) + // Remove the list + composerToolbarButton(in: app, for: .orderedList).tap() + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + } +} + +private extension ComposerUITests { + /// Returns the button of the composer toolbar associated with given format type. + /// + /// - Parameters: + /// - app: the running app + /// - formatType: format type to look for + /// - Returns: XCUIElement for the button + func composerToolbarButton(in app: XCUIApplication, for formatType: FormatType) -> XCUIElement { + // Note: state is irrelevant here, we're just building this to retrieve the accessibility identifier. + app.buttons[FormatItem(type: formatType, state: .enabled).accessibilityIdentifier] + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 6163f384a..e121d6075 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -71,12 +71,15 @@ struct Composer: View { } private var formatItems: [FormatItem] { - FormatType.allCases.map { type in - FormatItem( - type: type, - state: wysiwygViewModel.actionStates[type.composerAction] ?? .disabled - ) - } + return FormatType.allCases + // Exclude indent type outside of lists. + .filter { wysiwygViewModel.isInList || !$0.isIndentType } + .map { type in + FormatItem( + type: type, + state: wysiwygViewModel.actionStates[type.composerAction] ?? .disabled + ) + } } private var composerContainer: some View { @@ -171,7 +174,7 @@ struct Composer: View { if viewModel.viewState.sendMode == .edit { Image(Asset.Images.saveIcon.name) } else { - Image(Asset.Images.sendIcon.name) + Image(BWIBuildSettings.shared.bwiEnableBuMUI ? Asset.Images.sendIconBum.name : Asset.Images.sendIcon.name) } } .frame(width: 36, height: 36) @@ -257,6 +260,13 @@ struct Composer: View { } } +private extension WysiwygComposerViewModel { + /// Return true if the selection of the composer is currently located in a list. + var isInList: Bool { + actionStates[.orderedList] == .reversed || actionStates[.unorderedList] == .reversed + } +} + // MARK: Previews struct Composer_Previews: PreviewProvider { diff --git a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift index d8670ee0c..e7d59a989 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift @@ -32,21 +32,23 @@ struct FormattingToolbar: View { var formatAction: (FormatType) -> Void var body: some View { - HStack(spacing: 4) { - ForEach(formatItems) { item in - Button { - formatAction(item.type) - } label: { - Image(item.icon) - .renderingMode(.template) - .foregroundColor(getForegroundColor(for: item)) + ScrollView(.horizontal) { + HStack(spacing: 4) { + ForEach(formatItems) { item in + Button { + formatAction(item.type) + } label: { + Image(item.icon) + .renderingMode(.template) + .foregroundColor(getForegroundColor(for: item)) + } + .disabled(item.state == .disabled) + .frame(width: 44, height: 44) + .background(getBackgroundColor(for: item)) + .cornerRadius(8) + .accessibilityIdentifier(item.accessibilityIdentifier) + .accessibilityLabel(item.accessibilityLabel) } - .disabled(item.state == .disabled) - .frame(width: 44, height: 44) - .background(getBackgroundColor(for: item)) - .cornerRadius(8) - .accessibilityIdentifier(item.accessibilityIdentifier) - .accessibilityLabel(item.accessibilityLabel) } } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index e0d909898..5a7de9c92 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -41,17 +41,17 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin let avatarData = showAvatar ? AvatarInput( mxContentUri: room.summary.avatar, matrixItemId: room.roomId, - displayName: room.summary.displayname + displayName: room.summary.displayName ) : nil let viewModel = RoomNotificationSettingsSwiftUIViewModel( roomNotificationService: roomNotificationService, avatarData: avatarData, - displayName: room.summary.displayname, + displayName: room.summary.displayName, roomEncrypted: room.summary.isEncrypted ) let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager) let view = RoomNotificationSettings(viewModel: viewModel, presentedModally: presentedModally) - .addDependency(avatarService) + .environmentObject(AvatarViewModel(avatarService: avatarService)) let viewController = VectorHostingController(rootView: view) roomNotificationSettingsViewModel = viewModel roomNotificationSettingsViewController = viewController diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift index 96773de68..e887dd3ae 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift @@ -37,6 +37,6 @@ extension RoomNotificationSettingsViewState { extension RoomNotificationSettingsViewState { var roomEncryptedString: String { - roomEncrypted ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : "" + return roomEncrypted ? BWIL10n.roomNotifsSettingsEncryptedRoomNotice : "" } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift index e34bb5faa..c28f7aeb8 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift @@ -32,11 +32,20 @@ extension RoomNotificationState { var title: String { switch self { case .all: - return VectorL10n.roomNotifsSettingsAllMessages + return BWIL10n.roomNotifsSettingsAllMessages case .mentionsAndKeywordsOnly: return VectorL10n.roomNotifsSettingsMentionsAndKeywords case .mute: - return VectorL10n.roomNotifsSettingsNone + return BWIL10n.roomNotifsSettingsNone + } + } + + var subtitle: String { + switch self { + case .all: + return BWIL10n.roomNotifsSettingsAllDescription + default: + return "" } } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift index e9d192b49..caaa45a53 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift @@ -30,6 +30,10 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy private var observers: [ObjectIdentifier] = [] + private var notificationCenter: MXNotificationCenter? { + room.mxSession?.notificationCenter + } + // MARK: Public var notificationState: RoomNotificationState { @@ -166,7 +170,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.addRoomRule( + notificationCenter?.addRoomRule( room.roomId, notify: false, sound: false, @@ -184,7 +188,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.addOverrideRule( + notificationCenter?.addOverrideRule( withId: roomId, conditions: [["kind": "event_match", "key": "room_id", "pattern": roomId]], notify: false, @@ -196,11 +200,11 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy private func removePushRule(rule: MXPushRule, completion: @escaping Completion) { handleUpdateCallback(completion) { [weak self] in guard let self = self else { return true } - return self.room.mxSession.notificationCenter.rule(byId: rule.ruleId) == nil + return self.notificationCenter?.rule(byId: rule.ruleId) == nil } handleFailureCallback(completion) - room.mxSession.notificationCenter.removeRule(rule) + notificationCenter?.removeRule(rule) } private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) { @@ -210,7 +214,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.enableRule(rule, isEnabled: true) + notificationCenter?.enableRule(rule, isEnabled: true) } private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) { @@ -283,14 +287,14 @@ private extension MXRoom { } var overridePushRule: MXPushRule? { - guard let overrideRules = mxSession.notificationCenter.rules.global.override else { + guard let overrideRules = mxSession?.notificationCenter?.rules?.global?.override else { return nil } return getRoomRule(from: overrideRules) } var roomPushRule: MXPushRule? { - guard let roomRules = mxSession.notificationCenter.rules.global.room else { + guard let roomRules = mxSession?.notificationCenter?.rules?.global?.room else { return nil } return getRoomRule(from: roomRules) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift index 05a3b161f..f194c352a 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift @@ -22,6 +22,7 @@ struct FormPickerItem: View { @Environment(\.theme) var theme: ThemeSwiftUI var title: String + var subtitle: String? var selected: Bool var onTap: TapCallback? @@ -32,11 +33,20 @@ struct FormPickerItem: View { VStack { Spacer() HStack { - Text(title) + if let subtitle = subtitle, !subtitle.isEmpty && BWIBuildSettings.shared.notificationSettingsLikeAndroidAndWeb { + VStack(alignment: .leading, spacing: 4) { + Text(title) + Text(subtitle) + .font(.footnote) + .foregroundColor(.secondary) + } + } else { + Text(title) + } Spacer() if selected { Image("checkmark") - .foregroundColor(theme.colors.accent) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } } .padding(.trailing) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift index 3034f50db..6518b0284 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift @@ -48,11 +48,11 @@ struct RoomNotificationSettings: View { ) } SwiftUI.Section( - header: FormSectionHeader(text: VectorL10n.roomNotifsSettingsNotifyMeFor), + header: FormSectionHeader(text: BWIL10n.roomNotifsSettingsNotifyMeFor), footer: FormSectionFooter(text: viewModel.viewState.roomEncryptedString) ) { ForEach(viewModel.viewState.notificationOptions) { option in - FormPickerItem(title: option.title, selected: viewModel.viewState.notificationState == option) { + FormPickerItem(title: option.title, subtitle: option.subtitle, selected: viewModel.viewState.notificationState == option) { viewModel.process(viewAction: .selectNotificationState(option)) } } @@ -67,7 +67,7 @@ struct RoomNotificationSettings: View { .onAppear { viewModel.process(viewAction: .load) } - .accentColor(theme.colors.accent) + .accentColor(Color(ThemeService.shared().theme.tintColor)) .track(screen: .roomNotifications) } } @@ -85,13 +85,13 @@ struct RoomNotificationSettings_Previews: PreviewProvider { NavigationView { RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true) .navigationBarTitleDisplayMode(.inline) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } NavigationView { RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true) .navigationBarTitleDisplayMode(.inline) .theme(ThemeIdentifier.dark) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift index 20066b961..835c9bdd7 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift @@ -43,6 +43,6 @@ struct RoomNotificationSettingsHeader_Previews: PreviewProvider { static let name = "Element" static var previews: some View { RoomNotificationSettingsHeader(avatarData: MockAvatarInput.example, displayName: name) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 30de20849..dd6fb6252 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -69,6 +69,7 @@ struct PollEditForm: View { viewModel.send(viewAction: .addAnswerOption) } } + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) .disabled(!viewModel.viewState.addAnswerOptionButtonEnabled) Spacer() @@ -92,9 +93,12 @@ struct PollEditForm: View { .frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(VectorL10n.cancel, action: { + Button { viewModel.send(viewAction: .cancel) - }) + } label: { + Text(VectorL10n.cancel) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } } ToolbarItem(placement: .principal) { Text(BWIL10n.pollEditFormCreatePoll) @@ -104,9 +108,12 @@ struct PollEditForm: View { ToolbarItem(placement: .navigationBarTrailing) { if viewModel.viewState.mode == .editing { - Button(VectorL10n.save, action: { + Button { viewModel.send(viewAction: .update) - }) + } label: { + Text(VectorL10n.save) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } .disabled(!viewModel.viewState.confirmationButtonEnabled) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift index d3cfe0db6..ac9b11577 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift @@ -45,7 +45,13 @@ private struct PollEditFormTypeButton: View { selectedType = type } label: { HStack(alignment: .top, spacing: 8.0) { - Image(uiImage: selectionImage) + // bwi: 4179 + if type == selectedType { + Image(uiImage: Asset.Images.pollTypeCheckboxSelected.image) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } else { + Image(uiImage: Asset.Images.pollTypeCheckboxDefault.image) + } VStack(alignment: .leading, spacing: 2) { Text(title) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift new file mode 100644 index 000000000..faa293abf --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -0,0 +1,108 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit +import MatrixSDK +import SwiftUI + +struct PollHistoryCoordinatorParameters { + let mode: PollHistoryMode + let room: MXRoom + let navigationRouter: NavigationRouterType +} + +final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { + private let parameters: PollHistoryCoordinatorParameters + private let pollHistoryHostingController: UIViewController + private var pollHistoryViewModel: PollHistoryViewModelProtocol + private let navigationRouter: NavigationRouterType + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((MXEvent) -> Void)? + + init(parameters: PollHistoryCoordinatorParameters) { + self.parameters = parameters + let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room, chunkSizeInDays: PollHistoryConstants.chunkSizeInDays)) + let view = PollHistory(viewModel: viewModel.context) + pollHistoryViewModel = viewModel + pollHistoryHostingController = VectorHostingController(rootView: view) + navigationRouter = parameters.navigationRouter + } + + // MARK: - Public + + func start() { + MXLog.debug("[PollHistoryCoordinator] did start.") + pollHistoryViewModel.completion = { [weak self] result in + switch result { + case .showPollDetail(let poll): + self?.showPollDetail(poll) + } + } + } + + func showPollDetail(_ poll: TimelinePollDetails) { + guard let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, poll: poll, room: parameters.room)) else { + pollHistoryViewModel.context.alertInfo = .init(id: true, title: VectorL10n.settingsDiscoveryErrorMessage) + return + } + detailCoordinator.toPresentable().presentationController?.delegate = self + detailCoordinator.completion = { [weak self, weak detailCoordinator, weak event] result in + guard let self, let coordinator = detailCoordinator, let event = event else { return } + self.handlePollDetailResult(result, coordinator: coordinator, event: event, poll: poll) + } + + add(childCoordinator: detailCoordinator) + detailCoordinator.start() + toPresentable().present(detailCoordinator.toPresentable(), animated: true) + } + + func toPresentable() -> UIViewController { + pollHistoryHostingController + } + + private func handlePollDetailResult(_ result: PollHistoryDetailViewModelResult, coordinator: Coordinator, event: MXEvent, poll: TimelinePollDetails) { + switch result { + case .dismiss: + toPresentable().dismiss(animated: true) + remove(childCoordinator: coordinator) + case .viewInTimeline: + toPresentable().dismiss(animated: false) + remove(childCoordinator: coordinator) + var event = event + if poll.closed { + let room = parameters.room + let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) + let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) + event = pollEndedEvent ?? event + } + completion?(event) + } + } +} + +// MARK: UIAdaptivePresentationControllerDelegate + +extension PollHistoryCoordinator: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + guard let coordinator = childCoordinators.last else { + return + } + remove(childCoordinator: coordinator) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift new file mode 100644 index 000000000..a1c12e5f3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -0,0 +1,98 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockPollHistoryScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case active + case past + case activeNoMoreContent + case contentLoading + case empty + case emptyLoading + case emptyNoMoreContent + case loading + + /// The associated screen + var screenType: Any.Type { + PollHistory.self + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + var pollHistoryMode: PollHistoryMode = .active + let pollService = MockPollHistoryService() + + switch self { + case .active: + pollHistoryMode = .active + case .activeNoMoreContent: + pollHistoryMode = .active + pollService.hasNextBatch = false + case .past: + pollHistoryMode = .past + case .contentLoading: + pollService.nextBatchPublishers.append(MockPollPublisher.loadingPolls) + case .empty: + pollHistoryMode = .active + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls] + case .emptyLoading: + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls, MockPollPublisher.loadingPolls] + case .emptyNoMoreContent: + pollService.hasNextBatch = false + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls] + case .loading: + pollService.nextBatchPublishers = [MockPollPublisher.loadingPolls] + } + + let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) + + // can simulate service and viewModel actions here if needs be. + switch self { + case .contentLoading, .emptyLoading: + viewModel.process(viewAction: .loadMoreContent) + default: + break + } + + return ( + [pollHistoryMode, viewModel], + AnyView(PollHistory(viewModel: viewModel.context) + .environmentObject(AvatarViewModel.withMockedServices())) + ) + } +} + +enum MockPollPublisher { + static var emptyPolls: AnyPublisher { + Empty(completeImmediately: true).eraseToAnyPublisher() + } + + static var loadingPolls: AnyPublisher { + Empty(completeImmediately: false).eraseToAnyPublisher() + } + + static var failure: AnyPublisher { + Fail(error: NSError(domain: "fake", code: 1)).eraseToAnyPublisher() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift new file mode 100644 index 000000000..4b166b3aa --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -0,0 +1,66 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import CommonKit +import MatrixSDK +import SwiftUI + +struct PollHistoryDetailCoordinatorParameters { + let event: MXEvent + let poll: TimelinePollDetails + let room: MXRoom +} + +final class PollHistoryDetailCoordinator: Coordinator, Presentable { + private let parameters: PollHistoryDetailCoordinatorParameters + private let pollHistoryDetailHostingController: UIViewController + private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((PollHistoryDetailViewModelResult) -> Void)? + + init(parameters: PollHistoryDetailCoordinatorParameters) throws { + self.parameters = parameters + let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.room.mxSession, room: parameters.room, pollEvent: parameters.event)) + + let viewModel = PollHistoryDetailViewModel(poll: parameters.poll) + let view = PollHistoryDetail(viewModel: viewModel.context, contentPoll: timelinePollCoordinator.toView()) + pollHistoryDetailViewModel = viewModel + pollHistoryDetailHostingController = VectorHostingController(rootView: view) + add(childCoordinator: timelinePollCoordinator) + } + + // MARK: - Public + + func start() { + MXLog.debug("[PollHistoryDetailCoordinator] did start.") + pollHistoryDetailViewModel.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case .dismiss: + self.completion?(.dismiss) + case .viewInTimeline: + self.completion?(.viewInTimeline) + } + } + } + + func toPresentable() -> UIViewController { + pollHistoryDetailHostingController + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift new file mode 100644 index 000000000..09a8fb3c7 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { + case openDisclosed + case closedDisclosed + case openUndisclosed + case closedUndisclosed + case closedPollEnded + + var screenType: Any.Type { + PollHistoryDetail.self + } + + var poll: TimelinePollDetails { + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + + let poll = TimelinePollDetails(id: "id", + question: "Question", + answerOptions: answerOptions, + closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, + startDate: .init(timeIntervalSinceReferenceDate: 0), + totalAnswerCount: 20, + type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, + eventType: self == .closedPollEnded ? .ended : .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + return poll + } + + var screenView: ([Any], AnyView) { + let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) + let viewModel = PollHistoryDetailViewModel(poll: poll) + + return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context)))) + } +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift similarity index 58% rename from RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift rename to RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 76beb1205..7d4fd6365 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -15,29 +15,31 @@ // import Foundation -import UIKit +import SwiftUI // MARK: - Coordinator -// MARK: View model +typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult) -> Void -enum AllChatsOnboardingViewModelResult { - case cancel +enum PollHistoryDetailViewModelResult { + case dismiss + case viewInTimeline } // MARK: View -struct AllChatsOnboardingPageData: Identifiable { - let id = UUID().uuidString - let image: UIImage - let title: String - let message: String +struct PollHistoryDetailViewState: BindableState { + var poll: TimelinePollDetails + var pollStartDate: Date { + poll.startDate + } + + var isPollClosed: Bool { + poll.closed + } } -struct AllChatsOnboardingViewState: BindableState { - let pages: [AllChatsOnboardingPageData] -} - -enum AllChatsOnboardingViewAction { - case cancel +enum PollHistoryDetailViewAction { + case dismiss + case viewInTimeline } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift new file mode 100644 index 000000000..58a18441a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -0,0 +1,43 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias PollHistoryDetailViewModelType = StateStoreViewModel + +class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDetailViewModelProtocol { + // MARK: Public + + var completion: PollHistoryDetailViewModelCallback? + + // MARK: - Setup + + init(poll: TimelinePollDetails) { + super.init(initialViewState: PollHistoryDetailViewState(poll: poll)) + } + + // MARK: - Public + + override func process(viewAction: PollHistoryDetailViewAction) { + switch viewAction { + case .dismiss: + completion?(.dismiss) + case .viewInTimeline: + completion?(.viewInTimeline) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift similarity index 68% rename from RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift rename to RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift index dd963c407..0e4abb98a 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift @@ -16,8 +16,7 @@ import Foundation -protocol AllChatsOnboardingViewModelProtocol { - var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set } - static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol - var context: AllChatsOnboardingViewModelType.Context { get } +protocol PollHistoryDetailViewModelProtocol { + var completion: PollHistoryDetailViewModelCallback? { get set } + var context: PollHistoryDetailViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift new file mode 100644 index 000000000..952a271fb --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift @@ -0,0 +1,36 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +class PollHistoryDetailUITests: MockScreenTestCase { + func testPollHistoryDetailOpenPoll() { + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.openDisclosed.title) + let title = app.navigationBars.staticTexts.firstMatch.label + XCTAssertEqual(title, VectorL10n.pollHistoryActiveSegmentTitle) + XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01") + XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline) + } + + func testPollHistoryDetailClosedPoll() { + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.closedDisclosed.title) + let title = app.navigationBars.staticTexts.firstMatch.label + XCTAssertEqual(title, VectorL10n.pollHistoryPastSegmentTitle) + XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01") + XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift new file mode 100644 index 000000000..a501cbeb0 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -0,0 +1,67 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import RiotSwiftUI + +class PollHistoryDetailViewModelTests: XCTestCase { + private enum Constants { + static let counterInitialValue = 0 + } + + var viewModel: PollHistoryDetailViewModel! + var context: PollHistoryDetailViewModelType.Context! + + override func setUpWithError() throws { + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] + + let timelinePoll = TimelinePollDetails(id: "poll-id", + question: "Question", + answerOptions: answerOptions, + closed: false, + startDate: .init(), + totalAnswerCount: 3, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + viewModel = PollHistoryDetailViewModel(poll: timelinePoll) + context = viewModel.context + } + + func testInitialState() { + XCTAssertFalse(context.viewState.isPollClosed) + } + + func testProcessAction() { + viewModel.completion = { result in + XCTAssertEqual(result, .viewInTimeline) + } + viewModel.process(viewAction: .viewInTimeline) + } + + func testProcessDismiss() { + viewModel.completion = { result in + XCTAssertEqual(result, .dismiss) + } + viewModel.process(viewAction: .dismiss) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift new file mode 100644 index 000000000..54b327936 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -0,0 +1,111 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollHistoryDetail: View { + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollHistoryDetailViewModel.Context + var contentPoll: any View + + var body: some View { + navigation + } + + private var navigation: some View { + if #available(iOS 16.0, *) { + return NavigationStack { + content + } + } else { + return NavigationView { + content + } + } + } + + private var content: some View { + ScrollView { + VStack(alignment: .leading) { + Text(DateFormatter.pollShortDateFormatter.string(from: viewModel.viewState.pollStartDate)) + .foregroundColor(theme.colors.tertiaryContent) + .font(theme.fonts.caption1) + .padding([.top]) + .accessibilityIdentifier("PollHistoryDetail.date") + AnyView(contentPoll) + .navigationTitle(navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: backButton, trailing: doneButton) + viewInTimeline + } + } + .padding([.horizontal], 16) + .padding([.top, .bottom]) + .background(theme.colors.background.ignoresSafeArea()) + } + + private var backButton: some View { + Button(action: { + viewModel.send(viewAction: .dismiss) + }) { + Image(systemName: "chevron.left") + .aspectRatio(contentMode: .fit) + .foregroundColor(theme.colors.accent) + } + } + + private var doneButton: some View { + Button { + viewModel.send(viewAction: .dismiss) + } label: { + Text(VectorL10n.done) + } + .accentColor(theme.colors.accent) + } + + private var viewInTimeline: some View { + Button { + viewModel.send(viewAction: .viewInTimeline) + } label: { + Text(VectorL10n.pollHistoryDetailViewInTimeline) + } + .accentColor(theme.colors.accent) + .accessibilityIdentifier("PollHistoryDetail.viewInTimeLineButton") + } + + private var navigationTitle: String { + if viewModel.viewState.isPollClosed { + return VectorL10n.pollHistoryPastSegmentTitle + } else { + return VectorL10n.pollHistoryActiveSegmentTitle + } + } +} + +// MARK: - Previews + +struct PollHistoryDetail_Previews: PreviewProvider { + static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift new file mode 100644 index 000000000..4f4f2a606 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -0,0 +1,57 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// MARK: View model + +enum PollHistoryConstants { + static let chunkSizeInDays: UInt = 30 +} + +enum PollHistoryViewModelResult { + case showPollDetail(poll: TimelinePollDetails) +} + +// MARK: View + +enum PollHistoryMode: CaseIterable { + case active + case past +} + +struct PollHistoryViewBindings { + var mode: PollHistoryMode + var alertInfo: AlertInfo? +} + +struct PollHistoryViewState: BindableState { + init(mode: PollHistoryMode) { + bindings = .init(mode: mode) + } + + var bindings: PollHistoryViewBindings + var isLoading = false + var canLoadMoreContent = true + var polls: [TimelinePollDetails]? + var syncStartDate: Date = .init() + var syncedUpTo: Date = .distantFuture +} + +enum PollHistoryViewAction { + case viewAppeared + case segmentDidChange + case showPollDetail(poll: TimelinePollDetails) + case loadMoreContent +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift new file mode 100644 index 000000000..fa02f1a4f --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -0,0 +1,153 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias PollHistoryViewModelType = StateStoreViewModel + +final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol { + private let pollService: PollHistoryServiceProtocol + private var polls: [TimelinePollDetails]? + private var subcriptions: Set = .init() + + var completion: ((PollHistoryViewModelResult) -> Void)? + + init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) { + self.pollService = pollService + super.init(initialViewState: PollHistoryViewState(mode: mode)) + state.canLoadMoreContent = pollService.hasNextBatch + } + + // MARK: - Public + + override func process(viewAction: PollHistoryViewAction) { + switch viewAction { + case .viewAppeared: + setupUpdateSubscriptions() + fetchContent() + case .segmentDidChange: + updateViewState() + case .showPollDetail(let poll): + completion?(.showPollDetail(poll: poll)) + case .loadMoreContent: + fetchContent() + } + } +} + +private extension PollHistoryViewModel { + func fetchContent() { + state.isLoading = true + + pollService + .nextBatch() + .collect() + .sink { [weak self] completion in + self?.handleBatchEnded(completion: completion) + } receiveValue: { [weak self] polls in + self?.add(polls: polls) + } + .store(in: &subcriptions) + } + + func handleBatchEnded(completion: Subscribers.Completion) { + state.isLoading = false + state.canLoadMoreContent = pollService.hasNextBatch + + switch completion { + case .finished: + break + case .failure: + polls = polls ?? [] + state.bindings.alertInfo = .init(id: true, title: VectorL10n.pollHistoryFetchingError) + } + + updateViewState() + } + + func setupUpdateSubscriptions() { + subcriptions.removeAll() + + pollService + .updates + .sink { [weak self] detail in + self?.update(poll: detail) + self?.updateViewState() + } + .store(in: &subcriptions) + + pollService + .fetchedUpTo + .weakAssign(to: \.state.syncedUpTo, on: self) + .store(in: &subcriptions) + + pollService + .livePolls + .sink { [weak self] livePoll in + self?.add(polls: [livePoll]) + self?.updateViewState() + } + .store(in: &subcriptions) + } + + func update(poll: TimelinePollDetails) { + guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else { + return + } + + polls?[pollIndex] = poll + } + + func add(polls: [TimelinePollDetails]) { + self.polls = (self.polls ?? []) + polls + } + + func updateViewState() { + let renderedPolls: [TimelinePollDetails]? + + switch context.mode { + case .active: + renderedPolls = polls?.filter { $0.closed == false } + case .past: + renderedPolls = polls?.filter { $0.closed == true } + } + + state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate }) + } +} + +extension PollHistoryViewModel.Context { + var emptyPollsText: String { + switch (viewState.bindings.mode, viewState.canLoadMoreContent) { + case (.active, true): + return VectorL10n.pollHistoryNoActivePollPeriodText("\(syncedPastDays)") + case (.active, false): + return VectorL10n.pollHistoryNoActivePollText + case (.past, true): + return VectorL10n.pollHistoryNoPastPollPeriodText("\(syncedPastDays)") + case (.past, false): + return VectorL10n.pollHistoryNoPastPollText + } + } + + var syncedPastDays: Int { + guard let days = Calendar.current.dateComponents([.day], from: viewState.syncedUpTo, to: viewState.syncStartDate).day else { + return 0 + } + return max(0, days) + } +} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift similarity index 77% rename from RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift rename to RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift index eab3cdcdf..d116c0254 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift @@ -16,7 +16,7 @@ import Foundation -/// Class that can be extended that supports injection and the `@Inject` property wrapper. -open class InjectableObject: Injectable { - var dependencies: DependencyContainer! +protocol PollHistoryViewModelProtocol { + var completion: ((PollHistoryViewModelResult) -> Void)? { get set } + var context: PollHistoryViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift new file mode 100644 index 000000000..7f6d8c5f6 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -0,0 +1,233 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import MatrixSDK + +final class PollHistoryService: PollHistoryServiceProtocol { + private let room: MXRoom + private let timeline: MXEventTimeline + private let chunkSizeInDays: UInt + + private var timelineListener: Any? + private var roomListener: Any? + + // polls aggregation + private var pollAggregationContexts: [String: PollAggregationContext] = [:] + + // polls + private var currentBatchSubject: PassthroughSubject? + private var livePollsSubject: PassthroughSubject = .init() + + // polls updates + private let updatesSubject: PassthroughSubject = .init() + + // timestamps + private var targetTimestamp: Date = .init() + private var oldestEventDateSubject: CurrentValueSubject = .init(.init()) + + var updates: AnyPublisher { + updatesSubject.eraseToAnyPublisher() + } + + init(room: MXRoom, chunkSizeInDays: UInt) { + self.room = room + self.chunkSizeInDays = chunkSizeInDays + timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) + setupTimeline() + setupLiveUpdates() + } + + func nextBatch() -> AnyPublisher { + currentBatchSubject?.eraseToAnyPublisher() ?? startPagination() + } + + var hasNextBatch: Bool { + timeline.canPaginate(.backwards) + } + + var fetchedUpTo: AnyPublisher { + oldestEventDateSubject.eraseToAnyPublisher() + } + + var livePolls: AnyPublisher { + livePollsSubject.eraseToAnyPublisher() + } + + deinit { + guard let roomListener = roomListener else { + return + } + room.removeListener(roomListener) + } + + class PollAggregationContext { + var pollAggregator: PollAggregator? + let isLivePoll: Bool + var published: Bool + + init(pollAggregator: PollAggregator? = nil, isLivePoll: Bool, published: Bool = false) { + self.pollAggregator = pollAggregator + self.isLivePoll = isLivePoll + self.published = published + } + } +} + +private extension PollHistoryService { + enum Constants { + static let pageSize: UInt = 250 + } + + func setupTimeline() { + timeline.resetPagination() + + timelineListener = timeline.listenToEvents { [weak self] event, _, _ in + if event.eventType == .pollStart { + self?.aggregatePoll(pollStartEvent: event, isLivePoll: false) + } + + self?.updateTimestamp(event: event) + } + } + + func setupLiveUpdates() { + roomListener = room.listen(toEventsOfTypes: [kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381]) { [weak self] event, _, _ in + if event.eventType == .pollStart { + self?.aggregatePoll(pollStartEvent: event, isLivePoll: true) + } + } + } + + func updateTimestamp(event: MXEvent) { + oldestEventDate = min(event.originServerDate, oldestEventDate) + } + + func startPagination() -> AnyPublisher { + let startingTimestamp = oldestEventDate + targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) ?? startingTimestamp + + let batchSubject = PassthroughSubject() + currentBatchSubject = batchSubject + + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + self.paginate() + } + + return batchSubject.eraseToAnyPublisher() + } + + func paginate() { + timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in + guard let self = self else { + return + } + + switch response { + case .success: + if self.timeline.canPaginate(.backwards), self.timestampTargetReached == false { + self.paginate() + } else { + self.completeBatch(completion: .finished) + } + case .failure(let error): + self.completeBatch(completion: .failure(error)) + } + } + } + + func completeBatch(completion: Subscribers.Completion) { + currentBatchSubject?.send(completion: completion) + currentBatchSubject = nil + } + + func aggregatePoll(pollStartEvent: MXEvent, isLivePoll: Bool) { + let eventId: String = pollStartEvent.eventId + + guard pollAggregationContexts[eventId] == nil else { + return + } + + let newContext: PollAggregationContext = .init(isLivePoll: isLivePoll) + pollAggregationContexts[eventId] = newContext + + do { + newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) + } catch { + pollAggregationContexts.removeValue(forKey: eventId) + } + } + + var timestampTargetReached: Bool { + oldestEventDate <= targetTimestamp + } + + var oldestEventDate: Date { + get { + oldestEventDateSubject.value + } + set { + oldestEventDateSubject.send(newValue) + } + } +} + +private extension Date { + func subtractingDays(_ days: UInt) -> Date? { + Calendar.current.date(byAdding: DateComponents(day: -Int(days)), to: self) + } +} + +private extension MXEvent { + var originServerDate: Date { + .init(timeIntervalSince1970: Double(originServerTs) / 1000) + } +} + +// MARK: - PollAggregatorDelegate + +extension PollHistoryService: PollAggregatorDelegate { + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } + + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } + + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else { + return + } + + context.published = true + + let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) + + if context.isLivePoll { + livePollsSubject.send(newPoll) + } else { + currentBatchSubject?.send(newPoll) + } + } + + func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { + guard let context = pollAggregationContexts[aggregator.poll.id], context.published else { + return + } + updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift new file mode 100644 index 000000000..c98f4e136 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -0,0 +1,83 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +final class MockPollHistoryService: PollHistoryServiceProtocol { + lazy var nextBatchPublishers: [AnyPublisher] = [ + (activePollsData + pastPollsData) + .publisher + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + ] + + func nextBatch() -> AnyPublisher { + nextBatchPublishers.isEmpty ? Empty().eraseToAnyPublisher() : nextBatchPublishers.removeFirst() + } + + var updatesPublisher: AnyPublisher = Empty().eraseToAnyPublisher() + var updates: AnyPublisher { + updatesPublisher + } + + var hasNextBatch = true + + var fetchedUpToPublisher: AnyPublisher = Just(.init()).eraseToAnyPublisher() + var fetchedUpTo: AnyPublisher { + fetchedUpToPublisher + } + + var livePollsPublisher: AnyPublisher = Empty().eraseToAnyPublisher() + var livePolls: AnyPublisher { + livePollsPublisher + } +} + +private extension MockPollHistoryService { + var activePollsData: [TimelinePollDetails] { + (1...3) + .map { index in + TimelinePollDetails(id: "a\(index)", + question: "Do you like the active poll number \(index)?", + answerOptions: [], + closed: false, + startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } + } + + var pastPollsData: [TimelinePollDetails] { + (1...3) + .map { index in + TimelinePollDetails(id: "p\(index)", + question: "Do you like the active poll number \(index)?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], + closed: true, + startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift new file mode 100644 index 000000000..5132478cc --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -0,0 +1,37 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +protocol PollHistoryServiceProtocol { + /// Returns a Publisher publishing the polls in the next batch. + /// Implementations should return the same publisher if `nextBatch()` is called again before the previous publisher completes. + func nextBatch() -> AnyPublisher + + /// Publishes updates for the polls previously pusblished by the `nextBatch()` or `livePolls` publishers. + var updates: AnyPublisher { get } + + /// Publishes live polls not related with the current batch. + var livePolls: AnyPublisher { get } + + /// Returns true every time the service can fetch another batch. + /// There is no guarantee the `nextBatch()` returned publisher will publish something anyway. + var hasNextBatch: Bool { get } + + /// Publishes the date up to the service is synced (in the past). + /// This date doesn't need to be related with any poll event. + var fetchedUpTo: AnyPublisher { get } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift new file mode 100644 index 000000000..ddce4978c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -0,0 +1,115 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +final class PollHistoryUITests: MockScreenTestCase { + func testActivePollHistoryHasContent() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + let winningOption = app.staticTexts["PollListData.winningOption"] + + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertTrue(selectedSegment.exists) + XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertFalse(winningOption.exists) + } + + func testPastPollHistoryHasContent() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.past.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + let winningOption = app.buttons["PollAnswerOption0"] + + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertTrue(selectedSegment.exists) + XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertTrue(winningOption.exists) + } + + func testActivePollHistoryHasContentAndCantLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeNoMoreContent.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertFalse(loadMoreButton.exists) + } + + func testActivePollHistoryHasContentAndCanLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.contentLoading.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertFalse(loadMoreButton.isEnabled) + } + + func testActivePollHistoryEmptyAndCanLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.empty.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertTrue(loadMoreButton.isEnabled) + } + + func testActivePollHistoryEmptyAndLoading() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyLoading.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertFalse(loadMoreButton.isEnabled) + } + + func testActivePollHistoryEmptyAndCantLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyNoMoreContent.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertFalse(loadMoreButton.exists) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift new file mode 100644 index 000000000..efce641d4 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -0,0 +1,135 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +@testable import RiotSwiftUI +import XCTest + +final class PollHistoryViewModelTests: XCTestCase { + private var viewModel: PollHistoryViewModel! + private var pollHistoryService: MockPollHistoryService = .init() + + override func setUpWithError() throws { + pollHistoryService = .init() + viewModel = .init(mode: .active, pollService: pollHistoryService) + } + + func testEmitsContentOnLanding() throws { + XCTAssert(viewModel.state.polls == nil) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(try polls.isEmpty) + } + + func testLoadingState() throws { + XCTAssertFalse(viewModel.state.isLoading) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) + XCTAssertFalse(try polls.isEmpty) + } + + func testLoadingStateIsTrueWhileLoading() { + XCTAssertFalse(viewModel.state.isLoading) + pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls, MockPollPublisher.emptyPolls] + viewModel.process(viewAction: .viewAppeared) + XCTAssertTrue(viewModel.state.isLoading) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) + } + + func testUpdatesAreHandled() throws { + let mockUpdates: PassthroughSubject = .init() + pollHistoryService.updatesPublisher = mockUpdates.eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + + var firstPoll = try XCTUnwrap(try polls.first) + XCTAssertEqual(firstPoll.question, "Do you like the active poll number 1?") + firstPoll.question = "foo" + + mockUpdates.send(firstPoll) + + let updatedPoll = try XCTUnwrap(viewModel.state.polls?.first) + XCTAssertEqual(updatedPoll.question, "foo") + } + + func testSegmentsAreUpdated() throws { + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(try polls.isEmpty) + XCTAssertTrue(try polls.allSatisfy { !$0.closed }) + + viewModel.state.bindings.mode = .past + viewModel.process(viewAction: .segmentDidChange) + + XCTAssertTrue(try polls.allSatisfy(\.closed)) + } + + func testPollsAreReverseOrdered() throws { + viewModel.process(viewAction: .viewAppeared) + + let pollDates = try polls.map(\.startDate) + XCTAssertEqual(pollDates, pollDates.sorted(by: { $0 > $1 })) + } + + func testLivePollsAreHandled() throws { + pollHistoryService.nextBatchPublishers = [MockPollPublisher.emptyPolls] + pollHistoryService.livePollsPublisher = Just(mockPoll).eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + XCTAssertEqual(viewModel.state.polls?.count, 1) + XCTAssertEqual(viewModel.state.polls?.first?.id, "id") + } + + func testLivePollsDontChangeLoadingState() throws { + let livePolls = PassthroughSubject() + pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls] + pollHistoryService.livePollsPublisher = livePolls.eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + XCTAssertTrue(viewModel.state.isLoading) + XCTAssertNil(viewModel.state.polls) + livePolls.send(mockPoll) + XCTAssertTrue(viewModel.state.isLoading) + XCTAssertNotNil(viewModel.state.polls) + XCTAssertEqual(viewModel.state.polls?.count, 1) + } + + func testAfterFailureCompletionIsCalled() throws { + pollHistoryService.nextBatchPublishers = [MockPollPublisher.failure] + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) + XCTAssertNotNil(viewModel.state.polls) + XCTAssertNotNil(viewModel.state.bindings.alertInfo) + } +} + +private extension PollHistoryViewModelTests { + var polls: [TimelinePollDetails] { + get throws { + try XCTUnwrap(viewModel.state.polls) + } + } + + var mockPoll: TimelinePollDetails { + .init(id: "id", + question: "Do you like polls?", + answerOptions: [], + closed: false, + startDate: .init(), + totalAnswerCount: 3, + type: .undisclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift new file mode 100644 index 000000000..612f85089 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -0,0 +1,157 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollHistory: View { + @Environment(\.theme) private var theme + + @ObservedObject var viewModel: PollHistoryViewModel.Context + + var body: some View { + VStack { + SegmentedPicker( + segments: PollHistoryMode.allCases, + selection: $viewModel.mode, + interSegmentSpacing: 14 + ) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + + content + } + .padding(.top, 32) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) + .navigationTitle(VectorL10n.pollHistoryTitle) + .onAppear { + viewModel.send(viewAction: .viewAppeared) + } + .onChange(of: viewModel.mode) { _ in + viewModel.send(viewAction: .segmentDidChange) + } + .alert(item: $viewModel.alertInfo) { + $0.alert + } + } + + @ViewBuilder + private var content: some View { + if viewModel.viewState.polls == nil { + loadingView + } else if viewModel.viewState.polls?.isEmpty == true { + noPollsView + } else { + pollListView + } + } + + private var pollListView: some View { + ScrollView { + LazyVStack(spacing: 32) { + ForEach(viewModel.viewState.polls ?? []) { pollData in + Button(action: { + viewModel.send(viewAction: .showPollDetail(poll: pollData)) + }) { + PollListItem(pollData: pollData) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + loadMoreButton + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 32) + .padding(.horizontal, 16) + } + } + + @ViewBuilder + private var loadMoreButton: some View { + if viewModel.viewState.canLoadMoreContent { + HStack(spacing: 8) { + if viewModel.viewState.isLoading { + spinner + } + + Button { + viewModel.send(viewAction: .loadMoreContent) + } label: { + Text(VectorL10n.pollHistoryLoadMore) + .font(theme.fonts.body) + } + .accessibilityIdentifier("PollHistory.loadMore") + .disabled(viewModel.viewState.isLoading) + } + } + } + + private var spinner: some View { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + + private var noPollsView: some View { + VStack(spacing: 32) { + Text(viewModel.emptyPollsText) + .font(theme.fonts.body) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondaryContent) + .padding(.horizontal, 16) + .accessibilityIdentifier("PollHistory.emptyText") + + if viewModel.viewState.canLoadMoreContent { + loadMoreButton + } + } + .frame(maxHeight: .infinity) + } + + private var loadingView: some View { + HStack(spacing: 8) { + spinner + + Text(VectorL10n.pollHistoryLoadingText) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxHeight: .infinity) + .accessibilityIdentifier("PollHistory.loadingText") + } + .padding(.horizontal, 16) + } +} + +extension PollHistoryMode: CustomStringConvertible { + var description: String { + switch self { + case .active: + return VectorL10n.pollHistoryActiveSegmentTitle + case .past: + return VectorL10n.pollHistoryPastSegmentTitle + } + } +} + +// MARK: - Previews + +struct PollHistory_Previews: PreviewProvider { + static let stateRenderer = MockPollHistoryScreenState.stateRenderer + + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift new file mode 100644 index 000000000..6ee1b0ddf --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -0,0 +1,131 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollListItem: View { + @Environment(\.theme) private var theme + + private let pollData: TimelinePollDetails + @ScaledMetric private var imageSize = 16 + + init(pollData: TimelinePollDetails) { + self.pollData = pollData + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(DateFormatter.pollShortDateFormatter.string(from: pollData.startDate)) + .foregroundColor(theme.colors.tertiaryContent) + .font(theme.fonts.caption1) + + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(uiImage: Asset.Images.pollHistory.image) + .resizable() + .frame(width: imageSize, height: imageSize) + + Text(pollData.question) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + .lineLimit(2) + .accessibilityLabel("PollListItem.title") + } + .frame(maxWidth: .infinity, alignment: .leading) + + if pollData.closed { + VStack(alignment: .leading, spacing: 12) { + let winningOptions = pollData.answerOptions.filter(\.winner) + + ForEach(winningOptions) { + TimelinePollAnswerOptionButton(poll: pollData, answerOption: $0, action: nil) + } + + resultView + } + } + } + } + + private var resultView: some View { + let text = pollData.totalAnswerCount == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.totalAnswerCount)) + + return Text(text) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + } +} + +extension DateFormatter { + static let pollShortDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.dateStyle = .short + formatter.timeZone = .init(identifier: "UTC") + return formatter + }() +} + +// MARK: - Previews + +struct PollListItem_Previews: PreviewProvider { + static var previews: some View { + Group { + let pollData1 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + closed: true, + startDate: .init(), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + let pollData2 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + closed: false, + startDate: .init(), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + let pollData3 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", + answerOptions: [ + .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true), + .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) + ], + closed: true, + startDate: .init(), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + ForEach([pollData1, pollData2, pollData3]) { poll in + PollListItem(pollData: poll) + } + } + .padding() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift new file mode 100644 index 000000000..41891e43a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -0,0 +1,89 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SegmentedPicker: View { + private let segments: [Segment] + private let selection: Binding + private let interSegmentSpacing: CGFloat + + @Environment(\.theme) private var theme + + init(segments: [Segment], selection: Binding, interSegmentSpacing: CGFloat) { + self.segments = segments + self.selection = selection + self.interSegmentSpacing = interSegmentSpacing + } + + var body: some View { + HStack(spacing: interSegmentSpacing) { + ForEach(segments, id: \.hashValue) { segment in + let isSelectedSegment = segment == selection.wrappedValue + + Button { + selection.wrappedValue = segment + } label: { + Text(segment.description) + .font(isSelectedSegment ? theme.fonts.headline : theme.fonts.body) + .underlineBar(isSelectedSegment) + } + .accentColor(isSelectedSegment ? Color(ThemeService.shared().theme.tintColor) : theme.colors.primaryContent) + .accessibilityLabel(segment.description) + .accessibilityValue(isSelectedSegment ? VectorL10n.accessibilitySelected : "") + } + } + } +} + +private extension Text { + @ViewBuilder + func underlineBar(_ isActive: Bool) -> some View { + if #available(iOS 15.0, *) { + overlay(alignment: .bottom) { + if isActive { + Rectangle() + .frame(height: 1) + .offset(y: 2) + } + } + } else { + underline(isActive) + } + } +} + +struct SegmentedPicker_Previews: PreviewProvider { + static var previews: some View { + SegmentedPicker( + segments: [ + "Segment 1", + "Segment 2" + ], + selection: .constant("Segment 1"), + interSegmentSpacing: 14 + ) + + SegmentedPicker( + segments: [ + "Segment 1", + "Segment 2" + ], + selection: .constant("Segment 2"), + interSegmentSpacing: 14 + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift index fd8e74103..9f937d435 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift @@ -51,7 +51,7 @@ enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(RoomAccessTypeChooser(viewModel: viewModel.context, roomName: "Room Name") - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift index bc89e53cb..32564e69d 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift @@ -45,7 +45,7 @@ final class RoomUpgradeCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeService(session: parameters.session, roomId: parameters.roomId, parentSpaceId: parameters.parentSpaceId, versionOverride: parameters.versionOverride)) let view = RoomUpgrade(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) roomUpgradeViewModel = viewModel roomUpgradeHostingController = VectorHostingController(rootView: view) roomUpgradeHostingController.view.backgroundColor = .clear diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift index bca1f0ea7..4529c0eff 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift @@ -49,7 +49,7 @@ enum MockRoomUpgradeScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(RoomUpgrade(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index c3c1cf327..3214fae65 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -33,7 +33,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var pollAggregator: PollAggregator - private var viewModel: TimelinePollViewModelProtocol! + private(set) var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() // MARK: Public @@ -86,6 +86,10 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func toPresentable() -> UIViewController { VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context)) } + + func toView() -> any View { + TimelinePollView(viewModel: viewModel.context) + } func canEndPoll() -> Bool { pollAggregator.poll.isClosed == false @@ -114,10 +118,17 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } // MARK: - Private - - // PollProtocol is intentionally not available in the SwiftUI target as we don't want - // to add the SDK as a dependency to it. We need to translate from one to the other on this level. + func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails { + let representedType: TimelinePollEventType = parameters.pollEvent.eventType == .pollStart ? .started : .ended + return .init(poll: poll, represent: representedType) + } +} + +// PollProtocol is intentionally not available in the SwiftUI target as we don't want +// to add the SDK as a dependency to it. We need to translate from one to the other on this level. +extension TimelinePollDetails { + init(poll: PollProtocol, represent eventType: TimelinePollEventType) { let answerOptions = poll.answerOptions.map { pollAnswerOption in TimelinePollAnswerOption(id: pollAnswerOption.id, text: pollAnswerOption.text, @@ -126,21 +137,27 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel selected: pollAnswerOption.isCurrentUserSelection) } - return TimelinePollDetails(question: poll.text, - answerOptions: answerOptions, - closed: poll.isClosed, - totalAnswerCount: poll.totalAnswerCount, - type: pollKindToTimelinePollType(poll.kind), - eventType: parameters.pollEvent.eventType == .pollStart ? .started : .ended, - maxAllowedSelections: poll.maxAllowedSelections, - hasBeenEdited: poll.hasBeenEdited, - hasDecryptionError: poll.hasDecryptionError) - } - - private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { - let mapping = [PollKind.disclosed: TimelinePollType.disclosed, - PollKind.undisclosed: TimelinePollType.undisclosed] - - return mapping[kind] ?? .disclosed + self.init(id: poll.id, + question: poll.text, + answerOptions: answerOptions, + closed: poll.isClosed, + startDate: poll.startDate, + totalAnswerCount: poll.totalAnswerCount, + type: poll.kind.timelinePollType, + eventType: eventType, + maxAllowedSelections: poll.maxAllowedSelections, + hasBeenEdited: poll.hasBeenEdited, + hasDecryptionError: poll.hasDecryptionError) + } +} + +private extension PollKind { + var timelinePollType: TimelinePollType { + switch self { + case .disclosed: + return .disclosed + case .undisclosed: + return .undisclosed + } } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 8e5a04cd9..0c7233298 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -15,6 +15,7 @@ // import Foundation +import SwiftUI @objcMembers class TimelinePollProvider: NSObject { @@ -45,7 +46,7 @@ class TimelinePollProvider: NSObject { let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event) guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { - return nil + return messageViewController(for: event) } coordinatorsForEventIdentifiers[event.eventId] = coordinator @@ -62,3 +63,14 @@ class TimelinePollProvider: NSObject { coordinatorsForEventIdentifiers.removeAll() } } + +private extension TimelinePollProvider { + func messageViewController(for event: MXEvent) -> UIViewController? { + switch event.eventType { + case .pollEnd: + return VectorHostingController(rootView: TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll)) + default: + return nil + } + } +} diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index cd806da54..a36a7d092 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -29,9 +29,11 @@ class TimelinePollViewModelTests: XCTestCase { TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] - let timelinePoll = TimelinePollDetails(question: "Question", + let timelinePoll = TimelinePollDetails(id: "poll-id", + question: "Question", answerOptions: answerOptions, closed: false, + startDate: .init(), totalAnswerCount: 3, type: .disclosed, eventType: .started, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 3629aae3e..0ee87c55f 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -62,37 +62,20 @@ extension MutableCollection where Element == TimelinePollAnswerOption { } struct TimelinePollDetails { + var id: String var question: String var answerOptions: [TimelinePollAnswerOption] var closed: Bool + var startDate: Date var totalAnswerCount: UInt var type: TimelinePollType var eventType: TimelinePollEventType var maxAllowedSelections: UInt - var hasBeenEdited = true + var hasBeenEdited: Bool var hasDecryptionError: Bool - init(question: String, answerOptions: [TimelinePollAnswerOption], - closed: Bool, - totalAnswerCount: UInt, - type: TimelinePollType, - eventType: TimelinePollEventType, - maxAllowedSelections: UInt, - hasBeenEdited: Bool, - hasDecryptionError: Bool) { - self.question = question - self.answerOptions = answerOptions - self.closed = closed - self.totalAnswerCount = totalAnswerCount - self.type = type - self.eventType = eventType - self.maxAllowedSelections = maxAllowedSelections - self.hasBeenEdited = hasBeenEdited - self.hasDecryptionError = hasDecryptionError - } - var hasCurrentUserVoted: Bool { - answerOptions.filter { $0.selected == true }.count > 0 + answerOptions.contains(where: \.selected) } var shouldDiscloseResults: Bool { @@ -108,6 +91,8 @@ struct TimelinePollDetails { } } +extension TimelinePollDetails: Identifiable { } + struct TimelinePollViewState: BindableState { var poll: TimelinePollDetails var bindings: TimelinePollViewStateBindings diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index a53a745b8..8c70b21e3 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -33,9 +33,11 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - let poll = TimelinePollDetails(question: "Question", + let poll = TimelinePollDetails(id: "id", + question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, + startDate: .init(), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, eventType: self == .closedPollEnded ? .ended : .started, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 85309c31c..dd468b008 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -25,76 +25,83 @@ struct TimelinePollAnswerOptionButton: View { let poll: TimelinePollDetails let answerOption: TimelinePollAnswerOption - let action: () -> Void + let action: (() -> Void)? // MARK: Public var body: some View { - Button(action: action) { + Button { + action?() + } label: { let rect = RoundedRectangle(cornerRadius: 4.0) answerOptionLabel .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 8.0) .padding(.top, 12.0) - .padding(.bottom, 12.0) + .padding(.bottom, 8.0) .clipShape(rect) .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) .accentColor(progressViewAccentColor) } .accessibilityIdentifier("PollAnswerOption\(optionIndex)") + .disabled(action == nil) } var answerOptionLabel: some View { VStack(alignment: .leading, spacing: 12.0) { HStack(alignment: .top, spacing: 8.0) { if !poll.closed { - Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelectedBum.image : Asset.Images.pollCheckboxDefault.image) + } else { + Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) + } } Text(answerOption.text) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label") + .frame(maxWidth: .infinity, alignment: .leading) - if poll.closed, answerOption.winner { - Spacer() - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - } - - if poll.type == .disclosed || poll.closed { - HStack { - ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), - total: Double(poll.totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") + HStack(spacing: 6) { + if poll.closed, answerOption.winner { + Image(uiImage: Asset.Images.pollWinnerIcon.image) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } if poll.shouldDiscloseResults { Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) .font(theme.fonts.footnote) - .foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) + .foregroundColor(poll.closed && answerOption.winner ? Color(ThemeService.shared().theme.tintColor) : theme.colors.secondaryContent) .accessibilityIdentifier("PollAnswerOption\(optionIndex)Count") } } } + + if poll.type == .disclosed || poll.closed { + ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), total: Double(poll.totalAnswerCount)) + .progressViewStyle(LinearProgressViewStyle.linear) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") + } } } var borderAccentColor: Color { guard !poll.closed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) + return (answerOption.winner ? Color(ThemeService.shared().theme.tintColor) : theme.colors.quinaryContent) } - - return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent + + return answerOption.selected ? Color(ThemeService.shared().theme.tintColor) : theme.colors.quinaryContent } var progressViewAccentColor: Color { guard !poll.closed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) + return (answerOption.winner ? Color(ThemeService.shared().theme.tintColor) : theme.colors.quarterlyContent) } - return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent + return answerOption.selected ? Color(ThemeService.shared().theme.tintColor) : theme.colors.quarterlyContent } var optionIndex: Int { @@ -143,12 +150,15 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { } } } + .padding() } static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { - TimelinePollDetails(question: "", + TimelinePollDetails(id: UUID().uuidString, + question: "", answerOptions: [], closed: closed, + startDate: .init(), totalAnswerCount: 100, type: type, eventType: .started, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift new file mode 100644 index 000000000..758cbd490 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift @@ -0,0 +1,45 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view for showing polls' related messages whenever there aren't enough information to show a full poll in the timeline. +struct TimelinePollMessageView: View { + @Environment(\.theme) private var theme + private let imageSize: CGFloat = 16 + + let message: String + + var body: some View { + HStack { + Image(uiImage: Asset.Images.pollHistory.image) + .resizable() + .frame(width: imageSize, height: imageSize) + + Text(message) + .font(.system(size: 15)) + .foregroundColor(theme.colors.primaryContent) + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct TimelinePollMessageView_Previews: PreviewProvider { + static var previews: some View { + TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index c3812b5e8..c6d86a655 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -61,7 +61,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) userSuggestionViewModel = viewModel userSuggestionHostingController = VectorHostingController(rootView: view) @@ -105,7 +105,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { private func calculateViewHeight() -> CGFloat { let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) let controller = VectorHostingController(rootView: view) guard let view = controller.view else { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index f8a8acade..a0ed20268 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -37,7 +37,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { return ( [service, listViewModel], AnyView(UserSuggestionListWithInput(viewModel: viewModel) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index 0d3328b33..862e7573d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -55,6 +55,6 @@ struct UserSuggestionListItem: View { struct UserSuggestionHeader_Previews: PreviewProvider { static var previews: some View { UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 3f5e55c6e..7df3eaa04 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -57,7 +57,8 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } deinit { - viewModel.context.send(viewAction: .redact) + // If init has failed, our viewmodel will be nil. + viewModel?.context.send(viewAction: .redact) } // MARK: - Public @@ -66,7 +67,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { let view = VoiceBroadcastPlaybackView(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) return VectorHostingController(rootView: view) } @@ -80,8 +81,15 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } func endVoiceBroadcast() {} - + func pausePlaying() { viewModel.context.send(viewAction: .pause) } + + func pausePlayingInProgressVoiceBroadcast() { + // Pause the playback if we are playing a live voice broadcast (or waiting for more chunks) + if [.playing, .buffering].contains(viewModel.context.viewState.playbackState), viewModel.context.viewState.broadcastState != .stopped { + viewModel.context.send(viewAction: .pause) + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 3f28ab081..94c10eea4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -78,6 +78,12 @@ import Foundation } } + @objc public func pausePlayingInProgressVoiceBroadcast() { + coordinatorsForEventIdentifiers.forEach { _, coordinator in + coordinator.pausePlayingInProgressVoiceBroadcast() + } + } + private func handleEvent(event: MXEvent, direction: MXTimelineDirection, customObject: Any?) { if direction == .backwards { // ignore backwards events diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 1f5ac9872..35933ffce 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -16,6 +16,7 @@ import Combine import SwiftUI +import MediaPlayer // TODO: VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK // We need a VoiceBroadcastPlaybackServiceProtocol and VoiceBroadcastAggregatorProtocol @@ -43,7 +44,23 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private var reloadVoiceBroadcastChunkQueue: Bool = false private var seekToChunkTime: TimeInterval? + /// The last chunk we tried to load + private var lastChunkProcessed: UInt = 0 + /// The last chunk correctly loaded and added to the player's queue + private var lastChunkAddedToPlayer: UInt = 0 + + private var hasAttachmentErrors: Bool = false { + didSet { + updateErrorState() + } + } + private var isPlayingLastChunk: Bool { + // We can't play the last chunk if the brodcast is not stopped + guard state.broadcastState == .stopped else { + return false + } + let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) guard let chunkDuration = chunks.last?.duration else { return false @@ -52,6 +69,21 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration) } + /// Current chunk loaded in the audio player + private var currentChunk: VoiceBroadcastChunk? { + guard let currentAudioPlayerUrl = audioPlayer?.currentUrl, + let currentEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in + result.url == currentAudioPlayerUrl + })?.eventIdentifier else { + return nil + } + + let currentChunk = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in + chunk.attachment.eventId == currentEventId + }) + return currentChunk + } + private var isLivePlayback: Bool { return (!isPlaybackInitialized || isPlayingLastChunk) && (state.broadcastState == .started || state.broadcastState == .resumed) } @@ -89,7 +121,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic broadcastState: voiceBroadcastAggregator.voiceBroadcastState, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false, canMoveForward: false, canMoveBackward: false), - bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) + bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), + decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0), + showPlaybackError: false) super.init(initialViewState: viewState) displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -168,11 +202,24 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func stopIfVoiceBroadcastOver() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver") + var shouldStop = false + // Check if the broadcast is over before stopping everything - // If not, the player should not stopped. The view state must be move to buffering - if state.broadcastState == .stopped, isPlayingLastChunk { + if state.broadcastState == .stopped { + // If we known the last chunk sequence, use it to check if we need to stop + // Note: it's possible to be in .stopped state and to still have a last chunk sequence at 0 (old versions or a crash during recording). In this case, we use isPlayingLastChunk as a fallback solution + if voiceBroadcastAggregator.voiceBroadcastLastChunkSequence > 0 { + // we should stop only if we have already processed the last chunk + shouldStop = (lastChunkProcessed == voiceBroadcastAggregator.voiceBroadcastLastChunkSequence) + } else { + shouldStop = isPlayingLastChunk + } + } + + if shouldStop { stop() } else { + // If not, the player should not stopped. The view state must be move to buffering state.playbackState = .buffering } } @@ -200,9 +247,12 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func seek(to seekTime: Float) { // Flush the chunks queue and the current audio player playlist + lastChunkProcessed = 0 + lastChunkAddedToPlayer = 0 voiceBroadcastChunkQueue = [] reloadVoiceBroadcastChunkQueue = isProcessingVoiceBroadcastChunk audioPlayer?.removeAllPlayerItems() + hasAttachmentErrors = false let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) @@ -289,12 +339,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } + self.lastChunkProcessed = chunk.sequence + switch result { case .success(let result): guard result.eventIdentifier == chunk.attachment.eventId else { return } - + self.lastChunkAddedToPlayer = max(self.lastChunkAddedToPlayer, chunk.sequence) self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) // Instanciate audioPlayer if needed. @@ -302,6 +354,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // Init and start the player on the first chunk let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) audioPlayer.registerDelegate(self) + self.mediaServiceProvider.registerNowPlayingInfoDelegate(self, forPlayer: audioPlayer) audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) self.audioPlayer = audioPlayer @@ -331,19 +384,46 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic audioPlayer.seekToTime(time) self.seekToChunkTime = nil } - + + self.hasAttachmentErrors = false + self.processNextVoiceBroadcastChunk() + case .failure (let error): - MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) - if self.voiceBroadcastChunkQueue.count == 0 { - // No more chunk to try. Go to error - self.state.playbackState = .error + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: ["error": error, "chunk": chunk.sequence]) + self.hasAttachmentErrors = true + // If nothing has been added to the player's queue, exit the buffer state + if self.lastChunkAddedToPlayer == 0 { + self.pause() } } - - self.processNextVoiceBroadcastChunk() } } + private func resetErrorState() { + state.showPlaybackError = false + } + + private func updateErrorState() { + // Show an error if the playback state is .error + var showPlaybackError = state.playbackState == .error + + // Or if there is an attachment error + if hasAttachmentErrors { + // only if the audio player is not playing and has nothing left to play + let audioPlayerIsPlaying = audioPlayer?.isPlaying ?? false + let currentPlayerTime = audioPlayer?.currentTime ?? 0 + let currentPlayerDuration = audioPlayer?.duration ?? 0 + let currentChunkSequence = currentChunk?.sequence ?? 0 + let hasNoMoreChunkToPlay = (currentChunk == nil && lastChunkAddedToPlayer == 0) || (currentChunkSequence == lastChunkAddedToPlayer) + if !audioPlayerIsPlaying && hasNoMoreChunkToPlay && (currentPlayerDuration - currentPlayerTime < 0.2) { + showPlaybackError = true + } + } + + state.showPlaybackError = showPlaybackError + + } + private func updateDuration() { let duration = voiceBroadcastAggregator.voiceBroadcast.duration state.playingState.duration = Float(duration) @@ -366,23 +446,23 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } else { seek(to: state.bindings.progress) } + resetErrorState() } @objc private func handleDisplayLinkTick() { - guard let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in - result.url == audioPlayer?.currentUrl - })?.eventIdentifier, - let playingSequence = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in - chunk.attachment.eventId == playingEventId - })?.sequence else { + guard let playingSequence = self.currentChunk?.sequence else { return } - - let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in - chunk.sequence < playingSequence - }.reduce(0) { $0 + $1.duration}) + (audioPlayer?.currentTime.rounded() ?? 0) * 1000 - - state.bindings.progress = Float(progress) + + // Get the audioPlayer current time, which is the elapsed time in the currently playing media item. + // Note: if the audioPlayer is not ready (eg. after a seek), its currentTime will be 0 and we shouldn't update the progress to avoid visual glitches. + let currentTime = audioPlayer?.currentTime ?? .zero + if currentTime > 0 { + let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in + chunk.sequence < playingSequence + }.reduce(0) { $0 + $1.duration}) + currentTime * 1000 + state.bindings.progress = Float(progress) + } updateUI() } @@ -401,7 +481,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic state.playingState.remainingTimeLabel = label state.playingState.canMoveBackward = state.bindings.progress > 0 - state.playingState.canMoveForward = state.bindings.progress < state.playingState.duration + state.playingState.canMoveForward = (state.playingState.duration - state.bindings.progress) > 500 } private func handleVoiceBroadcastChunksProcessing() { @@ -436,49 +516,123 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { // Handle the live icon appearance state.playingState.isLive = isLivePlayback + + // Handle the case where the playback state is .buffering and the new broadcast state is .stopped + if didReceiveState == .stopped, self.state.playbackState == .buffering { + stopIfVoiceBroadcastOver() + } } func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { - updateDuration() if state.playbackState != .stopped, !isActuallyPaused { handleVoiceBroadcastChunksProcessing() } } + + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) { + state.decryptionState.errorCount = events.count + if events.count > 0 { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] voice broadcast decryption error count: \(events.count)/\(aggregator.voiceBroadcast.chunks.count)") + + if [.playing, .buffering].contains(state.playbackState) { + pause() + } + } + } } // MARK: - VoiceMessageAudioPlayerDelegate extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + updateErrorState() } func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { state.playbackState = .playing state.playingState.isLive = isLivePlayback isPlaybackInitialized = true + displayLink.isPaused = false + resetErrorState() } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { state.playbackState = .paused state.playingState.isLive = false + displayLink.isPaused = true } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying") state.playbackState = .stopped + + updateErrorState() + state.playingState.isLive = false audioPlayer.deregisterDelegate(self) + self.mediaServiceProvider.deregisterNowPlayingInfoDelegate(forPlayer: audioPlayer) self.audioPlayer = nil + displayLink.isPaused = true } func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { state.playbackState = .error + updateErrorState() } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidFinishPlaying: \(audioPlayer.playerItems.count)") - stopIfVoiceBroadcastOver() + if hasAttachmentErrors { + stop() + } else { + stopIfVoiceBroadcastOver() + } + } +} + +// MARK: - VoiceMessageNowPlayingInfoDelegate + +extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoDelegate { + + func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool { + guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else { + return false + } + + // we should setup the remote command center only for ended voice broadcast because we won't get new chunk if the app is in background. + return state.broadcastState == .stopped + } + + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool { + guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else { + return true + } + + // we should disconnect from the now playing info center if the playback is stopped or if the broadcast is in progress + return state.playbackState == .stopped || state.broadcastState != .stopped + } + + func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) { + guard audioPlayer != nil, audioPlayer === player else { + return + } + + // Don't update the NowPlayingInfoCenter for live broadcasts + guard state.broadcastState == .stopped else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + return + } + + let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + nowPlayingInfoCenter.nowPlayingInfo = [ + // Title + MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder, + // Duration + MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any, + // Elapsed time + MPNowPlayingInfoPropertyElapsedPlaybackTime: (state.bindings.progress / 1000.0) as Any, + ] } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift new file mode 100644 index 000000000..598bde5c3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift @@ -0,0 +1,47 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VoiceBroadcastPlaybackDecryptionErrorView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var body: some View { + ZStack { + HStack(spacing: 0) { + Image(uiImage: Asset.Images.errorIcon.image) + .frame(width: 40, height: 40) + Text(VectorL10n.voiceBroadcastPlaybackUnableToDecrypt) + .multilineTextAlignment(.center) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct VoiceBroadcastPlaybackDecryptionErrorView_Previews: PreviewProvider { + static var previews: some View { + VoiceBroadcastPlaybackDecryptionErrorView() + } +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift index 0ac7822c6..0836bc661 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift @@ -28,19 +28,17 @@ struct VoiceBroadcastPlaybackErrorView: View { var action: (() -> Void)? var body: some View { - VStack { - VStack { + ZStack { + HStack { Image(uiImage: Asset.Images.errorIcon.image) .frame(width: 40, height: 40) Text(VectorL10n.voiceBroadcastPlaybackLoadingError) .multilineTextAlignment(.center) .font(theme.fonts.caption1) - .foregroundColor(theme.colors.primaryContent) + .foregroundColor(theme.colors.alert) } - .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(theme.colors.system.ignoresSafeArea()) } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 09ed1ff44..b4bcaa7aa 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -91,7 +91,7 @@ struct VoiceBroadcastPlaybackView: View { } } }.frame(maxWidth: .infinity, alignment: .leading) - + if viewModel.viewState.broadcastState != .stopped { Label { Text(VectorL10n.voiceBroadcastLive) @@ -109,7 +109,12 @@ struct VoiceBroadcastPlaybackView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 4.0, trailing: 0.0)) - if viewModel.viewState.playbackState == .error { + if viewModel.viewState.decryptionState.errorCount > 0 { + VoiceBroadcastPlaybackDecryptionErrorView() + .fixedSize(horizontal: false, vertical: true) + .accessibilityIdentifier("decryptionErrorView") + } + else if viewModel.viewState.showPlaybackError { VoiceBroadcastPlaybackErrorView() } else { HStack (spacing: 34.0) { @@ -156,8 +161,8 @@ struct VoiceBroadcastPlaybackView: View { } VoiceBroadcastSlider(value: $viewModel.progress, - minValue: 0.0, - maxValue: viewModel.viewState.playingState.duration) { didChange in + minValue: 0.0, + maxValue: viewModel.viewState.playingState.duration) { didChange in viewModel.send(viewAction: .sliderChange(didChange: didChange)) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 488b65c1d..7a810a167 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -48,12 +48,18 @@ struct VoiceBroadcastPlayingState { var canMoveBackward: Bool } +struct VoiceBroadcastPlaybackDecryptionState { + var errorCount: Int +} + struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails var broadcastState: VoiceBroadcastInfoState var playbackState: VoiceBroadcastPlaybackState var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings + var decryptionState: VoiceBroadcastPlaybackDecryptionState + var showPlaybackError: Bool } struct VoiceBroadcastPlaybackViewStateBindings { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 306a5be8c..59b434ca9 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -43,11 +43,12 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0), showPlaybackError: false)) return ( [false, viewModel], - AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context)) + AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index 2a9fe90b8..2b33f4121 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -61,13 +61,22 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) + return VectorHostingController(rootView: view) } func pauseRecording() { voiceBroadcastRecorderViewModel.context.send(viewAction: .pause) } + + func pauseRecordingOnError() { + voiceBroadcastRecorderViewModel.context.send(viewAction: .pauseOnError) + } + + func isVoiceBroadcastRecording() -> Bool { + return voiceBroadcastRecorderService.isRecording + } // MARK: - Private } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index e7f998716..b69476593 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,13 +32,16 @@ import Foundation coordinatorsForEventIdentifiers.removeAll() } } + didSet { + sessionState = session?.state + } } private var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]() { didSet { if !self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener == nil { redactionsListener = session?.listenToEvents([MXEventType(identifier: kMXEventTypeStringRoomRedaction)], self.handleRedactedEvent) } - + if self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener != nil { session?.removeListener(self.redactionsListener) self.redactionsListener = nil @@ -49,9 +52,19 @@ import Foundation // MARK: Private private var currentEventIdentifier: String? - + private var sessionState: MXSessionState? + + private var sessionStateDidChangeObserver: Any? + // MARK: - Setup - private override init() { } + private override init() { + super.init() + self.registerNotificationObservers() + } + + deinit { + unregisterNotificationObservers() + } // MARK: - Public @@ -85,6 +98,19 @@ import Foundation voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecording() } + /// Pause current voice broadcast recording without sending pending events. + @objc public func pauseRecordingOnError() { + voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecordingOnError() + } + + @objc public func isVoiceBroadcastRecording() -> Bool { + guard let coordinator = voiceBroadcastRecorderCoordinatorForCurrentEvent() else { + return false + } + + return coordinator.isVoiceBroadcastRecording() + } + // MARK: - Private /// Retrieve the voiceBroadcast recorder coordinator for the current event or nil if it hasn't been created yet @@ -92,7 +118,7 @@ import Foundation guard let currentEventIdentifier = currentEventIdentifier else { return nil } - + return coordinatorsForEventIdentifiers[currentEventIdentifier] } @@ -101,11 +127,43 @@ import Foundation // ignore backwards events return } - + var coordinator = coordinatorsForEventIdentifiers.removeValue(forKey: event.redacts) - + coordinator?.toPresentable().dismiss(animated: false) { - coordinator = nil + coordinator = nil + } + } + + // MARK: - Notification handling + + private func registerNotificationObservers() { + self.sessionStateDidChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: session, queue: nil) { [weak self] notification in + guard let self else { return } + guard let concernedSession = notification.object as? MXSession, self.session === concernedSession else { return } + + self.update(sessionState: concernedSession.state) + } + } + + private func unregisterNotificationObservers() { + if let observer = self.sessionStateDidChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + // MARK: - Session state + private func update(sessionState: MXSessionState) { + let oldState = self.sessionState + self.sessionState = sessionState + + switch (oldState, sessionState) { + case (_, .homeserverNotReachable): + pauseRecordingOnError() + case (_, .running): + pauseRecording() + default: + break } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index 437abbe3c..c4851e779 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -44,6 +44,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // MARK: Public weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? + var isRecording: Bool { + return audioEngine.isRunning + } // MARK: - Setup @@ -113,6 +116,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // Discard the service on VoiceBroadcastService error. We keep the service in case of other error type if error as? VoiceBroadcastServiceError != nil { self.tearDownVoiceBroadcastService() + } else { + AppDelegate.theDelegate().showError(asAlert: error) } }) } @@ -133,6 +138,10 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error) + // Pause voice broadcast recording without sending pending events. + if error is VoiceBroadcastServiceError == false { + AppDelegate.theDelegate().showError(asAlert: error) + } }) } @@ -148,6 +157,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { UIApplication.shared.isIdleTimerDisabled = true }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error) + if error is VoiceBroadcastServiceError == false { + AppDelegate.theDelegate().showError(asAlert: error) + } }) } @@ -166,6 +178,15 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { self.tearDownVoiceBroadcastService() } + func pauseOnErrorRecordingVoiceBroadcast() { + audioEngine.pause() + UIApplication.shared.isIdleTimerDisabled = false + invalidateTimer() + + // Update state + self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .error) + } + // MARK: - Private /// Reset chunk values. private func resetValues() { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift index 9e48e2e9a..78492fe15 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift @@ -25,6 +25,9 @@ protocol VoiceBroadcastRecorderServiceProtocol { /// Service delegate var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? { get set } + /// Returns if a voice broadcast is currently recording. + var isRecording: Bool { get } + /// Start voice broadcast recording. func startRecordingVoiceBroadcast() @@ -39,4 +42,7 @@ protocol VoiceBroadcastRecorderServiceProtocol { /// Cancel voice broadcast recording after redacted it. func cancelRecordingVoiceBroadcast() + + /// Pause voice broadcast recording without sending pending events. + func pauseOnErrorRecordingVoiceBroadcast() } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift new file mode 100644 index 000000000..051a6477b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift @@ -0,0 +1,49 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VoiceBroadcastRecorderConnectionErrorView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var action: (() -> Void)? + + var body: some View { + ZStack { + HStack(spacing: 0) { + Image(uiImage: Asset.Images.errorIcon.image) + .frame(width: 40, height: 40) + Text(VectorL10n.voiceBroadcastRecorderConnectionError) + .multilineTextAlignment(.center) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct VoiceBroadcastRecorderConnectionErrorView_Previews: PreviewProvider { + static var previews: some View { + VoiceBroadcastRecorderConnectionErrorView() + } +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index c0cafed9b..c8e6532f6 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -26,7 +26,7 @@ struct VoiceBroadcastRecorderView: View { @State private var showingStopAlert = false private var backgroundColor: Color { - if viewModel.viewState.recordingState != .paused { + if viewModel.viewState.recordingState != .paused, viewModel.viewState.recordingState != .error { return theme.colors.alert } return theme.colors.quarterlyContent @@ -78,47 +78,53 @@ struct VoiceBroadcastRecorderView: View { .accessibilityIdentifier("liveButton") } - HStack(alignment: .top, spacing: 34.0) { - Button { - switch viewModel.viewState.recordingState { - case .started, .resumed: - viewModel.send(viewAction: .pause) - case .stopped: - viewModel.send(viewAction: .start) - case .paused: - viewModel.send(viewAction: .resume) + if viewModel.viewState.recordingState == .error { + VoiceBroadcastRecorderConnectionErrorView() + } else { + HStack(alignment: .top, spacing: 34.0) { + Button { + switch viewModel.viewState.recordingState { + case .started, .resumed: + viewModel.send(viewAction: .pause) + case .stopped: + viewModel.send(viewAction: .start) + case .paused: + viewModel.send(viewAction: .resume) + case .error: + break + } + } label: { + if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed { + Image("voice_broadcast_record_pause") + .renderingMode(.original) + } else { + Image("voice_broadcast_record") + .renderingMode(.original) + } } - } label: { - if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed { - Image("voice_broadcast_record_pause") - .renderingMode(.original) - } else { - Image("voice_broadcast_record") + .accessibilityIdentifier("recordButton") + + Button { + showingStopAlert = true + } label: { + Image("voice_broadcast_stop") .renderingMode(.original) } + .alert(isPresented:$showingStopAlert) { + Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle), + message: Text(VectorL10n.voiceBroadcastStopAlertDescription), + primaryButton: .cancel(), + secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton), + action: { + viewModel.send(viewAction: .stop) + })) + } + .accessibilityIdentifier("stopButton") + .disabled(viewModel.viewState.recordingState == .stopped) + .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0)) } - .accessibilityIdentifier("recordButton") - - Button { - showingStopAlert = true - } label: { - Image("voice_broadcast_stop") - .renderingMode(.original) - } - .alert(isPresented:$showingStopAlert) { - Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle), - message: Text(VectorL10n.voiceBroadcastStopAlertDescription), - primaryButton: .cancel(), - secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton), - action: { - viewModel.send(viewAction: .stop) - })) - } - .accessibilityIdentifier("stopButton") - .disabled(viewModel.viewState.recordingState == .stopped) - .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0)) + .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0)) } - .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0)) } .padding(EdgeInsets(top: 12.0, leading: 4.0, bottom: 12.0, trailing: 4.0)) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift index cb807a430..f992cd2f4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift @@ -21,6 +21,7 @@ enum VoiceBroadcastRecorderViewAction { case stop case pause case resume + case pauseOnError } enum VoiceBroadcastRecorderState { @@ -28,6 +29,7 @@ enum VoiceBroadcastRecorderState { case stopped case paused case resumed + case error } struct VoiceBroadcastRecorderDetails { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift index ba9690bfb..ff486c5df 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift @@ -56,6 +56,8 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic pause() case .resume: resume() + case .pauseOnError: + pauseOnError() } } @@ -80,6 +82,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast() } + private func pauseOnError() { + voiceBroadcastRecorderService.pauseOnErrorRecordingVoiceBroadcast() + } + private func updateRemainingTime(_ remainingTime: UInt) { state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime) } diff --git a/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift b/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift new file mode 100644 index 000000000..2b4827b81 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift @@ -0,0 +1,50 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct RoomWaitingForMembers: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var body: some View { + ZStack { + HStack(alignment: .top) { + Image(uiImage: Asset.Images.membersListIcon.image) + VStack(alignment: .leading, spacing: 6) { + Text(VectorL10n.roomWaitingOtherParticipantsTitle(AppInfo.current.displayName)) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(VectorL10n.roomWaitingOtherParticipantsMessage(AppInfo.current.displayName)) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(9) + .background(theme.colors.system) + .cornerRadius(4) + } + } +} + +struct RoomWaitingForMembers_Previews: PreviewProvider { + static var previews: some View { + RoomWaitingForMembers() + .padding(16) + } +} diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift index f432b44ab..cbc1f4758 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift @@ -109,6 +109,8 @@ struct ChangePasswordScreen: View { Button(action: submit) { Text(VectorL10n.save) + // bwi: use buttonStyle font color + // .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } .buttonStyle(PrimaryActionButtonStyle()) .disabled(!viewModel.viewState.canSubmit) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift index 2c4d16d8b..337ed4516 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift @@ -41,6 +41,10 @@ extension MXPushRule: NotificationPushRuleType { return false } + var ruleActions: NotificationActions? { + .init(notify: notify, highlight: highlight, sound: sound) + } + private func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? { guard let actions = actions as? [MXPushRuleAction] else { return nil diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift index 49166c99e..64aff0388 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift @@ -16,10 +16,12 @@ import Foundation -struct MockNotificationPushRule: NotificationPushRuleType { +struct MockNotificationPushRule: NotificationPushRuleType, Equatable { var ruleId: String! var enabled: Bool + var ruleActions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions + func matches(standardActions: NotificationStandardActions?) -> Bool { - false + standardActions?.actions == ruleActions } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift index 88b11b3be..98fa34764 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift @@ -17,7 +17,7 @@ import Foundation /// The actions defined on a push rule, used in the static push rule definitions. -struct NotificationActions { +struct NotificationActions: Equatable { let notify: Bool let highlight: Bool let sound: String? diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 77c6e1ef2..c30b81fec 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -22,7 +22,7 @@ extension NotificationPushRuleId { /// It is defined similarly across Web and Android. /// - Parameter index: The notification index for which to get the actions for. /// - Returns: The associated `NotificationStandardActions`. - func standardActions(for index: NotificationIndex) -> NotificationStandardActions? { + func standardActions(for index: NotificationIndex) -> NotificationStandardActions { switch self { case .containDisplayName: switch index { @@ -42,7 +42,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .highlight } - case .oneToOneRoom: + case .oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd: switch index { case .off: return .dontNotify case .silent: return .notify @@ -54,7 +54,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .notifyDefaultSound } - case .allOtherMessages: + case .allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd: switch index { case .off: return .dontNotify case .silent: return .notify diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index d74968c8b..46cca080e 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -30,6 +30,18 @@ enum NotificationPushRuleId: String { case allOtherMessages = ".m.rule.message" case encrypted = ".m.rule.encrypted" case keywords = "_keywords" + // poll started event + case pollStart = ".m.rule.poll_start" + case msc3930pollStart = ".org.matrix.msc3930.rule.poll_start" + // poll started event (one to one) + case oneToOnePollStart = ".m.rule.poll_start_one_to_one" + case msc3930oneToOnePollStart = ".org.matrix.msc3930.rule.poll_start_one_to_one" + // poll ended event + case pollEnd = ".m.rule.poll_end" + case msc3930pollEnd = ".org.matrix.msc3930.rule.poll_end" + // poll ended event (one to one) + case oneToOnePollEnd = ".m.rule.poll_end_one_to_one" + case msc3930oneToOnePollEnd = ".org.matrix.msc3930.rule.poll_end_one_to_one" } extension NotificationPushRuleId: Identifiable { @@ -65,6 +77,20 @@ extension NotificationPushRuleId { return VectorL10n.settingsEncryptedGroupMessages case .keywords: return VectorL10n.settingsMessagesContainingKeywords + case .pollStart, .msc3930pollStart, .oneToOnePollStart, .msc3930oneToOnePollStart, .pollEnd, .msc3930pollEnd, .oneToOnePollEnd, .msc3930oneToOnePollEnd: + // They don't need to be rendered on the UI + return "" + } + } + + var syncedRules: [NotificationPushRuleId] { + switch self { + case .oneToOneRoom: + return [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + case .allOtherMessages: + return [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] + default: + return [] } } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift index 1f98242c7..14ed88e69 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift @@ -19,5 +19,13 @@ import Foundation protocol NotificationPushRuleType { var ruleId: String! { get } var enabled: Bool { get } + var ruleActions: NotificationActions? { get } + func matches(standardActions: NotificationStandardActions?) -> Bool } + +extension NotificationPushRuleType { + var pushRuleId: NotificationPushRuleId? { + ruleId.flatMap(NotificationPushRuleId.init(rawValue:)) + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 375b50ab9..9bf01ef4c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -44,7 +44,9 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { // Observe future updates to content rules rulesUpdated - .compactMap { _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] } + .compactMap { [weak self] _ in + self?.session.notificationCenter.rules.global.content as? [MXPushRule] + } .assign(to: &$contentRules) // Set initial value of rules @@ -53,14 +55,15 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { } // Observe future updates to rules rulesUpdated - .compactMap { _ in self.session.notificationCenter.flatRules as? [MXPushRule] } + .compactMap { [weak self] _ in + self?.session.notificationCenter.flatRules as? [MXPushRule] + } .assign(to: &$rules) } func add(keyword: String, enabled: Bool) { let index = NotificationIndex.index(when: enabled) - guard let actions = NotificationPushRuleId.keywords.standardActions(for: index)?.actions - else { + guard let actions = NotificationPushRuleId.keywords.standardActions(for: index).actions else { return } session.notificationCenter.addContentRuleWithRuleId(matchingPattern: keyword, notify: actions.notify, sound: actions.sound, highlight: actions.highlight) @@ -71,16 +74,52 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { session.notificationCenter.removeRule(rule) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { - guard let rule = session.notificationCenter.rule(byId: ruleId) else { return } - session.notificationCenter.enableRule(rule, isEnabled: enabled) + func updatePushRuleActions(for ruleId: String, + enabled: Bool, + actions: NotificationActions?) async throws { - if let actions = actions { - session.notificationCenter.updatePushRuleActions(ruleId, - kind: rule.kind, - notify: actions.notify, - soundName: actions.sound, - highlight: actions.highlight) + guard let rule = session.notificationCenter.rule(byId: ruleId) else { + return + } + + guard let actions = actions else { + try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled) + return + } + + // Updating the actions before enabling the rule allows the homeserver to triggers just one sync update + try await session.notificationCenter.updatePushRuleActions(ruleId, + kind: rule.kind, + notify: actions.notify, + soundName: actions.sound, + highlight: actions.highlight) + + try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled) + } +} + +private extension MXNotificationCenter { + func enableRule(pushRule: MXPushRule, isEnabled: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + enableRule(pushRule, isEnabled: isEnabled) { error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume() + } + } + } + } + + func updatePushRuleActions(ruleId: String, kind: __MXPushRuleKind, notify: Bool, soundName: String, highlight: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + updatePushRuleActions(ruleId, kind: kind, notify: notify, soundName: soundName, highlight: highlight) { error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume() + } + } } } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 44a553f6c..0bff31370 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -44,5 +44,11 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws { + guard let ruleIndex = rules.firstIndex(where: { $0.ruleId == ruleId }) else { + return + } + + rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, enabled: enabled, ruleActions: actions) + } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index a5a1671e3..5b06dfb6d 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -40,5 +40,5 @@ protocol NotificationSettingsServiceType { /// - ruleId: The id of the rule. /// - enabled: Whether the rule should be enabled or disabled. /// - actions: The actions to update with. - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift new file mode 100644 index 000000000..95b5e08fa --- /dev/null +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -0,0 +1,140 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import RiotSwiftUI +import XCTest + +final class NotificationSettingsViewModelTests: XCTestCase { + private var viewModel: NotificationSettingsViewModel! + private var notificationService: MockNotificationSettingsService! + + override func setUpWithError() throws { + notificationService = .init() + } + + func testAllTheRulesAreChecked() throws { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 4) + XCTAssertTrue(viewModel.viewState.selectionState.values.allSatisfy { $0 }) + } + + func testUpdateRule() async { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default) + notificationService.rules = [MockNotificationPushRule].default + + await viewModel.update(ruleID: .encrypted, isChecked: false) + XCTAssertEqual(viewModel.viewState.selectionState.count, 4) + XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false) + } + + func testUpdateOneToOneRuleAlsoUpdatesPollRules() async { + setupWithPollRules() + + await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], false) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], false) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], false) + + // unrelated poll rules stay the same + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) + XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], true) + XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], true) + } + + func testUpdateMessageRuleAlsoUpdatesPollRules() async { + setupWithPollRules() + + await viewModel.update(ruleID: .allOtherMessages, isChecked: false) + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], false) + XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], false) + XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], false) + + // unrelated poll rules stay the same + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], true) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], true) + } + + func testMismatchingRulesAreHandled() async { + setupWithPollRules() + + await viewModel.update(ruleID: .allOtherMessages, isChecked: false) + + // simulating a "mismatch" on the poll started rule + await viewModel.update(ruleID: .pollStart, isChecked: true) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + + // The other messages rule ui flag should match the loudest related poll rule + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) + } + + func testMismatchingOneToOneRulesAreHandled() async { + setupWithPollRules() + + await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + // simulating a "mismatch" on the one to one poll started rule + await viewModel.update(ruleID: .oneToOnePollStart, isChecked: true) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + + // The one to one room rule ui flag should match the loudest related poll rule + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) + + // the oneToOneRoom rule should be flagged as "out of sync" + XCTAssertTrue(viewModel.isRuleOutOfSync(.oneToOneRoom)) + XCTAssertFalse(viewModel.isRuleOutOfSync(.allOtherMessages)) + } +} + +private extension NotificationSettingsViewModelTests { + func setupWithPollRules() { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default + .polls) + notificationService.rules = [MockNotificationPushRule].default + [MockNotificationPushRule].polls + } +} + +private extension Array where Element == NotificationPushRuleId { + static var `default`: [NotificationPushRuleId] { + [.oneToOneRoom, .allOtherMessages, .oneToOneEncryptedRoom, .encrypted] + } + + static var polls: [NotificationPushRuleId] { + [.pollStart, .pollEnd, .oneToOnePollStart, .oneToOnePollEnd] + } +} + +private extension Array where Element == MockNotificationPushRule { + static var `default`: [MockNotificationPushRule] { + [NotificationPushRuleId] + .default + .map { ruleId in + MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true) + } + } + + static var polls: [MockNotificationPushRule] { + [NotificationPushRuleId] + .polls + .map { ruleId in + MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true) + } + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift index ac1316bfd..6139eb72b 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift @@ -21,8 +21,7 @@ struct MentionsAndKeywordNotificationSettings: View { var keywordSection: some View { SwiftUI.Section( - header: FormSectionHeader(text: VectorL10n.settingsYourKeywords), - footer: FormSectionFooter(text: VectorL10n.settingsMentionsAndKeywordsEncryptionNotice) + header: FormSectionHeader(text: VectorL10n.settingsYourKeywords) ) { NotificationSettingsKeywords(viewModel: viewModel) } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 18be2680d..37367e101 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -21,6 +21,7 @@ import SwiftUI /// Also renders an optional bottom section. /// Used in the case of keywords, for the keyword chips and input. struct NotificationSettings: View { + @Environment(\.theme) var theme: ThemeSwiftUI @ObservedObject var viewModel: NotificationSettingsViewModel var bottomSection: BottomSection? @@ -28,18 +29,31 @@ struct NotificationSettings: View { var body: some View { VectorForm { SwiftUI.Section( - header: FormSectionHeader(text: VectorL10n.settingsNotifyMeFor) + header: FormSectionHeader(text: BWIL10n.settingsNotifyMeFor) ) { ForEach(viewModel.viewState.ruleIds) { ruleId in - let checked = viewModel.viewState.selectionState[ruleId] ?? false - FormPickerItem(title: ruleId.title, selected: checked) { - viewModel.update(ruleID: ruleId, isChecked: !checked) + VStack(alignment: .leading, spacing: 4) { + let checked = viewModel.viewState.selectionState[ruleId] ?? false + FormPickerItem(title: ruleId.title, selected: checked) { + Task { + await viewModel.update(ruleID: ruleId, isChecked: !checked) + } + } + + if viewModel.isRuleOutOfSync(ruleId) { + Text(VectorL10n.settingsPushRulesError) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + .padding(.horizontal) + .padding(.bottom, 16) + } } } } bottomSection } .activityIndicator(show: viewModel.viewState.saving) + .disabled(viewModel.viewState.saving) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 588597572..154f926ce 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -49,7 +49,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // Observe when the rules are updated, to subsequently update the state of the settings. notificationSettingsService.rulesPublisher - .sink(receiveValue: rulesUpdated(newRules:)) + .sink { [weak self] newRules in + self?.rulesUpdated(newRules: newRules) + } .store(in: &cancellables) // Only observe keywords if the current settings view displays it. @@ -88,7 +90,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // Keyword rules were updates, check if we need to update the setting. keywordsRules .map { $0.contains { $0.enabled } } - .sink(receiveValue: keywordRuleUpdated(anyEnabled:)) + .sink { [weak self] in + self?.keywordRuleUpdated(anyEnabled: $0) + } .store(in: &cancellables) // Update the viewState with the final keywords to be displayed. @@ -105,35 +109,27 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // MARK: - Public - func update(ruleID: NotificationPushRuleId, isChecked: Bool) { + @MainActor + func update(ruleID: NotificationPushRuleId, isChecked: Bool) async { let index = NotificationIndex.index(when: isChecked) - if ruleID == .keywords { - // Keywords is handled differently to other settings - updateKeywords(isChecked: isChecked) - return - } - // Get the static definition and update the actions and enabled state. - guard let standardActions = ruleID.standardActions(for: index) else { return } + let standardActions = ruleID.standardActions(for: index) let enabled = standardActions != .disabled - notificationSettingsService.updatePushRuleActions( - for: ruleID.rawValue, - enabled: enabled, - actions: standardActions.actions - ) - } - - private func updateKeywords(isChecked: Bool) { - guard !keywordsOrdered.isEmpty else { - viewState.selectionState[.keywords]?.toggle() - return - } - // Get the static definition and update the actions and enabled state for every keyword. - let index = NotificationIndex.index(when: isChecked) - guard let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) else { return } - let enabled = standardActions != .disabled - keywordsOrdered.forEach { keyword in - notificationSettingsService.updatePushRuleActions( - for: keyword, + + switch ruleID { + case .keywords: // Keywords is handled differently to other settings + await updateKeywords(isChecked: isChecked) + + case .oneToOneRoom, .allOtherMessages: + await updatePushAction( + id: ruleID, + enabled: enabled, + standardActions: standardActions, + then: ruleID.syncedRules + ) + + default: + try? await notificationSettingsService.updatePushRuleActions( + for: ruleID.rawValue, enabled: enabled, actions: standardActions.actions ) @@ -152,17 +148,94 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob notificationSettingsService.remove(keyword: keyword) } - // MARK: - Private + func isRuleOutOfSync(_ ruleId: NotificationPushRuleId) -> Bool { + viewState.outOfSyncRules.contains(ruleId) && viewState.saving == false + } +} - private func rulesUpdated(newRules: [NotificationPushRuleType]) { - for rule in newRules { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), - ruleIds.contains(ruleId) else { continue } - viewState.selectionState[ruleId] = isChecked(rule: rule) +// MARK: - Private + +private extension NotificationSettingsViewModel { + @MainActor + func updateKeywords(isChecked: Bool) async { + guard !keywordsOrdered.isEmpty else { + viewState.selectionState[.keywords]?.toggle() + return + } + + // Get the static definition and update the actions and enabled state for every keyword. + let index = NotificationIndex.index(when: isChecked) + let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) + let enabled = standardActions != .disabled + let keywordsToUpdate = keywordsOrdered + + await withThrowingTaskGroup(of: Void.self) { group in + for keyword in keywordsToUpdate { + group.addTask { + try await self.notificationSettingsService.updatePushRuleActions( + for: keyword, + enabled: enabled, + actions: standardActions.actions + ) + } + } + } + } + + func updatePushAction(id: NotificationPushRuleId, + enabled: Bool, + standardActions: NotificationStandardActions, + then rules: [NotificationPushRuleId]) async { + await MainActor.run { + viewState.saving = true + } + + do { + // update the 'parent rule' first + try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions) + + // synchronize all the 'children rules' with the parent rule + await withThrowingTaskGroup(of: Void.self) { group in + for ruleId in rules { + group.addTask { + try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions) + } + } + } + await completeUpdate() + } catch { + await completeUpdate() } } - private func keywordRuleUpdated(anyEnabled: Bool) { + @MainActor + func completeUpdate() { + viewState.saving = false + } + + func rulesUpdated(newRules: [NotificationPushRuleType]) { + var outOfSyncRules: Set = .init() + + for rule in newRules { + guard + let ruleId = rule.pushRuleId, + ruleIds.contains(ruleId) + else { + continue + } + + let relatedSyncedRules = ruleId.syncedRules(in: newRules) + viewState.selectionState[ruleId] = isChecked(rule: rule, syncedRules: relatedSyncedRules) + + if isOutOfSync(rule: rule, syncedRules: relatedSyncedRules) { + outOfSyncRules.insert(ruleId) + } + } + + viewState.outOfSyncRules = outOfSyncRules + } + + func keywordRuleUpdated(anyEnabled: Bool) { if !keywordsOrdered.isEmpty { viewState.selectionState[.keywords] = anyEnabled } @@ -174,8 +247,10 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob /// The same logic is used on android. /// - Parameter rule: The push rule type to check. /// - Returns: Wether it should be displayed as checked or not checked. - private func isChecked(rule: NotificationPushRuleType) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } + func defaultIsChecked(rule: NotificationPushRuleType) -> Bool { + guard let ruleId = rule.pushRuleId else { + return false + } let firstIndex = NotificationIndex.allCases.first { nextIndex in rule.matches(standardActions: ruleId.standardActions(for: nextIndex)) @@ -187,4 +262,45 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob return index.enabled } + + func isChecked(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { + guard let ruleId = rule.pushRuleId else { + return false + } + + switch ruleId { + case .oneToOneRoom, .allOtherMessages: + let ruleIsChecked = defaultIsChecked(rule: rule) + let someSyncedRuleIsChecked = syncedRules.contains(where: { defaultIsChecked(rule: $0) }) + // The "loudest" rule will be applied when there is a clash between a rule and its dependent rules. + return ruleIsChecked || someSyncedRuleIsChecked + default: + return defaultIsChecked(rule: rule) + } + } + + func isOutOfSync(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { + guard let ruleId = rule.pushRuleId else { + return false + } + + switch ruleId { + case .oneToOneRoom, .allOtherMessages: + let ruleIsChecked = defaultIsChecked(rule: rule) + return syncedRules.contains(where: { defaultIsChecked(rule: $0) != ruleIsChecked }) + default: + return false + } + } +} + +extension NotificationPushRuleId { + func syncedRules(in rules: [NotificationPushRuleType]) -> [NotificationPushRuleType] { + rules.filter { + guard let ruleId = $0.pushRuleId else { + return false + } + return syncedRules.contains(ruleId) + } + } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift index 22bb4fed8..a732f56b1 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift @@ -22,5 +22,6 @@ struct NotificationSettingsViewState { var saving: Bool var ruleIds: [NotificationPushRuleId] var selectionState: [NotificationPushRuleId: Bool] + var outOfSyncRules: Set = .init() var keywords = [String]() } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift index 467c69eb9..0f3155c9f 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift @@ -70,11 +70,12 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable { let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail, selectionHeader: parameters.selectionHeader) matrixItemChooserViewModel = viewModel if let viewProvider = parameters.viewProvider { - let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + let view = viewProvider.view(with: viewModel.context) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) matrixItemChooserHostingController = VectorHostingController(rootView: view) } else { let view = MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) matrixItemChooserHostingController = VectorHostingController(rootView: view) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index e18476c24..b2db1b340 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -61,7 +61,7 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift index 292ad2ee0..fca75dd38 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift @@ -27,7 +27,7 @@ extension MatrixListItemData { if parentSpaceIds.isEmpty { detailText = nil } else { - if let spaceName = spaceService.getSpace(withId: parentSpaceIds.first ?? "")?.summary?.displayname { + if let spaceName = spaceService.getSpace(withId: parentSpaceIds.first ?? "")?.summary?.displayName { let count = parentSpaceIds.count - 1 switch count { case 0: @@ -51,6 +51,6 @@ extension MatrixListItemData { } else { type = .room } - self.init(id: mxRoom.roomId, type: type, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname, detailText: detailText) + self.init(id: mxRoom.roomId, type: type, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayName, detailText: detailText) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift index 1d67f2a72..883fd6e8e 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift @@ -70,6 +70,6 @@ struct MatrixItemChooserListRow: View { struct MatrixItemChooserListRow_Previews: PreviewProvider { static var previews: some View { TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift index 318fd549c..8aae95f00 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift @@ -47,7 +47,7 @@ final class SpaceCreationCoordinator: Coordinator { init(parameters: SpaceCreationCoordinatorParameters) { let title: String let message: String - if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayname { + if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayName { title = VectorL10n.spacesSubspaceCreationVisibilityTitle message = VectorL10n.spacesSubspaceCreationVisibilityMessage(parentSpaceName) } else { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift index 28792056f..d7a31ede5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift @@ -42,7 +42,7 @@ final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable { let service = SpaceCreationEmailInvitesService(session: parameters.session) let viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: parameters.creationParams, service: service) let view = SpaceCreationEmailInvites(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationEmailInvitesViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift index 0b70a8746..add3eb6ed 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift @@ -64,7 +64,7 @@ enum MockSpaceCreationEmailInvitesScreenState: MockScreenState, CaseIterable { return ( [viewModel], AnyView(SpaceCreationEmailInvites(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift index 8d590936d..e3bcd6c9c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationMenuCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationMenuViewModel(navTitle: parameters.navTitle, creationParams: parameters.creationParams, title: parameters.title, detail: parameters.detail, options: parameters.options) let view = SpaceCreationMenu(viewModel: viewModel.context, showBackButton: parameters.showBackButton) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationMenuViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift index a1838458d..e385e30cb 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParams)) let view = SpaceCreationPostProcess(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationPostProcessViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift index 27c003b71..f3c18f543 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift @@ -54,7 +54,7 @@ enum MockSpaceCreationPostProcessScreenState: MockScreenState { return ( [service, viewModel], AnyView(SpaceCreationPostProcess(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift index 2e94feec2..3e7ef4b4e 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationRoomsCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationRoomsViewModel(creationParameters: parameters.creationParams) let view = SpaceCreationRooms(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationRoomsViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift index 3ef06a64f..f23e463e2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift @@ -55,7 +55,7 @@ enum MockSpaceCreationRoomsScreenState: MockScreenState, CaseIterable { return ( [viewModel], AnyView(SpaceCreationRooms(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift index d525297c6..9e8b69db3 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift @@ -48,7 +48,7 @@ final class SpaceCreationSettingsCoordinator: Coordinator, Presentable { let service = SpaceCreationSettingsService(roomName: parameters.creationParameters.name ?? "", userDefinedAddress: parameters.creationParameters.userDefinedAddress, session: parameters.session) let viewModel = SpaceCreationSettingsViewModel(spaceCreationSettingsService: service, creationParameters: parameters.creationParameters) let view = SpaceCreationSettings(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationSettingsViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift index 8738289a9..2942a43b4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift @@ -59,7 +59,7 @@ enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(SpaceCreationSettings(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift index 771ad0b8f..530f33eb9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift @@ -62,7 +62,7 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable { let service = SpaceSelectorService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, showHomeSpace: parameters.showHomeSpace, selectedSpaceId: parameters.selectedSpaceId) let viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: parameters.showCancel) let view = SpaceSelector(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) self.viewModel = viewModel let hostingViewController = VectorHostingController(rootView: view) hostingViewController.hidesBackTitleWhenPushed = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift index 4db35bf69..3339055c9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift @@ -77,7 +77,7 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { return nil } - return summary.displayname + return summary.displayName } // MARK: Public @@ -117,7 +117,7 @@ private extension SpaceSelectorListItemData { return SpaceSelectorListItemData(id: summary.roomId, avatar: summary.room.avatarData, - displayName: summary.displayname, + displayName: summary.displayName, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0, hasSubItems: !space.childSpaces.isEmpty, diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift index f5b69b6a5..5ac5ea3e5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift @@ -48,7 +48,8 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: SpaceSettingsService(session: parameters.session, spaceId: parameters.spaceId)) let view = SpaceSettings(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) + spaceSettingsViewModel = viewModel let controller = VectorHostingController(rootView: view) controller.enableNavigationBarScrollEdgeAppearance = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift index f18482638..342cf520f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift @@ -79,7 +79,7 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(SpaceSettings(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift index 5db4451ec..ffb4af8ac 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift @@ -50,7 +50,7 @@ enum MockTemplateSimpleScreenScreenState: MockScreenState, CaseIterable { return ( [promptType, viewModel], AnyView(TemplateSimpleScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index b3098629b..73dc2d987 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -37,7 +37,7 @@ final class TemplateUserProfileCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) let view = TemplateUserProfile(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) templateUserProfileViewModel = viewModel templateUserProfileHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift index 08082aec5..27e9d215b 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift @@ -55,7 +55,7 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateUserProfile(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift index a87e06605..f8e9bfe28 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift @@ -46,6 +46,6 @@ struct TemplateUserProfileHeader: View { struct TemplateUserProfileHeader_Previews: PreviewProvider { static var previews: some View { TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index bca98511f..afe669ec8 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -33,7 +33,8 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room)) let view = TemplateRoomChat(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.room.mxSession.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.room.mxSession.mediaManager))) + templateRoomChatViewModel = viewModel templateRoomChatHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift index 8ec8bced0..f9a74abae 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift @@ -56,7 +56,7 @@ enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateRoomChat(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index 364aa98f8..27f214bdd 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -28,7 +28,7 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { private(set) var roomInitializationStatus: CurrentValueSubject var roomName: String? { - room.summary.displayname + room.summary.displayName } init(room: MXRoom) { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift index 2e755636a..7eebeefaa 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift @@ -36,7 +36,11 @@ struct TemplateRoomChat: View { Button(action: { viewModel.send(viewAction: .sendMessage) }, label: { - Image(uiImage: Asset.Images.sendIcon.image) + if BWIBuildSettings.shared.bwiEnableBuMUI { + Image(uiImage: Asset.Images.sendIconBum.image) + } else { + Image(uiImage: Asset.Images.sendIcon.image) + } }) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift index 70495bd25..24df8b7e0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift @@ -58,6 +58,6 @@ struct TemplateRoomChatBubbleView_Previews: PreviewProvider { ) static var previews: some View { TemplateRoomChatBubbleView(bubble: bubble) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift index 26bbbbb99..263e878c0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift @@ -33,7 +33,7 @@ final class TemplateRoomListCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateRoomListViewModel(templateRoomListService: TemplateRoomListService(session: parameters.session)) let view = TemplateRoomList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) templateRoomListViewModel = viewModel templateRoomListHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift index 8b8c0e2a3..59ad3fc8e 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift @@ -47,7 +47,7 @@ enum MockTemplateRoomListScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateRoomList(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift index cd8b13649..dccb8b9f4 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift @@ -35,6 +35,6 @@ class TemplateRoomListService: TemplateRoomListServiceProtocol { private extension TemplateRoomListRoom { init(mxRoom: MXRoom) { - self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname) + self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayName) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift index a8db7b8ba..06841bbb2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift @@ -42,6 +42,6 @@ struct TemplateRoomListRow: View { struct TemplateRoomListRow_Previews: PreviewProvider { static var previews: some View { TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift index 331ede0c3..27950a74a 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift @@ -35,9 +35,12 @@ struct UserOtherSessionsToolbar: ToolbarContent { private func navigationBarLeading() -> some ToolbarContent { ToolbarItemGroup(placement: .navigationBarLeading) { if isEditModeEnabled { - Button(allItemsSelected ? VectorL10n.deselectAll : VectorL10n.selectAll, action: { + Button { onToggleSelection() - }) + } label: { + Text(allItemsSelected ? VectorL10n.deselectAll : VectorL10n.selectAll) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } } } } @@ -55,11 +58,13 @@ struct UserOtherSessionsToolbar: ToolbarContent { } private func cancelButton() -> some View { - Button(VectorL10n.cancel) { + Button { isEditModeEnabled = false + } label: { + Text(VectorL10n.cancel) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.accent) } private func filterMenuButton() -> some View { @@ -73,6 +78,7 @@ struct UserOtherSessionsToolbar: ToolbarContent { .labelsHidden() } label: { Image(filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } .accessibilityLabel(VectorL10n.userOtherSessionFilter) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift index 8f155814f..3e3c9ee0c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift @@ -94,7 +94,7 @@ struct UserSessionOverview: View { .accessibilityIdentifier(VectorL10n.signOut) } label: { Image(systemName: "ellipsis") - .foregroundColor(theme.colors.accent) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) .padding(.horizontal, 4) .padding(.vertical, 12) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift index 175a7d9a0..e09586b83 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift @@ -56,7 +56,7 @@ enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(UserSessionsOverview(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 71c1433df..99d1e3927 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -34,6 +34,7 @@ targets: platform: iOS dependencies: - target: DesignKit + - package: AnalyticsEvents - package: Mapbox - package: WysiwygComposer sources: diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 3cd441aa1..3895aefff 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: RiotSwiftUI + - package: AnalyticsEvents - package: WysiwygComposer settings: diff --git a/RiotSwiftUI/targetUnitTests.yml b/RiotSwiftUI/targetUnitTests.yml index 6362bf447..ec16e078a 100644 --- a/RiotSwiftUI/targetUnitTests.yml +++ b/RiotSwiftUI/targetUnitTests.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: RiotSwiftUI + - package: AnalyticsEvents configFiles: Debug: Debug.xcconfig diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift index 0fda6af36..33ed28389 100644 --- a/RiotTests/AnalyticsTests.swift +++ b/RiotTests/AnalyticsTests.swift @@ -78,7 +78,7 @@ class AnalyticsTests: XCTestCase { XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.") // When updating the user properties - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5)) // Then the properties should be cached XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -90,7 +90,7 @@ class AnalyticsTests: XCTestCase { func testMergingUserProperties() { // Given a client with a cached use case user properties let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") @@ -98,7 +98,7 @@ class AnalyticsTests: XCTestCase { XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.") // When updating the number of spaces - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -107,7 +107,7 @@ class AnalyticsTests: XCTestCase { XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.") // When updating the number of spaces - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: .Favourites)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -120,7 +120,7 @@ class AnalyticsTests: XCTestCase { func testSendingUserProperties() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -136,7 +136,7 @@ class AnalyticsTests: XCTestCase { func testSendingUserPropertiesWithIdentify() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") diff --git a/RiotTests/Experiments/CryptoSDKFeatureTests.swift b/RiotTests/Experiments/CryptoSDKFeatureTests.swift new file mode 100644 index 000000000..ffcf04521 --- /dev/null +++ b/RiotTests/Experiments/CryptoSDKFeatureTests.swift @@ -0,0 +1,79 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import Element + +class CryptoSDKFeatureTests: XCTestCase { + class RemoteFeatureClient: RemoteFeaturesClientProtocol { + var isEnabled = false + func isFeatureEnabled(_ feature: String) -> Bool { + isEnabled + } + } + + var remote: RemoteFeatureClient! + var feature: CryptoSDKFeature! + + override func setUp() { + RiotSettings.shared.enableCryptoSDK = false + remote = RemoteFeatureClient() + feature = CryptoSDKFeature(remoteFeature: remote) + } + + override func tearDown() { + RiotSettings.shared.enableCryptoSDK = false + } + + func test_disabledByDefault() { + XCTAssertFalse(feature.isEnabled) + } + + func test_enable() { + feature.enable() + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() { + feature.enable() + remote.isEnabled = false + + feature.enableIfAvailable(forUserId: "alice") + + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() { + remote.isEnabled = false + feature.enableIfAvailable(forUserId: "alice") + XCTAssertFalse(feature.isEnabled) + } + + func test_canManuallyEnable() { + remote.isEnabled = false + XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice")) + + remote.isEnabled = true + XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice")) + } + + func test_reset() { + feature.enable() + feature.reset() + XCTAssertFalse(RiotSettings.shared.enableCryptoSDK) + } +} diff --git a/RiotTests/Experiments/ExperimentTests.swift b/RiotTests/Experiments/ExperimentTests.swift new file mode 100644 index 000000000..ea0b8af2b --- /dev/null +++ b/RiotTests/Experiments/ExperimentTests.swift @@ -0,0 +1,67 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import Element + +class ExperimentTests: XCTestCase { + + private func randomUserId() -> String { + return "user_" + UUID().uuidString.prefix(6) + } + + func test_singleVariant() { + let experiment = Experiment(name: "single", variants: 1) + for _ in 0 ..< 1000 { + let variant = experiment.variant(userId: randomUserId()) + XCTAssertEqual(variant, 0) + } + } + + func test_twoVariants() { + let experiment = Experiment(name: "two", variants: 2) + + var variants = Set() + for _ in 0 ..< 1000 { + let variant = experiment.variant(userId: randomUserId()) + variants.insert(variant) + } + + // We perform the test by collecting all assigned variants for 1000 users + // and ensuring we only encounter variants 0 and 1 + XCTAssertEqual(variants.count, 2) + XCTAssertTrue(variants.contains(0)) + XCTAssertTrue(variants.contains(1)) + XCTAssertFalse(variants.contains(2)) + } + + func test_manyVariants() { + let experiment = Experiment(name: "many", variants: 5) + + var variants = Set() + for _ in 0 ..< 10000 { + let variant = experiment.variant(userId: randomUserId()) + variants.insert(variant) + } + + // We perform the test by collecting all assigned variants for 10000 users + // and ensuring we only encounter variants between 0 and 4 + XCTAssertTrue(variants.count >= 2 && variants.count <= 5) + XCTAssertTrue(variants.isSubset(of: .init([0, 1, 2, 3, 4]))) + XCTAssertFalse(variants.contains(5)) + } +} diff --git a/RiotTests/Experiments/PhasedRolloutFeatureTests.swift b/RiotTests/Experiments/PhasedRolloutFeatureTests.swift new file mode 100644 index 000000000..3c50078d3 --- /dev/null +++ b/RiotTests/Experiments/PhasedRolloutFeatureTests.swift @@ -0,0 +1,55 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import Element + +class PhasedRolloutFeatureTests: XCTestCase { + + private func randomUserId() -> String { + return "user_" + UUID().uuidString.prefix(6) + } + + func test_allDisabled() { + let feature = PhasedRolloutFeature(name: "disabled", targetPercentage: 0) + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + XCTAssertFalse(isEnabled) + } + } + + func test_allEnabled() { + let feature = PhasedRolloutFeature(name: "enabled", targetPercentage: 1) + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + XCTAssertTrue(isEnabled) + } + } + + func test_someEnabled() { + let feature = PhasedRolloutFeature(name: "some", targetPercentage: 0.5) + var statuses = Set() + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + statuses.insert(isEnabled) + } + + // We test by checking that we encountered both enabled and disabled cases + XCTAssertTrue(statuses.contains(true)) + XCTAssertTrue(statuses.contains(false)) + } +} diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift similarity index 73% rename from RiotTests/MatrixKitTests/MXKEventFormatterTests.swift rename to RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift index 31db30954..457f10853 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift +++ b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift @@ -29,22 +29,16 @@ private enum Constants { static let expectedEditedHTML = "
In reply to alice
Edited message
Reply" static let expectedEditedHTMLWithNewContent = "
In reply to alice
New content
Reply" static let expectedEditedHTMLWithParsedItalic = "
In reply to alice
New content
Reply" + static let expectedReplyToPollEndedEvent = "
In reply to alice
Ended poll
Reply" } -class MXKEventFormatterTests: XCTestCase { +class MXKEventFormatterSwiftTests: XCTestCase { func testBuildHTMLString() { let formatter = MXKEventFormatter() - let repliedEvent = MXEvent() + let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringRoomMessage) let event = MXEvent() func buildHTML() -> String? { return formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) } - // Initial setup. - repliedEvent.sender = "alice" - repliedEvent.roomId = Constants.roomId - repliedEvent.eventId = Constants.repliedEventId - repliedEvent.wireType = kMXEventTypeStringRoomMessage - repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText, - kMXMessageBodyKey: Constants.repliedEventBody] event.sender = "bob" event.wireType = kMXEventTypeStringRoomMessage event.wireContent = [ @@ -73,4 +67,39 @@ class MXKEventFormatterTests: XCTestCase { repliedEvent.wireContent[kMXMessageContentKeyNewContent] = nil XCTAssertNil(buildHTML()) } + + func testBuildHTMLStringWithPollEndedReply() { + let formatter = MXKEventFormatter() + let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringPollEnd, body: nil) + + let event = MXEvent() + event.sender = "bob" + event.wireType = kMXEventTypeStringRoomMessage + event.wireContent = [ + kMXMessageTypeKey: kMXMessageTypeText, + kMXMessageBodyKey: Constants.replyBody, + kMXEventRelationRelatesToKey: [kMXEventContentRelatesToKeyInReplyTo: ["event_id": Constants.repliedEventId]] + ] + + let formattedText = formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) + + XCTAssertEqual(formattedText, Constants.expectedReplyToPollEndedEvent) + } +} + +private extension MXEvent { + static func mockEvent(eventType: String, body: String? = Constants.repliedEventBody) -> MXEvent { + let repliedEvent = MXEvent() + repliedEvent.sender = "alice" + repliedEvent.roomId = Constants.roomId + repliedEvent.eventId = Constants.repliedEventId + repliedEvent.wireType = eventType + repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText] + + if let body = body { + repliedEvent.wireContent[kMXMessageBodyKey] = body + } + + return repliedEvent + } } diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index fbe801665..b21f57715 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -414,14 +414,16 @@ NSString *s = @"Matrix HQ room is at https://matrix.to/#/room/#matrix:matrix.org."; NSAttributedString *as = [eventFormatter renderString:s forEvent:anEvent]; - __block NSUInteger ranges = 0; + __block BOOL hasLink = false; [as enumerateAttributesInRange:NSMakeRange(0, as.length) options:(0) usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { - - ranges++; + if (attrs[NSLinkAttributeName]) { + hasLink = true; + *stop = true; + } }]; - XCTAssertEqual(ranges, 1, @"There should be no link in this case. We let the UI manage the link"); + XCTAssertEqual(hasLink, false, @"There should be no link in this case. We let the UI manage the link"); } #pragma mark - Event sender/target info diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift new file mode 100644 index 000000000..1eec9dda5 --- /dev/null +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -0,0 +1,106 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest +@testable import Element + +final class PushRulesUpdaterTests: XCTestCase { + private var notificationService: MockNotificationSettingsService! + private var pushRulesUpdater: PushRulesUpdater! + + override func setUpWithError() throws { + notificationService = .init() + notificationService.rules = [MockNotificationPushRule].default + pushRulesUpdater = .init(notificationSettingsService: notificationService) + } + + func testNoRuleIsUpdated() async throws { + await pushRulesUpdater.syncRulesIfNeeded() + XCTAssertEqual(notificationService.rules as? [MockNotificationPushRule], [MockNotificationPushRule].default) + } + + func testSingleRuleAffected() async throws { + let targetActions: NotificationActions = .init(notify: true, sound: "default") + let targetRuleIndex = try mockRule(ruleId: .pollStart, enabled: false, actions: targetActions) + + await pushRulesUpdater.syncRulesIfNeeded() + + XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) + } + + func testAffectedRulesAreUpdated() async throws { + let targetActions: NotificationActions = .init(notify: true, sound: "abc") + try mockRule(ruleId: .allOtherMessages, enabled: true, actions: targetActions) + let affectedRules: [NotificationPushRuleId] = [.allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] + + await pushRulesUpdater.syncRulesIfNeeded() + + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } + } + + func testAffectedOneToOneRulesAreUpdated() async throws { + let targetActions: NotificationActions = .init(notify: true, sound: "abc") + try mockRule(ruleId: .oneToOneRoom, enabled: true, actions: targetActions) + let affectedRules: [NotificationPushRuleId] = [.oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + + await pushRulesUpdater.syncRulesIfNeeded() + + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } + } +} + +private extension PushRulesUpdaterTests { + @discardableResult + func mockRule(ruleId: NotificationPushRuleId, enabled: Bool, actions: NotificationActions) throws -> Int { + guard let ruleIndex = notificationService.rules.firstIndex(where: { $0.pushRuleId == ruleId }) else { + throw NSError(domain: "no ruleIndex found", code: 0) + } + notificationService.rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: enabled, ruleActions: actions) + return ruleIndex + } +} + +private extension Array where Element == MockNotificationPushRule { + static var `default`: [MockNotificationPushRule] { + let ids: [NotificationPushRuleId] = [.oneToOneRoom, .allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + + return ids.map { + MockNotificationPushRule(ruleId: $0.rawValue, enabled: true) + } + } +} diff --git a/RiotTests/RendezvousServiceTests.swift b/RiotTests/RendezvousServiceTests.swift index 36297f778..2ffe58f06 100644 --- a/RiotTests/RendezvousServiceTests.swift +++ b/RiotTests/RendezvousServiceTests.swift @@ -19,10 +19,10 @@ import XCTest @MainActor class RendezvousServiceTests: XCTestCase { - func testEnd2End() async { + func testEnd2EndV1() async { let mockTransport = MockRendezvousTransport() - let aliceService = RendezvousService(transport: mockTransport) + let aliceService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V1) guard case .success(let rendezvousDetails) = await aliceService.createRendezvous(), let alicePublicKey = rendezvousDetails.key else { @@ -32,7 +32,49 @@ class RendezvousServiceTests: XCTestCase { XCTAssertNotNil(mockTransport.rendezvousURL) - let bobService = RendezvousService(transport: mockTransport) + let bobService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V1) + + guard case .success = await bobService.joinRendezvous(withPublicKey: alicePublicKey) else { + XCTFail("Bob failed to join") + return + } + + guard case .success = await aliceService.waitForInterlocutor() else { + XCTFail("Alice failed to establish connection") + return + } + + guard let messageData = "Hello from alice".data(using: .utf8) else { + fatalError() + } + + guard case .success = await aliceService.send(data: messageData) else { + XCTFail("Alice failed to send message") + return + } + + guard case .success(let data) = await bobService.receive() else { + XCTFail("Bob failed to receive message") + return + } + + XCTAssertEqual(messageData, data) + } + + func testEnd2EndV2() async { + let mockTransport = MockRendezvousTransport() + + let aliceService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V2) + + guard case .success(let rendezvousDetails) = await aliceService.createRendezvous(), + let alicePublicKey = rendezvousDetails.key else { + XCTFail("Rendezvous creation failed") + return + } + + XCTAssertNotNil(mockTransport.rendezvousURL) + + let bobService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V2) guard case .success = await bobService.joinRendezvous(withPublicKey: alicePublicKey) else { XCTFail("Bob failed to join") diff --git a/RiotTests/target.yml b/RiotTests/target.yml index 849585240..ad4d778cc 100644 --- a/RiotTests/target.yml +++ b/RiotTests/target.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: Riot + - package: AnalyticsEvents configFiles: Debug: Debug.xcconfig diff --git a/SiriIntents/ContactResolver/ContactResolver.m b/SiriIntents/ContactResolver/ContactResolver.m index 42d121261..15fa233fa 100644 --- a/SiriIntents/ContactResolver/ContactResolver.m +++ b/SiriIntents/ContactResolver/ContactResolver.m @@ -122,7 +122,7 @@ INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; // For rooms we try to use room display name - NSString *displayName = summary.displayname ? summary.displayname : user.displayname; + NSString *displayName = summary.displayName ? summary.displayName : user.displayname; INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle nameComponents:nil diff --git a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h index ca6d81962..01aeb20d1 100644 --- a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h +++ b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h @@ -16,3 +16,4 @@ #import "MatrixKit-Bridging-Header.h" #import "BuildInfo.h" +#import "ThemeService.h" diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 9b8d31482..a2bd3c871 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -33,7 +33,9 @@ targets: dependencies: - sdk: Intents.framework + - package: AnalyticsEvents - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig @@ -44,6 +46,7 @@ targets: - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Categories/MXEvent.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Config/BuildSettings.swift - path: ../Config/BWIBuildSettings.swift - path: ../Config/BuM/BWIBuildSettings+BuM.swift @@ -68,3 +71,5 @@ targets: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK + - path: ../Riot/Managers/Theme + - path: ../Riot/Categories/UIColor.swift diff --git a/bwi/AppConfig/AppConfigService.swift b/bwi/AppConfig/AppConfigService.swift index a45e8caba..955c68e9d 100644 --- a/bwi/AppConfig/AppConfigService.swift +++ b/bwi/AppConfig/AppConfigService.swift @@ -158,10 +158,8 @@ extension UserDefaults return "https://" + url } else if let url = ServerURLHelper.shared.httpsPermalink() { return url - } else if let url = BuildSettings.clientPermalinkBaseUrl { - return url } else { - return nil + return BWIBuildSettings.shared.clientPermalinkBaseUrl } } diff --git a/bwi/CommonUI/ServerIcon.swift b/bwi/CommonUI/ServerIcon.swift index 5b72b0f38..3e188afe0 100644 --- a/bwi/CommonUI/ServerIcon.swift +++ b/bwi/CommonUI/ServerIcon.swift @@ -20,17 +20,22 @@ import SwiftUI struct ServerIcon: View { @Environment(\.theme) private var theme - let image: ImageAsset + let image: ImageAsset? let size: CGFloat var body: some View { - Image(image.name) + Image((image ?? getDefaultServerIcon()).name) .resizable() .frame(width: size, height: size) - .background(Color.white) + .background(BWIBuildSettings.shared.bwiLoginFlowLayout ? Color.clear : Color.white) .cornerRadius(20) .accessibilityHidden(true) } + + // get app specific ServerIcon + func getDefaultServerIcon() -> ImageAsset { + return BWIBuildSettings.shared.bwiLoginFlowLayout ? Asset.Images.launchScreenLogo : Asset.SharedImages.loginFlowLogo + } } struct ServerIcon_Previews: PreviewProvider { diff --git a/bwi/ContentScanner/UI/Status/ContentScannerStatus.swift b/bwi/ContentScanner/UI/Status/ContentScannerStatus.swift index 1f1f0ba7a..63bc8c2db 100644 --- a/bwi/ContentScanner/UI/Status/ContentScannerStatus.swift +++ b/bwi/ContentScanner/UI/Status/ContentScannerStatus.swift @@ -57,8 +57,8 @@ class ContentScannerStatus: ObservableObject, ContentScannerContentDelegate { self.scanStatusVisibility = (scanStatus == .infected) ? false : true - self.backgroundColor = (scanStatus == .infected) ? self.theme.textPrimaryColor : self.theme.baseColor - self.tintColor = (scanStatus != .infected) ? self.theme.backgroundColor : self.theme.textSecondaryColor + self.backgroundColor = (scanStatus == .infected) ? self.theme.warningColor : self.theme.baseColor + self.tintColor = (scanStatus != .infected) ? self.theme.backgroundColor : self.theme.warningColor self.statusColor = (scanStatus != .infected) ? self.theme.textPrimaryColor : self.theme.textSecondaryColor } } diff --git a/bwi/ContentScanner/UI/Status/ContentScannerStatusContentView.swift b/bwi/ContentScanner/UI/Status/ContentScannerStatusContentView.swift index 89290bc1f..dfa5fec02 100644 --- a/bwi/ContentScanner/UI/Status/ContentScannerStatusContentView.swift +++ b/bwi/ContentScanner/UI/Status/ContentScannerStatusContentView.swift @@ -29,9 +29,11 @@ struct ContentScannerStatusContentView: View { ProgressView() } else { Image(scanStatus.statusImagePath) + .foregroundColor(.white) } } .background(Color(scanStatus.backgroundColor)) + .cornerRadius(5) Text(scanStatus.fileName) .font(.system(size: 15)) diff --git a/bwi/DeveloperSettings/DeveloperSettingsView.swift b/bwi/DeveloperSettings/DeveloperSettingsView.swift index cb7827895..7fb41e534 100644 --- a/bwi/DeveloperSettings/DeveloperSettingsView.swift +++ b/bwi/DeveloperSettings/DeveloperSettingsView.swift @@ -34,50 +34,68 @@ struct DeveloperSettingsView: View { @State private var showAlert = false @State private var showAlertBirthdayCampaign = false + @State private var permalinkPrefix: String? = UserDefaults.standard.string(forKey: "bwi_permalink_prefix") var body: some View { - List { - Button(action: { showAlert = createNewPersonalNotesRoom(mxSession: session) }) { - Text(BWIL10n.bwiSettingsDeveloperCreateNewPersonalNotesRoom) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) - } - .alert(isPresented: $showAlert) { - Alert(title: Text(BWIL10n.bwiSettingsDeveloper), dismissButton: .default(Text("Ok"))) - } - - Button(action: { showAlert = resetMatomoInfoScreen() }) { - Text(BWIL10n.bwiSettingsDeveloperResetMatomoInfo) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) - } - .alert(isPresented: $showAlert) { - Alert(title: Text(BWIL10n.bwiSettingsDeveloperShowMatomoPrivacyNotesResetted), message: Text(BWIL10n.bwiSettingsDeveloperShowMatomoPrivacyNotesResetted), dismissButton: .default(Text("Ok"))) - } - Button(action: { showAlertBirthdayCampaign = resetBirthdayCampaignScreen(mxSession: session) }) { - Text(BWIL10n.bwiSettingsDeveloperSettingsResetBirthdayBanner) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) - } - .alert(isPresented: $showAlertBirthdayCampaign) { - Alert(title: Text(BWIL10n.bwiSettingsDeveloperSettingsBirthdayBannerResettetTitle), message: Text(BWIL10n.bwiSettingsDeveloperSettingsBirthdayBannerResettetMessage), dismissButton: .default(Text("Ok"))) - } - Button(action: { _ = restrictUser(mxSession: session) }) { - Text(BWIL10n.bwiSettingsDeveloperRestrictUser) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) - } - Button(action: { _ = unrestrictUser(mxSession: session) }) { + Form { + SwiftUI.Section { + Button(action: { showAlert = createNewPersonalNotesRoom(mxSession: session) }) { + Text(BWIL10n.bwiSettingsDeveloperCreateNewPersonalNotesRoom) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } + .alert(isPresented: $showAlert) { + Alert(title: Text(BWIL10n.bwiSettingsDeveloper), dismissButton: .default(Text("Ok"))) + } + + Button(action: { showAlert = resetMatomoInfoScreen(mxSession: session) }) { + Text(BWIL10n.bwiSettingsDeveloperResetMatomoInfo) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } + .alert(isPresented: $showAlert) { + Alert(title: Text(BWIL10n.bwiSettingsDeveloperShowMatomoPrivacyNotesResetted), message: Text(BWIL10n.bwiSettingsDeveloperShowMatomoPrivacyNotesResetted), dismissButton: .default(Text("Ok"))) + } + Button(action: { showAlertBirthdayCampaign = resetBirthdayCampaignScreen(mxSession: session) }) { + Text(BWIL10n.bwiSettingsDeveloperSettingsResetBirthdayBanner) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } + .alert(isPresented: $showAlertBirthdayCampaign) { + Alert(title: Text(BWIL10n.bwiSettingsDeveloperSettingsBirthdayBannerResettetTitle), message: Text(BWIL10n.bwiSettingsDeveloperSettingsBirthdayBannerResettetMessage), dismissButton: .default(Text("Ok"))) + } + Button(action: { _ = restrictUser(mxSession: session) }) { + Text(BWIL10n.bwiSettingsDeveloperRestrictUser) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } + Button(action: { _ = unrestrictUser(mxSession: session) }) { Text(BWIL10n.bwiSettingsDeveloperUnrestrictUser) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } + Button(action: { _ = unmarkBannerVersion(mxSession: session) }) { + Text(BWIL10n.bwiSettingsDeveloperUnmarkBanner) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(.system(size: 17)) + } } - Button(action: { _ = unmarkBannerVersion(mxSession: session) }) { - Text(BWIL10n.bwiSettingsDeveloperUnmarkBanner) - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) - .font(.system(size: 17)) + if BWIBuildSettings.shared.permalinkPrefixSettings && !BWIBuildSettings.shared.permalinkPrefixes.isEmpty { + SwiftUI.Section(header: Text(BWIL10n.settingsPermalinkPrefixPickerTitle)) { + Picker("", selection: $permalinkPrefix) { + ForEach(BWIBuildSettings.shared.permalinkPrefixes, id: \.self) { prefix in + Text(prefix) + .tag(String?.some(prefix)) + } + } + .id(UUID()) + .onChange(of: permalinkPrefix) { newValue in + UserDefaults.standard.set(newValue, forKey: "bwi_permalink_prefix") + } + } } } + .listStyle(.grouped) .navigationTitle(BWIL10n.bwiSettingsDeveloper) .navigationBarTitleDisplayMode(.inline) } @@ -104,8 +122,11 @@ fileprivate func createNewPersonalNotesRoom(mxSession: MXSession?) -> Bool { return true } -fileprivate func resetMatomoInfoScreen() -> Bool { - BWIAnalytics.sharedTracker.setPromtShown(false) +fileprivate func resetMatomoInfoScreen(mxSession: MXSession?) -> Bool { + guard let mxSession = mxSession else { + return false + } + BWIAnalyticsAccountDataService(mxSession: mxSession) return true } diff --git a/bwi/FeatureBanner/FeatureBannerView.swift b/bwi/FeatureBanner/FeatureBannerView.swift new file mode 100644 index 000000000..10e07818d --- /dev/null +++ b/bwi/FeatureBanner/FeatureBannerView.swift @@ -0,0 +1,173 @@ +// +/* + * Copyright (c) 2023 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import UIKit + + +protocol FeatureBannerDelegate { + func didPressClose() + func didPressShowDetails() +} + +@objcMembers class FeatureBannerViewCell: UITableViewCell, FeatureBannerDelegate { + private var parentViewController: UIViewController? + private let hostingController = UIHostingController(rootView: nil) + private var webViewController: WebViewViewController? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + hostingController.view.backgroundColor = UIColor.clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func setupView(parent: UIViewController, rootView: Content) { + self.parentViewController = parent + hostingController.rootView = rootView + + let shouldControllerMove = hostingController.parent != parent + if shouldControllerMove { + removeController() + parent.addChild(hostingController) + } + + if !self.contentView.subviews.contains(hostingController.view) { + self.contentView.addSubview(hostingController.view) + self.contentView.vc_addSubViewMatchingParentSafeArea(hostingController.view) + } + + if shouldControllerMove { + hostingController.didMove(toParent: parent) + } + } + + func calculateHeight() -> CGFloat { + hostingController.view.setNeedsLayout() + hostingController.view.layoutIfNeeded() + let result = hostingController.view.intrinsicContentSize.height + + return result + } + + private func removeController() { + hostingController.willMove(toParent: nil) + hostingController.view.removeFromSuperview() + hostingController.removeFromParent() + parentViewController = nil + } + + deinit { + removeController() + } + + + // MARK: user interaction + + func hideTopBanner() { + removeController() + NotificationCenter.default.post(name: .bwiHideTopBanner, object: self, userInfo: ["type" : "feature_banner"]) + } + + func didPressClose() { + hideTopBanner() + } + + func didPressShowDetails() { + let htmlFile = BWIBuildSettings.shared.bwiFeatureHistoryFilePath + self.webViewController = WebViewViewController(localHTMLFile: htmlFile) + webViewController?.title = BWIL10n.bwiSettingsNewFeaturesHeader + let navigationBar: UINavigationController = UINavigationController(rootViewController: webViewController!) + webViewController?.navigationItem.setLeftBarButton(UIBarButtonItem(title: VectorL10n.close, style: .plain, target: self, action: #selector(closeModal)), animated: false) + hostingController.parent?.present(navigationBar, animated: true, completion: { () -> Void in + self.hideTopBanner() + }) + } + + @objc func closeModal() { + webViewController?.dismiss(animated: true) + } +} + + +struct FeatureBannerView: View { + var delegate: FeatureBannerDelegate? + + var body: some View { + VStack(alignment: .center) { + closeButton + header + advertisementText + showMoreButton + } + .background(Color(ThemeService.shared().theme.tintColor)) + .cornerRadius(12) + .padding(16) + } + + var header: some View { + HStack() { + Image(Asset.Images.newFeatures.name) + .renderingMode(.template) + .foregroundColor(.white) + Text(BWIL10n.bwiFeatureBannerHeader) + .font(.system(size: 20).bold()) + .foregroundColor(.white) + } + } + + var advertisementText: some View { + Text(BWIL10n.bwiFeatureBannerAdvertisementText) + .font(.system(size: 15)) + .multilineTextAlignment(.center) + .lineLimit(nil) + .foregroundColor(.white) + .padding(5) + } + + var closeButton: some View { + HStack() { + Spacer() + Button { + delegate?.didPressClose() + } label: { + Image(Asset.Images.closeButton.name) + .renderingMode(.template) + .foregroundColor(.white) + .padding(10) + } + } + } + + var showMoreButton: some View { + Button { + delegate?.didPressShowDetails() + } label: { + Text(BWIL10n.bwiFeatureBannerShowMoreButton) + .font(.system(size: 15)) + .padding(10) + .foregroundColor(.white) + .overlay(RoundedRectangle(cornerRadius: 12) + .stroke(Color.white, lineWidth: 2)) + } + .padding(.top, 10) + .padding(.bottom, 25) + } + +} diff --git a/bwi/MatomoAnalytics/BWIAnalytics.swift b/bwi/MatomoAnalytics/BWIAnalytics.swift index 3b3faf4a4..2cb973d36 100644 --- a/bwi/MatomoAnalytics/BWIAnalytics.swift +++ b/bwi/MatomoAnalytics/BWIAnalytics.swift @@ -26,13 +26,13 @@ import MatomoTracker guard let session = session else { return false } - return BWIAnalyticsAccountDataService(mxSession: session).isEnabled() } set(enabled) { guard let session = session else { return } + fastRunning = enabled BWIAnalyticsAccountDataService(mxSession: session).setEnabled(enabled) } } @@ -42,7 +42,10 @@ import MatomoTracker private let appVersion: String private let appPlattform = "iOS" - var session: MXSession? = nil + // use internally to not always check for Account data + private var fastRunning: Bool = false + + private var session: MXSession? = nil private override init() { if !BWIBuildSettings.shared.secondaryAppName.isEmpty { @@ -50,9 +53,10 @@ import MatomoTracker } else { appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String } - appVersion = AppDelegate.theDelegate().appVersion + appVersion = AppInfo.current.appVersion?.bundleShortVersion ?? "" - if BWIBuildSettings.shared.bwiEnableErrorTracking { + if BWIBuildSettings.shared.bwiMatomoEnabled { + // bwi: Analytics use custom config guard let url = URL(string: AppConfigService.shared.analyticsUrl()) else { matomo = nil return @@ -71,12 +75,40 @@ import MatomoTracker } } + func setSession(session: MXSession) { + self.session = session + fastRunning = running + } + + func firstCall() { + fastRunning = running + } + func isEnabled() -> Bool { - return matomo != nil + return matomo != nil && BWIBuildSettings.shared.bwiMatomoEnabled } + // reset clientId on logout using internal matomo keys + func resetUserdefaults() { + // bwi: Analytics use custom config + let suiteName = BWIBuildSettings.shared.bwiAnalyticsAppId + AppConfigService.shared.analyticsUrl() + if let userdefaults = UserDefaults(suiteName: suiteName) { + userdefaults.removeObject(forKey: "PiwikTotalNumberOfVistsKey") + userdefaults.removeObject(forKey: "PiwikCurrentVisitTimestampKey") + userdefaults.removeObject(forKey: "PiwikPreviousVistsTimestampKey") + userdefaults.removeObject(forKey: "PiwikFirstVistsTimestampKey") + userdefaults.removeObject(forKey: "PiwikVisitorIDKey") + userdefaults.removeObject(forKey: "PiwikForcedVisitorIDKey") + userdefaults.removeObject(forKey: "PiwikVisitorUserIDKey") + userdefaults.removeObject(forKey: "PiwikOptOutKey") + userdefaults.removeObject(forKey: "PiwikLastOrderDateKey") + } + + } + + // Uses Userdefaults instead of Account Data because it needs to be shown on every login func needsToShowPromt() -> Bool { - if !BWIBuildSettings.shared.bwiEnableErrorTracking { + if !BWIBuildSettings.shared.bwiMatomoEnabled { return false } @@ -84,46 +116,53 @@ import MatomoTracker return false } - return !BWIAccountNotificationService(mxSession: session).isNotificationShown(BWIAccountNotificationService.AccountNotifications.CodingKeys.analyticsPromt.rawValue) + return BWIAnalyticsAccountDataService(mxSession: session).needsToShowPromt() } - func setPromtShown(_ shown: Bool) { - guard let session = session else { - return + func dispatchAll() { + if fastRunning { + matomo?.dispatch() } - - BWIAccountNotificationService(mxSession: session).setNotification(BWIAccountNotificationService.AccountNotifications.CodingKeys.analyticsPromt.rawValue, shown: shown) } func trackScreen(_ screenName: String) { - if running { + if fastRunning { matomo?.track(view: ["ios", appName, appVersion, screenName], url: nil) } } func trackEvent(_ category: String, action: String) { - if running { + if fastRunning { matomo?.track(eventWithCategory: category, action: action, url:nil) } } func trackBwiDuration(_ duration: TimeInterval, _ category: String, _ name: String) { - if running { + if fastRunning { matomo?.track(eventWithCategory: "Metrics", action: category, name: name, number: NSNumber(value: duration), url:nil) } } func trackBwiValue(_ value: NSNumber, _ category: String, _ action: String) { - if running { + if fastRunning { matomo?.track(eventWithCategory: category, action: action, name: nil, number: value, url:nil) } } func trackBwiValue(_ value: NSNumber, _ category: String, _ action: String, _ name: String) { - if running { + if fastRunning { matomo?.track(eventWithCategory: category, action: action, name: name, number: value, url:nil) } } + + func trackSlowMessage( dimension: String, value: Int) { + if fastRunning { + // bwi: Analytics use custom config + matomo?.setDimension(dimension, forIndex: 2) + matomo?.track(eventWithCategory: "Performance", action: "SendMessage", name: nil, number: NSNumber(value: value), url:nil) + matomo?.remove(dimensionAtIndex: 2) + } + } } extension BWIAnalytics : MXAnalyticsDelegate { @@ -185,5 +224,25 @@ extension BWIAnalytics : MXAnalyticsDelegate { func trackNonFatalIssue(_ issue: String, details: [String : Any]?) { // dont track NV specific logs } + + func trackE2EEError(_ errorCode: Int) { + if let errorCode = AnalyticsE2EEErrorCode(rawValue: errorCode) { + self.trackBwiValue(NSNumber(value: 1), "Encryption", "SendMessage", errorCode.description) + } else { + self.trackBwiValue(NSNumber(value: 1), "Encryption", "SendMessage", "Unknown_Error") + } + + } + + func trackE2EEErrors(_ reason: DecryptionFailureReason, count: Int) { + switch reason { + case .unspecified: + self.trackBwiValue(NSNumber(value: count), "Encryption", "SendMessage", "Unknown_Error") + case .olmIndexError: + self.trackBwiValue(NSNumber(value: count), "Encryption", "SendMessage", AnalyticsE2EEErrorCode(rawValue: 3)!.description) + case .olmKeysNotSent: + self.trackBwiValue(NSNumber(value: count), "Encryption", "SendMessage", AnalyticsE2EEErrorCode(rawValue: 4)!.description) + } + } } diff --git a/bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift b/bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift index 307aaff57..5a3bb06ae 100644 --- a/bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift +++ b/bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift @@ -22,10 +22,21 @@ import Foundation private struct AccountAnalytics: Codable { enum CodingKeys: String, CodingKey { - case analyticsEnabled = "enabled" + case consent + case time + case platform + case appVersion = "app_version" + } + + let consent: Bool + let time: Int + let platform: String + let appVersion: String + + func toJsonDict() -> [String : Any] { + return [CodingKeys.consent.rawValue:consent, CodingKeys.time.rawValue:time, CodingKeys.platform.rawValue:platform, CodingKeys.appVersion.rawValue:appVersion] } - let analyticsEnabled: Bool } private enum AccountDataTypes { @@ -36,17 +47,32 @@ import Foundation private lazy var serializationService: SerializationServiceType = SerializationService() init(mxSession: MXSession) { - self.session = mxSession + session = mxSession + } + + func needsToShowPromt() -> Bool { + guard let analyticsArray = session.accountData.accountData(forEventType: AccountDataTypes.analytics) as? [String: Any] else { + return true + } + guard analyticsArray[session.myDeviceId] is [String: Any] else { + return true + } + return false } func isEnabled() -> Bool { - guard let analyticsDict = session.accountData.accountData(forEventType: AccountDataTypes.analytics) as? [String: Any] else { + guard let analyticsArray = session.accountData.accountData(forEventType: AccountDataTypes.analytics) as? [String: Any] else { return false } + + guard let deviceId = session.myDeviceId, let analyticsDict = analyticsArray[deviceId] as? [String: Any] else { + return false + } + do { let analytics: AccountAnalytics = try serializationService.deserialize(analyticsDict) - return analytics.analyticsEnabled + return analytics.consent } catch { } @@ -56,8 +82,11 @@ import Foundation func setEnabled(_ enabled: Bool) { var analyticsDict = session.accountData.accountData(forEventType: AccountDataTypes.analytics) ?? [:] - analyticsDict[AccountAnalytics.CodingKeys.analyticsEnabled.rawValue] = enabled + let currentConsent = AccountAnalytics(consent: enabled, time: Int(Date().timeIntervalSince1970*1000), platform: "ios", appVersion: AppInfo.current.appVersion?.bundleShortVersion ?? "") + + analyticsDict[session.myDeviceId] = currentConsent.toJsonDict() + session.setAccountData(analyticsDict, forType: AccountDataTypes.analytics, success: nil, failure: nil) } } diff --git a/bwi/MatomoAnalytics/E2EEError.swift b/bwi/MatomoAnalytics/E2EEError.swift new file mode 100644 index 000000000..1c0a85af6 --- /dev/null +++ b/bwi/MatomoAnalytics/E2EEError.swift @@ -0,0 +1,99 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/* + typedef enum : NSUInteger + { + encryptionNotEnabledCode = 0, + unableToEncryptCode, + unableToDecryptCode, + olmCode, + unknownInboundSessionIdCode, + inboundSessionMismatchRoomIdCode, + missingFieldsCode, + missingCiphertextCode, + notIncludedInRecipientsCode, + badRecipientCode, + badRecipientKeyCode, + forwardedMessageCode, + badRoomCode, + badEncryptedMessageCode, + duplicateMessageIndexCode, + missingPropertyCode, + } Code; + */ + +enum AnalyticsE2EEErrorCode: Int { + case encryptionNotEnabledCode = 0 + case unableToEncryptCode = 1 + case unableToDecryptCode = 2 + case olmCode = 3 + case unknownInboundSessionIdCode = 4 + case inboundSessionMismatchRoomIdCode = 5 + case missingFieldsCode = 6 + case missingCiphertextCode = 7 + case notIncludedInRecipientsCode = 8 + case badRecipientCode = 9 + case badRecipientKeyCode = 10 + case forwardedMessageCode = 11 + case badRoomCode = 12 + case badEncryptedMessageCode = 13 + case duplicateMessageIndexCode = 14 + case missingPropertyCode = 15 + // Add more error codes as needed + + var description: String { + switch self { + case .encryptionNotEnabledCode: + return "Encryption_Not_Enabled" + case .unableToEncryptCode: + return "Unable_To_Encrypt" + case .unableToDecryptCode: + return "Unable_To_Decrypt" + case .olmCode: + return "Olm_Unknown_Message_Index" + case .unknownInboundSessionIdCode: + return "Unknown_Inbound_SessionId" + case .inboundSessionMismatchRoomIdCode: + return "Inbound_Session_Mismatch_RoomId" + case .missingFieldsCode: + return "Missing_Fields" + case .missingCiphertextCode: + return "Missing_Ciphertext" + case .notIncludedInRecipientsCode: + return "Not_Included_In_Recipients" + case .badRecipientCode: + return "Bad_Recipient" + case .badRecipientKeyCode: + return "Bad_Recipient_Key" + case .forwardedMessageCode: + return "Forwarded_Message" + case .badRoomCode: + return "Bad_Room" + case .badEncryptedMessageCode: + return "Bad_Encrypted_Message" + case .duplicateMessageIndexCode: + return "Duplicate_Message_Index" + case .missingPropertyCode: + return "Missing_Property" + } + } + + // You can also define additional methods or properties as needed +} diff --git a/bwi/PerformanceProfiles/PerformanceProfile.swift b/bwi/PerformanceProfiles/PerformanceProfile.swift index 194fc6138..b48aefe52 100644 --- a/bwi/PerformanceProfiles/PerformanceProfile.swift +++ b/bwi/PerformanceProfiles/PerformanceProfile.swift @@ -19,12 +19,12 @@ import Foundation @objcMembers class PerformanceProfile : NSObject { private var startTime: DispatchTime? - private var threshold: TimeInterval + private var trackingThreshold: TimeInterval private var timeInterval: TimeInterval private var aborted = false init(threshold: TimeInterval) { - self.threshold = threshold + self.trackingThreshold = threshold self.timeInterval = .zero super.init() } @@ -48,14 +48,38 @@ import Foundation } func isLogable() -> Bool { - return ((timeInterval > threshold) && !aborted) + return ((timeInterval > trackingThreshold) && !aborted) } func log2Analytics(users: Int, devices: Int) { if isLogable() { - let name = "Users: " + String(users) + " Devices: " + String(devices) + " Time: " + String(format: "%.1f", timeInterval) - print(name) - BWIAnalytics.sharedTracker.trackBwiValue(NSNumber(value: timeInterval), "Slow_Message", "Text_Message", name) + BWIAnalytics.sharedTracker.trackSlowMessage(dimension: dimensionForDeviceCount(devices), value: Int(timeInterval*1000)) + } + } + + func dimensionForDeviceCount(_ deviceCount: Int) -> String { + if deviceCount <= 0 { + return "Undefiniert" + } + if deviceCount <= 10 { + return "1 bis 10" + } + if deviceCount <= 100 { + return "11 bis 100" + } + if deviceCount <= 200 { + return "101 bis 200" + } + if deviceCount <= 500 { + return "201 bis 500" + } + if deviceCount <= 1000 { + return "501 bis 1000" + } + if deviceCount <= 2500 { + return "1001 bis 2500" + } else { + return "mehr als 2500" } } } diff --git a/bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift b/bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift index 9b137ff49..196fb2558 100644 --- a/bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift +++ b/bwi/QRCode/AuthenticationServerSelectionQRCodeScanner.swift @@ -16,8 +16,6 @@ */ import SwiftUI -import UIKit -import AVFoundation struct AuthenticationServerSelectionQRCodeScanner: View { @Environment(\.presentationMode) var presentationMode @@ -31,8 +29,11 @@ struct AuthenticationServerSelectionQRCodeScanner: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(VectorL10n.close) { + Button { presentationMode.wrappedValue.dismiss() + } label: { + Text(VectorL10n.close) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } } } @@ -44,102 +45,3 @@ struct AuthenticationServerSelectionQRCodeScanner: View { } } } - -struct ScannerView: UIViewControllerRepresentable { - @Binding var qrCode: String - @Binding var scanCompleted: Bool - - class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { - var parent: ScannerView - - init(_ parent: ScannerView) { - self.parent = parent - } - - func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { - - if let metadataObject = metadataObjects.first { - guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } - guard let stringValue = readableObject.stringValue else { return } - AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) - DispatchQueue.main.async { - self.parent.qrCode = stringValue - self.parent.scanCompleted = true - } - } - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - func makeUIViewController(context: Context) -> some ScannerViewController { - let controller = ScannerViewController() - controller.delegate = context.coordinator - return controller - } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - } - -} - -class ScannerViewController: UIViewController { - var captureSession: AVCaptureSession? - var previewLayer: AVCaptureVideoPreviewLayer! - let metadataOutput = AVCaptureMetadataOutput() - weak var delegate: AVCaptureMetadataOutputObjectsDelegate? - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = UIColor.black - captureSession = AVCaptureSession() - - guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else { - captureSession = nil - return - } - - if let captureSession = captureSession { - guard captureSession.canAddInput(videoInput) && captureSession.canAddOutput(metadataOutput) else { - self.captureSession = nil - return - } - - captureSession.addInput(videoInput) - captureSession.addOutput(metadataOutput) - - metadataOutput.setMetadataObjectsDelegate(delegate, queue: .main) - metadataOutput.metadataObjectTypes = [.qr] - - previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - previewLayer.frame = view.layer.bounds - previewLayer.videoGravity = .resizeAspectFill - view.layer.addSublayer(previewLayer) - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let dispatchQueue = DispatchQueue(label: "AVCapturesession.startRunning", qos: .background) - dispatchQueue.async{ - if (self.captureSession?.isRunning == false) { - self.captureSession?.startRunning() - } - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - let dispatchQueue = DispatchQueue(label: "AVCapturesession.stopRunning", qos: .background) - dispatchQueue.async{ - if (self.captureSession?.isRunning == true) { - self.captureSession?.stopRunning() - } - } - } -} diff --git a/bwi/QRCode/MyQRCodeView.swift b/bwi/QRCode/MyQRCodeView.swift new file mode 100644 index 000000000..f2588af49 --- /dev/null +++ b/bwi/QRCode/MyQRCodeView.swift @@ -0,0 +1,141 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import UIKit +import CoreImage.CIFilterBuiltins + +class MyQRCodeViewController: NSObject { + + @objc static func createFromSwiftUIView(matrixID: String, displayName: String, avatarUIImage: UIImage) -> UIViewController { + return UIHostingController(rootView: MyQRCodeView(matrixID: matrixID, displayName: displayName, avatarUIImage: avatarUIImage)) + } + + @objc static func createFromSwiftUIView(matrixID: String, displayName: String) -> UIViewController { + return UIHostingController(rootView: MyQRCodeView(matrixID: matrixID, displayName: displayName)) + } + +} + +struct MyQRCodeView: View { + let avatarSize = 80.0 + var matrixID: String + var displayName: String + var avatarUIImage: UIImage? + + var avatarImage: some View { + if let avatarUIImage = avatarUIImage { + return Image(uiImage: avatarUIImage) + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + } else { + return Image(uiImage: generateAvatarUIImage()) + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + } + } + + var qrCode: some View { + return Image(uiImage: generateQRCodeUIImage()) + .interpolation(.none) + .resizable() + .aspectRatio(1, contentMode: .fit) + .padding(8) + .frame(maxWidth: 200, maxHeight: 200) + .background(Color.white) + .cornerRadius(8) + } + + var grayContainer: some View { + qrCode + .padding(.top, 120) + .padding(.bottom, 30) + .padding(.horizontal, 50) + .background(Color(ThemeService.shared().theme.colors.system)) + .cornerRadius(8) + } + + var content: some View { + ZStack(alignment: .top) { + grayContainer + .padding(.top, avatarSize / 2) + VStack { + avatarImage + .padding(.bottom, 2) + Text(displayName) + .lineLimit(1) + .font(.system(size: 17, weight: .bold)) + .foregroundColor(Color(ThemeService.shared().theme.colors.primaryContent)) + .padding(.bottom, 2) + Text(matrixID) + .lineLimit(1) + .font(.system(size: 15)) + .foregroundColor(Color(ThemeService.shared().theme.colors.secondaryContent)) + .padding(.bottom, 20) + } + .padding(.horizontal, 10) + } + } + + var footer: some View { + Text(BWIL10n.showMyQrScreenMessage(AppInfo.current.displayName)) + .font(.system(size: 15)) + .foregroundColor(Color(ThemeService.shared().theme.colors.primaryContent)) + .multilineTextAlignment(.center) + } + + var body: some View { + ScrollView { + VStack(spacing: 20) { + content + footer + } + .padding(.vertical, 40) + .padding(.horizontal, 40) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(BWIL10n.showMyQrScreenTitle) + } + } + + private func generateAvatarUIImage() -> UIImage { + return AvatarGenerator.generateAvatar(forMatrixItem: matrixID, withDisplayName: displayName) + } + private func generateQRCodeUIImage() -> UIImage { + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + + if let permalink = MXTools.permalinkToUser(withUserId: matrixID) { + filter.message = Data(permalink.utf8) + + if let outputImage = filter.outputImage { + if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgImage) + } + } + } + return UIImage() + } + +} + +struct MyQRCodeView_Previews: PreviewProvider { + static var previews: some View { + MyQRCodeView(matrixID: "User ID", displayName: "Display Name") + } +} diff --git a/bwi/QRCode/PermalinkQRCodeScanner.swift b/bwi/QRCode/PermalinkQRCodeScanner.swift new file mode 100644 index 000000000..df3945181 --- /dev/null +++ b/bwi/QRCode/PermalinkQRCodeScanner.swift @@ -0,0 +1,83 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import UIKit + + +class PermalinkQRCodeScannerController: NSObject { + + @objc static func createFromSwiftUIView() -> UIViewController { + return UIHostingController(rootView: PermalinkQRCodeScanner()) + } + +} + +struct PermalinkQRCodeScanner: View { + @Environment(\.presentationMode) var presentationMode + @State var qrCode: String = "" + @State var scanCompleted = false + @State var showInvalidCodeAlert = false + + var body: some View { + NavigationView { + ScannerView(qrCode: $qrCode, scanCompleted: $scanCompleted) + .navigationTitle(BWIL10n.roomRecentsScanQrCode) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + Text(VectorL10n.close) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + } + } + } + .alert(isPresented: $showInvalidCodeAlert) { + Alert( + title: Text(BWIL10n.roomRecentsScanFailedTitle), + message: Text(BWIL10n.roomRecentsScanFailedMessage), + dismissButton: .default(Text(VectorL10n.ok)) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + presentationMode.wrappedValue.dismiss() + } + } + ) + } + } + .onChange(of: scanCompleted) { newValue in + if newValue { + checkQRCode() + } + } + } + + private func checkQRCode() { + if !BWIBuildSettings.shared.clientPermalinkBaseUrl.isEmpty && qrCode.hasPrefix(BWIBuildSettings.shared.clientPermalinkBaseUrl) { + presentationMode.wrappedValue.dismiss() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + if let url = URL(string: qrCode) { + AppDelegate.theDelegate().handleUniversalLinkURL(url) + } + } + } else { + showInvalidCodeAlert = true + } + } + +} diff --git a/bwi/QRCode/ScannerView.swift b/bwi/QRCode/ScannerView.swift new file mode 100644 index 000000000..e6d5fff21 --- /dev/null +++ b/bwi/QRCode/ScannerView.swift @@ -0,0 +1,120 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import UIKit +import AVFoundation + + +struct ScannerView: UIViewControllerRepresentable { + @Binding var qrCode: String + @Binding var scanCompleted: Bool + + class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + var parent: ScannerView + + init(_ parent: ScannerView) { + self.parent = parent + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + + if let metadataObject = metadataObjects.first { + guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } + guard let stringValue = readableObject.stringValue else { return } + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + DispatchQueue.main.async { + self.parent.qrCode = stringValue + self.parent.scanCompleted = true + } + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> some ScannerViewController { + let controller = ScannerViewController() + controller.delegate = context.coordinator + return controller + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } + +} + +class ScannerViewController: UIViewController { + var captureSession: AVCaptureSession? + var previewLayer: AVCaptureVideoPreviewLayer! + let metadataOutput = AVCaptureMetadataOutput() + weak var delegate: AVCaptureMetadataOutputObjectsDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.black + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else { + captureSession = nil + return + } + + if let captureSession = captureSession { + guard captureSession.canAddInput(videoInput) && captureSession.canAddOutput(metadataOutput) else { + self.captureSession = nil + return + } + + captureSession.addInput(videoInput) + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(delegate, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + let dispatchQueue = DispatchQueue(label: "AVCapturesession.startRunning", qos: .background) + dispatchQueue.async{ + if (self.captureSession?.isRunning == false) { + self.captureSession?.startRunning() + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + let dispatchQueue = DispatchQueue(label: "AVCapturesession.stopRunning", qos: .background) + dispatchQueue.async{ + if (self.captureSession?.isRunning == true) { + self.captureSession?.stopRunning() + } + } + } +} diff --git a/bwi/RollsAndRights/RoomRollsAndRightsPickerViewController.swift b/bwi/RollsAndRights/RoomRollsAndRightsPickerViewController.swift index c5135ab0e..29d1d62c1 100644 --- a/bwi/RollsAndRights/RoomRollsAndRightsPickerViewController.swift +++ b/bwi/RollsAndRights/RoomRollsAndRightsPickerViewController.swift @@ -122,7 +122,7 @@ extension RoomRollsAndRightsPickerViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .default, reuseIdentifier: "rolls_and_rights_picker_cells") - cell.tintColor = theme.tintColor // only for the accessory view + cell.tintColor = ThemeService.shared().theme.tintColor // only for the accessory view cell.backgroundColor = theme.backgroundColor cell.textLabel?.textColor = theme.textPrimaryColor diff --git a/bwi/ServerMaintenance/ServerDownTimeService.swift b/bwi/ServerMaintenance/ServerDownTimeService.swift index d63c38be3..cc2dc808e 100644 --- a/bwi/ServerMaintenance/ServerDownTimeService.swift +++ b/bwi/ServerMaintenance/ServerDownTimeService.swift @@ -29,4 +29,5 @@ protocol ServerDowntimeService { func downtimeText() -> String func downtimeColor() -> UIColor func downtimeTextColor() -> UIColor + func isSameDay() -> Bool } diff --git a/bwi/ServerMaintenance/ServerDowntimeDefaultService.swift b/bwi/ServerMaintenance/ServerDowntimeDefaultService.swift index 0ffc5ac48..dfd1d557b 100644 --- a/bwi/ServerMaintenance/ServerDowntimeDefaultService.swift +++ b/bwi/ServerMaintenance/ServerDowntimeDefaultService.swift @@ -200,8 +200,18 @@ extension ServerDowntimeDefaultService : ServerDowntimeService { return "" } + let dayOfWeekFormatter = DateFormatter() + dayOfWeekFormatter.dateFormat = "EEEE" + dayOfWeekFormatter.timeZone = TimeZone.current + dayOfWeekFormatter.calendar = Calendar.current + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd.MM.yyyy" + dateFormatter.timeZone = TimeZone.current + dateFormatter.calendar = Calendar.current + let dateTimeFormatter = DateFormatter() - dateTimeFormatter.dateFormat = "EE dd.MM.yyyy HH:mm" + dateTimeFormatter.dateFormat = "dd.MM.yyyy HH:mm" dateTimeFormatter.timeZone = TimeZone.current dateTimeFormatter.calendar = Calendar.current @@ -214,18 +224,20 @@ extension ServerDowntimeDefaultService : ServerDowntimeService { utcFormatter.dateFormat = "ZZZZZ" utcFormatter.timeZone = TimeZone.current utcFormatter.calendar = Calendar.current - - let isSameDay = Calendar.current.dateComponents([.day], from: startDate, to: endDate).day == 0 - - if isSameDay { + + if self.isSameDay() { return BWIL10n.settingsDowntimeMessageSameDay(AppInfo.current.displayName, - dateTimeFormatter.string(from: startDate), + dayOfWeekFormatter.string(from: startDate), + dateFormatter.string(from: startDate), + timeFormatter.string(from: startDate), timeFormatter.string(from: endDate), utcFormatter.string(from: startDate)) } else { return BWIL10n.settingsDowntimeMessageDifferentDays(AppInfo.current.displayName, + dayOfWeekFormatter.string(from: startDate), dateTimeFormatter.string(from: startDate), utcFormatter.string(from: startDate), + dayOfWeekFormatter.string(from: endDate), dateTimeFormatter.string(from: endDate), utcFormatter.string(from: endDate)) } @@ -260,4 +272,11 @@ extension ServerDowntimeDefaultService : ServerDowntimeService { } } + @objc func isSameDay() -> Bool { + guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601LocalDate, let endDate = downTime.endTime.iso8601LocalDate else { + return false + } + + return Calendar.current.dateComponents([.day], from: startDate, to: endDate).day == 0 + } } diff --git a/bwi/Tools/BWINewFeatureHelper.swift b/bwi/Tools/BWINewFeatureHelper.swift deleted file mode 100644 index 852fe06de..000000000 --- a/bwi/Tools/BWINewFeatureHelper.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -/* - * Copyright (c) 2022 BWI GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Foundation - -@objc class BwiNewFeatureHelper: NSObject { - - @objc func replaceFeatureHistoryLink() { - guard let urlPath = Bundle.main.url(forResource: BWIBuildSettings.shared.newFeaturesHTML, withExtension: "html") else { - return - } - - //reading - do { - var text = try String(contentsOf: urlPath, encoding: .utf8) - text = text.replacingOccurrences(of: "$FEATURELINK", with: BWIBuildSettings.shared.bwiFeatureHistoryLink) - try text.write(to: urlPath, atomically: false, encoding: .utf8) - } - catch { - - } - } -} - diff --git a/bwi/TopBanner/FeatureBannerVisibilityService.swift b/bwi/TopBanner/FeatureBannerVisibilityService.swift index 13a655bce..81d5b0d3e 100644 --- a/bwi/TopBanner/FeatureBannerVisibilityService.swift +++ b/bwi/TopBanner/FeatureBannerVisibilityService.swift @@ -50,7 +50,10 @@ import Foundation featureDict[version] = true - return session.setAccountData(featureDict, forType: AccountDataTypes.featureVisibility, success: nil, failure: nil) + return session.setAccountData(featureDict, forType: AccountDataTypes.featureVisibility, success: { + // bwi: update tableview + NotificationCenter.default.post(name: .bwiMarkTopBannerAsUnRead, object: nil, userInfo: ["type" : "feature_banner"]) + }, failure: nil) } func isUnread( version: String, completion: @escaping (_ unread : Bool) -> Void) { diff --git a/bwi/TopBanner/TopBannerViewController.swift b/bwi/TopBanner/TopBannerViewController.swift index d46593e91..f0571b8a6 100644 --- a/bwi/TopBanner/TopBannerViewController.swift +++ b/bwi/TopBanner/TopBannerViewController.swift @@ -85,4 +85,5 @@ extension TopBannerViewController: UIPageViewControllerDelegate { extension NSNotification.Name { static let bwiHideTopBanner = Notification.Name("de.bwi.messenger.hide_top_banner") static let bwiMarkTopBannerAsRead = Notification.Name("de.bwi.messenger.mark_top_banner_as_read") + static let bwiMarkTopBannerAsUnRead = Notification.Name("de.bwi.messenger.mark_top_banner_as_unread") } diff --git a/bwi/Wellknown/Wellknown+Bwi.swift b/bwi/Wellknown/Wellknown+Bwi.swift index 0fc55e224..7040fc475 100644 --- a/bwi/Wellknown/Wellknown+Bwi.swift +++ b/bwi/Wellknown/Wellknown+Bwi.swift @@ -20,22 +20,19 @@ import Foundation public extension MXWellKnown { @objc func backupRequired() -> Bool { - do { - let security = try WellknownSecurity(dict: self.jsonDictionary()["io.element.e2ee"] as! [String : Any]) + if let dict = self.jsonDictionary()["io.element.e2ee"] as? [String : Any], let security = try? WellknownSecurity(dict: dict) { return security.backupRequired - } - catch { + } else { return true } } @objc func backupMethods() -> Array? { - do { - let security = try WellknownSecurity(dict: self.jsonDictionary()["io.element.e2ee"] as! [String : Any]) + if let dict = self.jsonDictionary()["io.element.e2ee"] as? [String : Any], let security = try? WellknownSecurity(dict: dict) { return security.backupMethods } - catch { + else { return ["passphrase"] } } diff --git a/project.yml b/project.yml index eb23828f9..9d63a33aa 100644 --- a/project.yml +++ b/project.yml @@ -42,6 +42,9 @@ include: - path: CommonKit/targetUnitTests.yml packages: + AnalyticsEvents: + url: https://github.com/matrix-org/matrix-analytics-events + exactVersion: 0.5.0 Mapbox: url: https://github.com/maplibre/maplibre-gl-native-distribution minVersion: 5.12.2 @@ -52,14 +55,17 @@ packages: maxVersion: 2.0.0 SwiftOGG: url: https://github.com/vector-im/swift-ogg - branch: main + branch: 0.0.1 Lottie: url: https://github.com/airbnb/lottie-ios.git minVersion: 3.5.0 maxVersion: 3.5.0 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - revision: 534ee5bae5e8de69ed398937b5edb7b5f21551d2 + version: 1.1.1 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 + DTCoreText: + url: https://github.com/Cocoanetics/DTCoreText + version: 1.6.27