diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7b0eb99c0b..6b753920e0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -9,7 +9,7 @@ on: jobs: integration_tests: name: Integration Tests - runs-on: macos-14 + runs-on: perf-only concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. @@ -29,7 +29,10 @@ jobs: - name: Setup environment run: source ci_scripts/ci_common.sh && setup_github_actions_environment - + + - name: Delete old log files + run: find '/Users/Shared' -name 'console*' -delete + - name: Run tests run: bundle exec fastlane integration_tests env: @@ -37,6 +40,12 @@ jobs: INTEGRATION_TESTS_USERNAME: ${{ secrets.INTEGRATION_TESTS_USERNAME }} INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }} + - name: Check logs are set to the `trace` level + run: (grep ' TRACE ' /Users/Shared -qR) + + - name: Check logs don't contain private messages + run: "! grep 'Go down in flames' /Users/Shared -R" + - name: Zip results # for faster upload if: failure() working-directory: fastlane/test_output @@ -69,6 +78,7 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} flags: integrationtests + version: v0.7.3 - name: Collect test results if: ${{ !cancelled() }} diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml index eab569396a..5fa202cb6f 100644 --- a/.github/workflows/ui_tests.yml +++ b/.github/workflows/ui_tests.yml @@ -13,7 +13,7 @@ on: jobs: tests: name: Tests - runs-on: macos-14 + runs-on: perf-only concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. @@ -30,9 +30,6 @@ jobs: restore-keys: | ${{ runner.os }}-gems- - - name: Free disk space - run: ci_scripts/free_space.sh - - name: Setup environment run: source ci_scripts/ci_common.sh && setup_github_actions_environment @@ -67,6 +64,7 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} flags: uitests + version: v0.7.3 - name: Collect test results if: ${{ !cancelled() }} diff --git a/.github/workflows/unit_tests_enterprise.yml b/.github/workflows/unit_tests_enterprise.yml index 8c195bf915..5bc8baac36 100644 --- a/.github/workflows/unit_tests_enterprise.yml +++ b/.github/workflows/unit_tests_enterprise.yml @@ -44,7 +44,7 @@ jobs: - name: SwiftFormat run: swiftformat --lint . - + - name: Run tests run: bundle exec fastlane unit_tests skip_previews:true diff --git a/CHANGES.md b/CHANGES.md index 8ae2d2c28c..2987f6851c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,182 @@ +## Changes in 1.9.3 (2024-10-24) + +### What's Changed + +🙌 Improvements +* Update HeroImage to match the BigIcon component from Compound. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3439 +* Update compound to change checkmark color by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3440 + +🐛 Bugfixes +* Fix a bug where the pinned items banner could overlay the composer. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3441 +* Fix composer mention pills showing up as file icons on first use on iOS 18 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3444 +* Fix a bug where the room state wouldn't indicate when a call was in progress. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3442 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.2...1.9.3 + +## Changes in 1.9.2 (2024-10-23) + +### What's Changed + +🙌 Improvements +* Add support for rendering media captions in the timeline. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3429 +* Show a verification badge on the Room Member/User Profile screens. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3427 + +🐛 Bugfixes +* Only subscribe to identity updates if the room is encrypted. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3414 +* Fix the pinned identity banner to always show the user ID regardless of ambiguity. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3415 +* Fix a bug where uploaded images could have the wrong aspect ratio in the timeline. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3435 + +⚠️ API Changes +* Adopt various rust side Timeline API additions by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3423 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3433 + +🚧 In development 🚧 +* Allow image uploads to be optimised to reduce bandwidth. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3412 +* Knock and knocked state for the join room screen by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3424 + +Others +* Fix some warnings. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3416 +* Refactor the`TimelineItemIdentifier` handling by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3418 +* Remove superfluous media request upload handle cancellation call. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3425 +* Update dependency fastlane to v2.225.0 by @renovate in https://github.com/element-hq/element-x-ios/pull/3434 +* Adopt various Rust side API changes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3437 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.1...1.9.2 + +## Changes in 1.9.1 (2024-10-15) + +### What's Changed + +🐛 Bugfixes +* Fix a bug opening images with a valid filename but a mimetype of `image/*` (sent by EXA). by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3407 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3406 + +🚧 In development 🚧 +* Create Room with knock rule by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3397 +* Allow video uploads to be optimised to reduce bandwidth. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3408 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.0...1.9.1 + +## Changes in 1.9.0 (2024-10-10) + +### What's Changed + +🐛 Bugfixes +* Fix identity pinning link. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3395 + +🧱 Build +* Update the version to 1.9.0. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3396 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.8.6...1.9.0 + +## Changes in 1.8.6 (2024-10-10) + +### What's Changed + +✨ Features +* crypto: Configure decryption trustRequirement based on config flag by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/3358 +* Introduce a feature flag for the new identity pinning violation notifications feature by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3394 +* Show the Login with QR Code button. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3392 + +🙌 Improvements +* Add a subtitle to the QR Code login instructions. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3386 +* Tweak the UI in the EncryptionReset, IdentityConfirmation and SecureBackupRecovery screens. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3391 +* Update the secondary button stroke colour. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3393 + +Others +* Fix an authentication UI test snapshot. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3387 +* Ask the iPad to reveal the keyboard in UI Tests when it's hidden. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3389 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.8.5...1.8.6 + +## Changes in 1.8.5 (2024-10-08) + +### What's Changed + +✨ Features +* Display a warning when a user's pinned identity changes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3368 + +🙌 Improvements +* Add detection for latest devices. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3327 +* Configure the AuthenticationService later now that we have 2 flows on the start screen. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3316 +* Selecting a server that doesn't support login now fails instead of letting you continue to a failure later. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3342 +* Add new emoji from iOS 17.4 to the reaction picker. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3376 + +🐛 Bugfixes +* Use a plain view for reactions instead of a TabView. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3328 +* Upgrade Kingfisher to fix a bug that prevented GIFs from being tapped. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3326 +* Make sure the room header takes up as much space as possible (to hide the back button). by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3335 +* Have ElementCall always default to the speaker; prevent the lock button from ending the call by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3338 +* Allow focusing the different avatars making up a DM details cluster separately. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3341 +* Disable auto correction when running on the Mac by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3364 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3347 +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3371 + +🧱 Build +* Start fixing flakey tests ❄️ by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3329 +* Integration test runner switch by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3343 +* Switch UI tests back to the perf-only runner. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3349 + +🚧 In development 🚧 +* Add developer option to hide media in the timeline. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3366 + +Others +* Integration test improvements by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3330 +* crypto: rename invisible crypto flag to deviceIsolationMode by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/3331 +* chore(deps): update dependency fastlane to v2.223.0 by @renovate in https://github.com/element-hq/element-x-ios/pull/3337 +* Log any failures when creating a call widget. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3339 +* chore(deps): update dependency fastlane to v2.223.1 by @renovate in https://github.com/element-hq/element-x-ios/pull/3340 +* Tracing and integration test tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3336 +* Remove message pinning FF by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3318 +* Move the core logic in LoginScreenCoordinator into the ViewModel. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3348 +* Bump the RustSDK to v1.0.53: adopt latest record based timeline item APIs by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3356 +* use element-hq RTE version by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3360 +* Hide timeline media preparation by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3361 +* chore(deps): update dependency fastlane to v2.224.0 by @renovate in https://github.com/element-hq/element-x-ios/pull/3370 +* Record a missing snapshot. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3374 +* Update the SDK and use media `filename` and `caption` internally. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3375 +* update sdk by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3377 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.8.4...1.8.5 + +## Changes in 1.8.4 (2024-09-24) + +### What's Changed + +✨ Features +* Enable message pinning by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3308 + +🐛 Bugfixes +* Fix: confusion of lab flags for invisible crypto by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/3319 +* Fix a regression where you can't scroll the timeline on iOS 17 by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3320 +* Fix a bug where the Join Room screen was sometimes shown instead of the Room. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3323 +* Fix a bug on iOS 18 where the timeline background would use the wrong colour scheme when using the app switcher. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3324 +* Don't use the new iPad modal presentation mode for the timeline item menu by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3325 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3315 + +🧱 Build +* Update the project to use Xcode 16. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3303 + +Others +* A bunch of random tweaks. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3317 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.8.3...1.8.4 + ## Changes in 1.8.3 (2024-09-19) ### What's Changed diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8d5496b220..715b05cf70 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; }; 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; }; 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; }; - 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422724361B6555364C43281E /* RoomHeaderView.swift */; }; 04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */; }; 053B8BD2496207838878C6C9 /* PinnedItemsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C9BAE9F9436B14E4E22E8F /* PinnedItemsBannerView.swift */; }; 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; @@ -46,6 +45,7 @@ 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; 06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; + 07376A5274822EB45CC320C7 /* InvitedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 077CB230153E072C94B1E6C3 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */; }; 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */; }; @@ -83,6 +83,7 @@ 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; + 0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; }; 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; @@ -136,20 +137,19 @@ 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; - 1C815DD79B401DEBA2914773 /* TimelineItemMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A1003A0F7A1DFB47F4E2D0 /* TimelineItemMock.swift */; }; 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; }; 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; 1D623953F970D11F6F38499C /* AppLockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851B95BB98649B8E773D6790 /* AppLockService.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; }; - 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; }; 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 20C16A3F718802B0E4A19C83 /* URLComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76310030C831D4610A705603 /* URLComponentsTests.swift */; }; + 210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */; }; 2118E35D312951B241067BD5 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345172AD4377E83A44BD864F /* MessageComposerTextField.swift */; }; 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; 21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; }; @@ -158,6 +158,7 @@ 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; }; + 230981086F0199F913434D6B /* EncryptionSettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */; }; 2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; }; 23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; @@ -205,11 +206,13 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; 2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; + 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */; }; 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; }; 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; }; 2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; + 2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; 2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; }; 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; @@ -224,8 +227,8 @@ 3113065AABBC14CEAE6843FA /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; }; 3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; }; + 31A27DD23C8637A0EBA76AFB /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 7229371D48BE92239D852C1B /* test_rotated_image.jpg */; }; 32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; }; - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; 33F1FB19F222BA9930AB1A00 /* RoomListFiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6372DD10DED30E7AD7BCE21 /* RoomListFiltersView.swift */; }; @@ -278,6 +281,7 @@ 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */; }; 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */; }; + 3E23BB48F91485D893D0A429 /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9C2BCB402FEB0FA1A54BEF4B /* test_animated_image.gif */; }; 3E7B65C2C97748D5D65AAA8B /* NotificationPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */; }; 3EC5A41F9FB7DD63A4DC6144 /* RoomChangeRolesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14B1DE3E2D5D26732C49036 /* RoomChangeRolesScreenViewModel.swift */; }; 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 893777A4997BBDB68079D4F5 /* ArrayTests.swift */; }; @@ -290,6 +294,7 @@ 407DCE030E0F9B7C9861D38A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 40B79D20A873620F7F128A2C /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; + 41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */; }; 41CE5E1289C8768FC5B6490C /* RoomTimelineItemViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */; }; 41DFDD212D1BE57CA50D783B /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; }; 41F553349AF44567184822D8 /* APNSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D670124FC3E84F23A62CCF /* APNSPayload.swift */; }; @@ -309,6 +314,7 @@ 454311EAC17D778E19F46592 /* NotificationPermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91868EB98818044E6FEBE532 /* NotificationPermissionsScreenCoordinator.swift */; }; 454F8DDC4442C0DE54094902 /* LABiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F219838588C62198E726E3 /* LABiometryType.swift */; }; 4557192F5B15A8D9BB920232 /* AdvancedSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */; }; + 45D6DC594816288983627484 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */; }; 46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */; }; @@ -320,6 +326,7 @@ 4715FE33667C5899E64DD0E6 /* ResolveVerifiedUserSendFailureScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */; }; 4716587A9BA69ED8FD1B986B /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B19D10B102956066AF117B /* PollOptionView.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; + 478C363F63A5DFC3C83E334C /* MediaProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; 4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */; }; 48416BBEB8DDF3E4DED0EDB6 /* ElementCallServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */; }; @@ -348,6 +355,7 @@ 4CE638FD837ED72CD98AD9A9 /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; }; 4D0F4385B7DDB68C66C78857 /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258C9C815272911A5B132C3 /* FormattedBodyText.swift */; }; 4D23D41B8109E010304050F8 /* QRCodeLoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */; }; + 4D2B54233C7B2C04B4ABE55A /* EncryptionSettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */; }; 4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */; }; 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */; }; 4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; @@ -358,7 +366,6 @@ 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; 4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; }; 4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; }; - 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; 4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; }; 4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; }; 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; @@ -408,7 +415,7 @@ 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; + 5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */; }; 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; }; 5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; @@ -444,6 +451,7 @@ 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; 64E541F88F35BD126C4AFCA1 /* AppLockScreenPINKeypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */; }; 64EE9D2CF7AD02EE53983CE1 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */; }; + 64F8590F4BEE4DA231F97D83 /* EncryptionResetFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */; }; 651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; }; 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; 6530865EB9A8C0F0AF0216DA /* ServerSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */; }; @@ -454,11 +462,14 @@ 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */; }; 66357ECB73B1290E5490A012 /* WebRegistrationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F418426410F3823F3EB0828 /* WebRegistrationScreenViewModelProtocol.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; + 6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */; }; + 6793E75E3EBE48EBB8F857AF /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */; }; + 67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */; }; 67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C713D124FE915ABF47A6B7 /* TimelineView.swift */; }; 6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; }; 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; @@ -505,10 +516,10 @@ 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; - 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; + 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; }; 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */; }; 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; }; 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; }; @@ -523,6 +534,7 @@ 7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.swift */; }; 767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; }; 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; }; + 76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; 7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */; }; @@ -541,6 +553,7 @@ 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; }; 798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */; }; 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */; }; + 79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */; }; 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; }; 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; }; 7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; }; @@ -602,9 +615,11 @@ 865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; }; 8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; + 86DFA58FBBEB0AF671D2A1E1 /* HomeScreenKnockedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */; }; 86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; 874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F7CCC4A9D1927223F559D5 /* AuthenticationStartScreenViewModelProtocol.swift */; }; + 877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */; }; 878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; }; 87B4E59A4467F8EC75F82372 /* VoiceMessageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */; }; 87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; }; @@ -655,13 +670,14 @@ 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; }; + 91D1A46A733EC24C081DD353 /* SessionVerificationRequestDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */; }; + 92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; }; 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; }; 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; }; 934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */; }; 937985546F708339711ECDFC /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */; }; - 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93A549135E6C027A0D823BFE /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 593FBBF394712F2963E98A0B /* DTCoreText */; }; 93AC1E8418D8C827671FB3A9 /* IdentityConfirmedScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */; }; 93BA4A81B6D893271101F9F0 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; @@ -706,6 +722,7 @@ 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9C4EC28A921486B1775D7F8C /* IdentityConfirmedScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */; }; 9C55746D8F6A3E35CFCF4A7A /* AuthenticationStartLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598F01EBD0C4CC550C644418 /* AuthenticationStartLogo.swift */; }; + 9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */; }; 9CBB04365408F9D6F46BA3A7 /* PinnedEventsTimelineFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */; }; 9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; @@ -715,6 +732,7 @@ 9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; }; 9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; }; 9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */; }; + 9EF9773DBE3F6497A25CE236 /* test_apple_image.heic in Resources */ = {isa = PBXBuildFile; fileRef = F6B676B4866F5B383DE819B2 /* test_apple_image.heic */; }; 9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; }; 9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */; }; 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */; }; @@ -749,7 +767,9 @@ A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */; }; A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; + A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; }; A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; }; + A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; }; @@ -817,11 +837,9 @@ B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; }; - B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; - B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */; }; B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; }; B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; @@ -847,7 +865,6 @@ BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */; }; BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1DDB2293A51EA4C2739351 /* RoomListFiltersEmptyStateView.swift */; }; - BD11E639CF566A9DA8FCA717 /* RoundedLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */; }; BD6685592716CA957D7BAAC4 /* RoomChangeRolesScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9B45D584D232CB9E5C7734 /* RoomChangeRolesScreenSelectedItem.swift */; }; BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */; }; BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */; }; @@ -889,6 +906,7 @@ C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; }; C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68010886142843705E342645 /* ProgressMaskModifier.swift */; }; + C7B07EBA0F12B5912DA9BB97 /* UserIdentitySDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */; }; C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; }; C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */; }; C8A9C595038AFA2D707AC8C1 /* NotificationPermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */; }; @@ -896,12 +914,14 @@ C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4427AF4B7FB7EF3E3D424C7 /* QRCodeLoginService.swift */; }; C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; + C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; C9A631FD968249B4BA0B7B3C /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */; }; C9ABF75A43F2D26F1D9A1F27 /* DeactivateAccountScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC3FDB58F57386741A4FC7F /* DeactivateAccountScreenViewModel.swift */; }; C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; }; C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; }; CA12AE0DCD57D49CD96C699A /* WaveformCursorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */; }; + CACD1352927336F01FC76612 /* EncryptionResetUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */; }; CB137BFB3E083C33E398A6CB /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; CB498F4E27AA0545DCEF0F6F /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; CB6956565D858C523E3E3B16 /* ComposerDraftServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */; }; @@ -950,13 +970,11 @@ D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; - D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; - D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; }; D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; }; @@ -1030,6 +1048,7 @@ E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; }; E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; + E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; }; E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; }; E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; }; @@ -1054,11 +1073,11 @@ EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; }; EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; }; EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; }; - EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; + EED33AFD9334EFD7398707A6 /* VisualListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD529C89924EE32CE307F36F /* VisualListItem.swift */; }; EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; EF47D802A404A53F15D5D4B6 /* JoinRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */; }; EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; }; @@ -1120,13 +1139,13 @@ FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; }; FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; + FC0EEFF630F34899953BB950 /* BigIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01FD1171FF40E34D707FD00 /* BigIcon.swift */; }; FC10228E73323BDC09526F97 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; FC8B95EC506E6BB5793D81CE /* ClientProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34685D186453E429ADEE58E /* ClientProtocolTests.swift */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */; }; FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; - FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; @@ -1135,6 +1154,7 @@ FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */; }; FF9C06BBF6AC6F1CFFBEBFFC /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 90791B9C739C716A40E1B230 /* target.yml */; }; + FFD52DCDA6962055A363CC8F /* IdentityResetHandleSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1219,6 +1239,7 @@ 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = ""; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = ""; }; + 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactoryMock.swift; sourceTree = ""; }; 05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = ""; }; 05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = ""; }; @@ -1229,6 +1250,7 @@ 06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenCoordinator.swift; sourceTree = ""; }; 0724EBDFE8BB4C9E5547C57D /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsChatType.swift; sourceTree = ""; }; + 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedRoomProxy.swift; sourceTree = ""; }; 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = ""; }; @@ -1288,6 +1310,7 @@ 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorProtocol.swift; sourceTree = ""; }; 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = ""; }; 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; + 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; @@ -1298,10 +1321,12 @@ 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = ""; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = ""; }; 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; + 1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationRequestDetailsView.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = ""; }; 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = ""; }; 1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = ""; }; + 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = ""; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = ""; }; 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = ""; }; @@ -1311,7 +1336,6 @@ 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItemSDKMock.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = ""; }; @@ -1323,7 +1347,6 @@ 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = ""; }; 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = ""; }; 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = ""; }; - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = ""; }; @@ -1440,7 +1463,6 @@ 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = ""; }; - 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreen.swift; sourceTree = ""; }; @@ -1453,12 +1475,13 @@ 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerMock.swift; sourceTree = ""; }; 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = ""; }; + 3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitedRoomProxyMock.swift; sourceTree = ""; }; 3AD253E7EFF88F308D644272 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SAS.strings"; sourceTree = ""; }; 3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = ""; }; 3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = ""; }; 3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = ""; }; 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = ""; }; - 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; + 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsUITests.swift; sourceTree = ""; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; @@ -1482,8 +1505,10 @@ 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = ""; }; 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = ""; }; 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = ""; }; + 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInfoProxy.swift; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = ""; }; + 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = ""; }; 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = ""; }; 419957D7B1C983D7B3B93678 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenModels.swift; sourceTree = ""; }; @@ -1491,7 +1516,6 @@ 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutLabelStyle.swift; sourceTree = ""; }; - 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = ""; }; 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; @@ -1519,6 +1543,7 @@ 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = ""; }; 475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = ""; }; 475EB595D7527E9A8A14043E /* uz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uz; path = uz.lproj/Localizable.strings; sourceTree = ""; }; + 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderMock.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1535,6 +1560,7 @@ 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; + 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = ""; }; @@ -1550,7 +1576,6 @@ 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorTests.swift; sourceTree = ""; }; 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = ""; }; - 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = ""; }; 502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenModels.swift; sourceTree = ""; }; 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; @@ -1601,6 +1626,7 @@ 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = ""; }; 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = ""; }; 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = ""; }; 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = ""; }; 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = ""; }; 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = ""; }; @@ -1614,6 +1640,7 @@ 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = ""; }; 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = ""; }; + 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityResetHandleSDKMock.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; @@ -1646,6 +1673,7 @@ 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; + 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceTests.swift; sourceTree = ""; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = ""; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; @@ -1672,6 +1700,7 @@ 6F1C3CBBC62C566DDF5E84C1 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; 6F418426410F3823F3EB0828 /* WebRegistrationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreenViewModelProtocol.swift; sourceTree = ""; }; + 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderMock.swift; sourceTree = ""; }; 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = ""; }; 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1685,6 +1714,7 @@ 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelTests.swift; sourceTree = ""; }; + 7229371D48BE92239D852C1B /* test_rotated_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = test_rotated_image.jpg; sourceTree = ""; }; 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; 7310D8DFE01AF45F0689C3AA /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; @@ -1734,7 +1764,6 @@ 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenCoordinator.swift; sourceTree = ""; }; 7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenViewModelProtocol.swift; sourceTree = ""; }; 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInviterLabel.swift; sourceTree = ""; }; - 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroImage.swift; sourceTree = ""; }; 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelTests.swift; sourceTree = ""; }; 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = ""; }; 7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; @@ -1770,6 +1799,7 @@ 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = ""; }; 8585C636A10B8141A7AE909F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/InfoPlist.strings; sourceTree = ""; }; + 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockedRoomProxy.swift; sourceTree = ""; }; 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; 8609BE4CA71C30D1FCE3AF9B /* AuthenticationStartScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenModels.swift; sourceTree = ""; }; 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1795,6 +1825,7 @@ 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; + 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderProtocol.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1805,6 +1836,7 @@ 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; + 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; 8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = ""; }; 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = ""; }; @@ -1862,6 +1894,7 @@ 9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = ""; }; 9B67DF223EEB8DCAF178A1D4 /* AnalyticsPromptScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenCoordinator.swift; sourceTree = ""; }; 9B7D8D3638864B7482E148CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + 9C2BCB402FEB0FA1A54BEF4B /* test_animated_image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test_animated_image.gif; sourceTree = ""; }; 9C3ACC093F88FD9888518561 /* AuthenticationStartScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModel.swift; sourceTree = ""; }; 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; @@ -1871,6 +1904,8 @@ 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; + 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockedRoomProxyMock.swift; sourceTree = ""; }; + 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreMock.swift; sourceTree = ""; }; 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceProtocol.swift; sourceTree = ""; }; 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; 9F40FB0A43DAECEC27C73722 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/SAS.strings; sourceTree = ""; }; @@ -1879,8 +1914,9 @@ A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = ""; }; A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = ""; }; A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = ""; }; - A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; + A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetFlowCoordinator.swift; sourceTree = ""; }; + A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenKnockedCell.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = ""; }; @@ -1895,7 +1931,6 @@ A433BE28B40D418237BE37B5 /* ReportContentScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreen.swift; sourceTree = ""; }; A436057DBEA1A23CA8CB1FD7 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; - A4A1003A0F7A1DFB47F4E2D0 /* TimelineItemMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMock.swift; sourceTree = ""; }; A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineFlowCoordinator.swift; sourceTree = ""; }; A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = ""; }; A6702BC84D3CC2421D78CD4E /* WebRegistrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreenViewModel.swift; sourceTree = ""; }; @@ -1912,6 +1947,7 @@ A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8DF55467ED4CE76B7AE9A33 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; + A9873374E72AA53260AE90A2 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFlowCoordinator.swift; sourceTree = ""; }; A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProvider.swift; sourceTree = ""; }; A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; @@ -1932,6 +1968,7 @@ ACD7BD6BEE21264F6677904A /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedWaveformView.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + AD529C89924EE32CE307F36F /* VisualListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualListItem.swift; sourceTree = ""; }; AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreen.swift; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineView.swift; sourceTree = ""; }; @@ -1967,7 +2004,6 @@ B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = ""; }; B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; - B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelTests.swift; sourceTree = ""; }; B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = ""; }; @@ -1981,6 +2017,7 @@ B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; + B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilderFactory.swift; sourceTree = ""; }; B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHooks.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = ""; }; @@ -2008,6 +2045,7 @@ BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; + BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetUITests.swift; sourceTree = ""; }; BE78CAD0B964C66FD06EF83E /* DeactivateAccountScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenModels.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = ""; }; @@ -2056,6 +2094,7 @@ C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; + C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = ""; }; C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenModels.swift; sourceTree = ""; }; C733D11B421CFE3A657EF230 /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -2092,6 +2131,7 @@ CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + D01FD1171FF40E34D707FD00 /* BigIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigIcon.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; @@ -2130,7 +2170,6 @@ D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = ""; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = ""; }; - D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInviteCell.swift; sourceTree = ""; }; @@ -2138,7 +2177,6 @@ D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = PreviewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = ""; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = ""; }; - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationService.swift; sourceTree = ""; }; DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = ""; }; DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = ""; }; DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = ""; }; @@ -2149,7 +2187,6 @@ DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = ""; }; DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelTests.swift; sourceTree = ""; }; - DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabelItem.swift; sourceTree = ""; }; DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = ""; }; DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = ""; }; DF17EA323AD0205A6AB621AA /* Snapshotting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Snapshotting.swift; sourceTree = ""; }; @@ -2178,7 +2215,9 @@ E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = ""; }; E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = ""; }; E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = ""; }; + E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelTests.swift; sourceTree = ""; }; E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = ""; }; + E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemIdentifier.swift; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; @@ -2200,6 +2239,7 @@ E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = ""; }; E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = ""; }; E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; + E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentitySDKMock.swift; sourceTree = ""; }; E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFixtures.swift; sourceTree = ""; }; E992D7B8BE54B2AB454613AF /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineItem.swift; sourceTree = ""; }; @@ -2215,6 +2255,7 @@ EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledOffsetModifier.swift; sourceTree = ""; }; + ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsFlowCoordinator.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = ""; }; @@ -2225,7 +2266,6 @@ ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; - EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = ""; }; EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; @@ -2263,6 +2303,7 @@ F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = ""; }; F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderProtocol.swift; sourceTree = ""; }; F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModel.swift; sourceTree = ""; }; + F6B676B4866F5B383DE819B2 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockUITests.swift; sourceTree = ""; }; F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderMock.swift; sourceTree = ""; }; @@ -2805,7 +2846,6 @@ 2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */ = { isa = PBXGroup; children = ( - D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */, BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */, 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */, E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */, @@ -2887,11 +2927,16 @@ children = ( 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */, 3BAC027034248429A438886B /* AppMediatorMock.swift */, + 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */, + 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */, E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */, - 1C7A6BBC686B1F840FA807FB /* EventTimelineItemSDKMock.swift */, + 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */, + 3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */, 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */, + 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */, + 6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */, 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */, @@ -2902,12 +2947,14 @@ FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */, D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */, F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */, + 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */, 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */, - A4A1003A0F7A1DFB47F4E2D0 /* TimelineItemMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */, + 9EB9BA2F30EB8C33226D8FF1 /* UserSessionStoreMock.swift */, B23135B06B044CB811139D2F /* Generated */, + E5E545F92D01588360A9BAC5 /* SDK */, ); path = Mocks; sourceTree = ""; @@ -2928,8 +2975,8 @@ CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */, 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */, C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */, + D01FD1171FF40E34D707FD00 /* BigIcon.swift */, 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */, - 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */, FFECCE59967018204876D0A5 /* LocationMarkerView.swift */, @@ -2937,11 +2984,12 @@ 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */, + 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */, 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */, 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, - DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */, AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */, E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */, + AD529C89924EE32CE307F36F /* VisualListItem.swift */, ); path = Views; sourceTree = ""; @@ -3047,10 +3095,9 @@ 39557ADF21345E18F3865B9E /* Emojis */ = { isa = PBXGroup; children = ( - 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */, - 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */, 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */, 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */, + 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */, ); path = Emojis; sourceTree = ""; @@ -3179,7 +3226,9 @@ children = ( 0E95B3BDB80531C85CD50AE6 /* InvitedRoomProxy.swift */, 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */, + 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */, B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */, + 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */, 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */, 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */, 2C0F49BD446849654C0D24E0 /* RoomMember */, @@ -3249,6 +3298,7 @@ 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */, 62B07B296D7A9D2F09120853 /* OrderedSet.swift */, D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */, + 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */, 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */, 7310D8DFE01AF45F0689C3AA /* Publisher.swift */, 584A61D9C459FAFEF038A7C0 /* Section.swift */, @@ -3396,6 +3446,7 @@ A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */, C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */, D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */, + A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */, 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, @@ -3493,6 +3544,8 @@ 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */, A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */, 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */, + A07B011547B201A836C03052 /* EncryptionResetFlowCoordinator.swift */, + ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */, C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */, A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, @@ -3790,6 +3843,7 @@ 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */, C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */, 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */, + 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */, 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */, 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */, 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */, @@ -3816,7 +3870,7 @@ 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, - A05707BF550D770168A406DB /* LoginViewModelTests.swift */, + 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */, @@ -3858,7 +3912,7 @@ 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */, 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */, - EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, + E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */, 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */, A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */, DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */, @@ -4006,8 +4060,8 @@ 79023E5904B155E8E2B8B502 /* View */ = { isa = PBXGroup; children = ( - 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, + 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, 464C6BFAA853DC755B9C1F60 /* PinnedItemsBanner */, ); @@ -4405,7 +4459,8 @@ 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */, - 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, + BE4C76F31A382B8E4DD07583 /* EncryptionResetUITests.swift */, + 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */, 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, 45571C2EBD98ED7E0CEA7AF7 /* RoomRolesAndPermissionsUITests.swift */, @@ -4614,6 +4669,7 @@ A722D372674EE5687E1A67E4 /* View */ = { isa = PBXGroup; children = ( + 1A1265FAF2C0AF1C30605BE7 /* SessionVerificationRequestDetailsView.swift */, 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */, ); path = View; @@ -4651,9 +4707,9 @@ isa = PBXGroup; children = ( 0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */, + B655A536341D2695158C6664 /* AuthenticationClientBuilderFactory.swift */, F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */, 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */, - DA38899517F08FE2AF34EB45 /* MockAuthenticationService.swift */, A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */, ); path = Authentication; @@ -4683,9 +4739,12 @@ 9A2AC7BE17C05CF7D2A22338 /* landscape_test_video.mov */, AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */, F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */, + 9C2BCB402FEB0FA1A54BEF4B /* test_animated_image.gif */, + F6B676B4866F5B383DE819B2 /* test_apple_image.heic */, D5E26C54362206BBDD096D83 /* test_audio.mp3 */, C733D11B421CFE3A657EF230 /* test_image.png */, 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */, + 7229371D48BE92239D852C1B /* test_rotated_image.jpg */, 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */, ); path = Media; @@ -4855,7 +4914,6 @@ 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, - B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */, B1E227F34BE43B08E098796E /* TestablePreview.swift */, @@ -4969,7 +5027,6 @@ F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */, 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */, D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */, - 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */, ); path = Provider; sourceTree = ""; @@ -5243,6 +5300,16 @@ path = Screens; sourceTree = ""; }; + E5E545F92D01588360A9BAC5 /* SDK */ = { + isa = PBXGroup; + children = ( + 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */, + 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */, + E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */, + ); + path = SDK; + sourceTree = ""; + }; E600AACDF87CDBCE32683236 /* Resources */ = { isa = PBXGroup; children = ( @@ -5453,6 +5520,7 @@ 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */, 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */, 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */, + E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */, 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */, 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */, F9E543072DE58E751F028998 /* TimelineProxy.swift */, @@ -5737,6 +5805,7 @@ en, es, et, + fa, fr, hu, id, @@ -5779,7 +5848,7 @@ E2F3DA35D462724CCC61DE2C /* XCRemoteSwiftPackageReference "swift-ogg" */, 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */, EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */, - 44FA555384AD79668D886043 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, + EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, ); projectDirPath = ""; projectRoot = ""; @@ -5850,9 +5919,12 @@ 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */, 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */, 35E975CFDA60E05362A7CF79 /* target.yml in Resources */, + 3E23BB48F91485D893D0A429 /* test_animated_image.gif in Resources */, + 9EF9773DBE3F6497A25CE236 /* test_apple_image.heic in Resources */, 87CEDB8A0696F0D5AE2ABB28 /* test_audio.mp3 in Resources */, 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */, E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */, + 31A27DD23C8637A0EBA76AFB /* test_rotated_image.jpg in Resources */, CBB4F39A1309F7281AE7AA8E /* test_voice_message.m4a in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6019,7 +6091,6 @@ 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */, 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */, E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */, - 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */, 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */, 30CC4F796B27BE8B1DFDBF5A /* NSEUserSession.swift in Sources */, 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */, @@ -6033,6 +6104,7 @@ 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */, 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */, F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */, + 76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */, 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */, 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */, 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */, @@ -6043,6 +6115,7 @@ E0FB26262689F04D66A949D7 /* TestablePreview.swift in Sources */, 24DF253C18D3E2C56DD0E597 /* TracingConfiguration.swift in Sources */, DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */, + 45D6DC594816288983627484 /* UITestsScreenIdentifier.swift in Sources */, 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */, 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */, 06B55882911B4BF5B14E9851 /* URL.swift in Sources */, @@ -6072,6 +6145,7 @@ C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */, 3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */, 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */, + 92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */, 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */, CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */, 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */, @@ -6100,7 +6174,7 @@ 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, - 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, + 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */, @@ -6149,7 +6223,7 @@ 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */, - 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, + 67ECD32538F6DAFE38A623F9 /* ServerSelectionScreenViewModelTests.swift in Sources */, CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */, 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */, 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */, @@ -6278,6 +6352,9 @@ 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */, 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */, 8A6CB15C8FC68F557750BF54 /* AuthenticationClientBuilder.swift in Sources */, + 210DB40676DF2A23E69C2D06 /* AuthenticationClientBuilderFactory.swift in Sources */, + A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */, + 0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */, 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */, 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, 56DACDD379A86A1F5DEFE7BE /* AuthenticationServiceProtocol.swift in Sources */, @@ -6293,6 +6370,7 @@ D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, 7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */, A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */, + FC0EEFF630F34899953BB950 /* BigIcon.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, EB9F4688006B52E69DF5358F /* BlankFormCoordinator.swift in Sources */, 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */, @@ -6333,6 +6411,7 @@ 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, + 41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */, 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */, 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, @@ -6390,9 +6469,7 @@ 370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */, 3F997171C3C79A45E92BF9EF /* ElementWellKnown.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, - 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */, E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */, - D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */, 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */, 340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */, C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */, @@ -6401,6 +6478,7 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, + A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */, 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */, 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */, 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */, @@ -6410,6 +6488,7 @@ 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */, FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */, 46A6DB0F78FB399BD59E2D41 /* EncryptionKeyProviderProtocol.swift in Sources */, + 64F8590F4BEE4DA231F97D83 /* EncryptionResetFlowCoordinator.swift in Sources */, 0C6DF318E9C8F6461E6ABDE7 /* EncryptionResetPasswordScreen.swift in Sources */, 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */, B1B255CE0E4306DD6E09D936 /* EncryptionResetPasswordScreenModels.swift in Sources */, @@ -6420,10 +6499,11 @@ 97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */, EC3320639828BED8B3E5F2C6 /* EncryptionResetScreenViewModel.swift in Sources */, A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */, + 4D2B54233C7B2C04B4ABE55A /* EncryptionSettingsFlowCoordinator.swift in Sources */, 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, - EE57A96130DD8DB053790AE2 /* EventTimelineItemSDKMock.swift in Sources */, + 2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, @@ -6443,13 +6523,13 @@ D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */, 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */, E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */, - D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */, 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, 77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */, 22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */, + 86DFA58FBBEB0AF671D2A1E1 /* HomeScreenKnockedCell.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */, 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */, @@ -6467,6 +6547,7 @@ D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */, 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */, AADE7C2497A7B55D8BED7BD6 /* IdentityConfirmedScreenViewModelProtocol.swift in Sources */, + FFD52DCDA6962055A363CC8F /* IdentityResetHandleSDKMock.swift in Sources */, BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */, 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */, B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */, @@ -6481,6 +6562,7 @@ F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */, A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */, 89B909AC66B96FA054EF3C14 /* InvitedRoomProxy.swift in Sources */, + 07376A5274822EB45CC320C7 /* InvitedRoomProxyMock.swift in Sources */, 6A54F52443EC52AC5CD772C0 /* JoinRoomScreen.swift in Sources */, AFE2AB612A1460E49578D746 /* JoinRoomScreenCoordinator.swift in Sources */, DEDBD3E9CFCC9F20CAC79881 /* JoinRoomScreenModels.swift in Sources */, @@ -6492,6 +6574,8 @@ 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */, FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */, CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */, + C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */, + 6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */, 454F8DDC4442C0DE54094902 /* LABiometryType.swift in Sources */, E468CC731C3F4D678499E52F /* LAContextMock.swift in Sources */, D5681C80D8281560AACE0035 /* Label.swift in Sources */, @@ -6543,6 +6627,7 @@ F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */, 762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */, B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */, + 478C363F63A5DFC3C83E334C /* MediaProviderMock.swift in Sources */, 30CC1DB7CE357659C82AA115 /* MediaProviderProtocol.swift in Sources */, 5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */, C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */, @@ -6562,10 +6647,7 @@ F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */, C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */, C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */, - 32FC143630CE22A9E403370B /* MockAuthenticationService.swift in Sources */, - B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, - B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */, @@ -6648,7 +6730,7 @@ 128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */, 1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, - FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */, + 6793E75E3EBE48EBB8F857AF /* ProcessInfo.swift in Sources */, 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */, C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */, 9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */, @@ -6719,7 +6801,8 @@ 2814E7075BF3A5C0CCBC9F90 /* RoomDirectorySearchView.swift in Sources */, 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */, D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */, - 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */, + 9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */, + 5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */, 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */, F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */, 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */, @@ -6769,6 +6852,7 @@ F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */, C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */, A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */, + E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */, 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, @@ -6793,7 +6877,6 @@ B272E5D1DE8BDA87A6B7A696 /* RoomTimelineProviderMock.swift in Sources */, 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */, B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */, - BD11E639CF566A9DA8FCA717 /* RoundedLabelItem.swift in Sources */, 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */, D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */, 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */, @@ -6806,6 +6889,7 @@ 67160204A8D362BB7D4AD259 /* Search.swift in Sources */, 339BC18777912E1989F2F17D /* Section.swift in Sources */, F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */, + 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */, 0C88044649BAEE6C49BFC43A /* SecureBackupControllerProtocol.swift in Sources */, 21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */, 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */, @@ -6844,6 +6928,7 @@ 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */, AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */, 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */, + 91D1A46A733EC24C081DD353 /* SessionVerificationRequestDetailsView.swift in Sources */, 707E49BE07E8EB8A13C0EB1E /* SessionVerificationScreen.swift in Sources */, D02DEB36D32A72A1B365E452 /* SessionVerificationScreenCoordinator.swift in Sources */, 5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */, @@ -6905,11 +6990,11 @@ E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */, 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */, A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */, + 877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */, C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */, 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */, AE07F215EBC2B9CBF17AA54B /* TimelineItemMenuAction.swift in Sources */, 12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */, - 1C815DD79B401DEBA2914773 /* TimelineItemMock.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */, F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */, @@ -6958,6 +7043,7 @@ 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */, 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */, 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */, + C7B07EBA0F12B5912DA9BB97 /* UserIdentitySDKMock.swift in Sources */, 988BA75A182738150894A23F /* UserIndicator.swift in Sources */, C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */, 3467FEE8210D301FF1B77001 /* UserIndicatorControllerMock.swift in Sources */, @@ -6982,11 +7068,13 @@ 6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */, 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */, + 79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */, AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */, 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, 2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */, 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */, + EED33AFD9334EFD7398707A6 /* VisualListItem.swift in Sources */, 1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */, 4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */, 4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */, @@ -7026,7 +7114,8 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */, - 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, + CACD1352927336F01FC76612 /* EncryptionResetUITests.swift in Sources */, + 230981086F0199F913434D6B /* EncryptionSettingsUITests.swift in Sources */, 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, D29E046C1E3045E0346C479D /* RoomRolesAndPermissionsUITests.swift in Sources */, @@ -7097,6 +7186,7 @@ 7447C0AD7EF302CD027D6230 /* en */, 6722709BD6178E10B70C9641 /* es */, F3C7252B3461D06175D975A4 /* et */, + C715CFE00686DACA59D836EA /* fa */, CEE20623EB4A9B88FB29F2BA /* fr */, D196116D2DD3F2757D45FCB7 /* hu */, 330AF4D121C3396F7A14B21D /* id */, @@ -7156,6 +7246,7 @@ CACA846B3E3E9A521D98B178 /* en */, CBBCC6E74774E79B599625D0 /* es */, A443FAE2EE820A5790C35C8D /* et */, + A9873374E72AA53260AE90A2 /* fa */, CC680E0E79D818706CB28CF8 /* fr */, 624244C398804ADC885239AA /* hu */, EF98A02DED04075F7CF0C721 /* id */, @@ -7403,10 +7494,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER)"; - MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 1.8.4; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.9.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCTION_APP_NAME = Element; @@ -7480,10 +7571,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER)"; - MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 1.8.4; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MARKETING_VERSION = 1.9.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -7726,16 +7817,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzysztofzablocki/KZFileWatchers"; requirement = { - branch = master; - kind = branch; - }; - }; - 44FA555384AD79668D886043 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/matrix-org/matrix-rich-text-editor-swift"; - requirement = { - kind = exactVersion; - version = 2.37.7; + kind = upToNextMinorVersion; + minimumVersion = 1.2.0; }; }; 4BDA7F6042968E8422470F3F /* XCRemoteSwiftPackageReference "LoremSwiftum" */ = { @@ -7775,7 +7858,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.52; + version = 1.0.65; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { @@ -7783,7 +7866,7 @@ repositoryURL = "https://github.com/nicklockwood/GZIP"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.3.2; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { @@ -7791,7 +7874,7 @@ repositoryURL = "https://github.com/matrix-org/emojibase-bindings"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 1.0.0; + minimumVersion = 1.3.3; }; }; 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { @@ -7815,7 +7898,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-analytics-events"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.25.0; + minimumVersion = 0.28.0; }; }; C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */ = { @@ -7839,7 +7922,7 @@ repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 7.6.0; + minimumVersion = 8.0.3; }; }; D5F7D47BBAAE0CF1DDEB3034 /* XCRemoteSwiftPackageReference "DeviceKit" */ = { @@ -7847,7 +7930,7 @@ repositoryURL = "https://github.com/devicekit/DeviceKit"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 5.2.2; + minimumVersion = 5.5.0; }; }; E025F19D013D9BA6C58B37F4 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { @@ -7879,7 +7962,15 @@ repositoryURL = "https://github.com/mxcl/Version"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 2.0.0; + minimumVersion = 2.1.0; + }; + }; + EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/element-hq/matrix-rich-text-editor-swift"; + requirement = { + kind = exactVersion; + version = 2.37.12; }; }; F71C70A4404CC6D9C4AF35F2 /* XCRemoteSwiftPackageReference "compound-ios" */ = { @@ -7887,7 +7978,7 @@ repositoryURL = "https://github.com/element-hq/compound-ios"; requirement = { kind = revision; - revision = 92110afc158ac6ee7c68d5e975144bafa6c58396; + revision = e3f9665621872f60d3652579c3f0dc7bf806e72c; }; }; F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = { @@ -8158,7 +8249,7 @@ }; CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */ = { isa = XCSwiftPackageProductDependency; - package = 44FA555384AD79668D886043 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */; + package = EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */; productName = WysiwygComposer; }; CCE5BF78B125320CBF3BB834 /* PostHog */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index aedc0764c2..4cd886738e 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "52385e2a478cc9455693d0b93dc33b988ac1d4742acceeaee288944dff8b78e0", + "originHash" : "f9011692b20e61e6f0df94b6e0946a4c8f4d58429a88998f249712bb1fee47f1", "pins" : [ { "identity" : "compound-design-tokens", "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/compound-design-tokens", "state" : { - "revision" : "2af7bb571eb30cbfbd67cdda6617500507ef46aa", - "version" : "1.8.0" + "revision" : "976db67b849775799b4153e7894d61e90fc96888", + "version" : "1.9.0" } }, { @@ -15,7 +15,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/compound-ios", "state" : { - "revision" : "92110afc158ac6ee7c68d5e975144bafa6c58396" + "revision" : "e3f9665621872f60d3652579c3f0dc7bf806e72c" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/devicekit/DeviceKit", "state" : { - "revision" : "fe41d18eccd92a115cffaa35dfff03018c67e635", - "version" : "5.2.2" + "revision" : "7ff5331960151aec74fb422e1d45f54ef6cc086d", + "version" : "5.5.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/emojibase-bindings", "state" : { - "revision" : "6ca06fefac9329fece851a2a4df7979b1699970a", - "version" : "1.0.5" + "revision" : "d4682a2ad5e68cfd2f41544c3c6c970b4d524bd1", + "version" : "1.3.3" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/GZIP", "state" : { - "revision" : "bd55f1d89e71ae3f481da74cd4adeadbb849620c", - "version" : "1.3.1" + "revision" : "f710a37aa978a93b815a4f64bd504dc4c3256312", + "version" : "1.3.2" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "af4be924ad984cf4d16f4ae4df424e79a443d435", - "version" : "7.6.2" + "revision" : "33696a71dd4d71f1b0f781ade11a48ba5549581a", + "version" : "8.0.3" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzysztofzablocki/KZFileWatchers", "state" : { - "branch" : "master", - "revision" : "d27a9557427d261adccdf4b566acc9d9c0fec6f4" + "revision" : "d27a9557427d261adccdf4b566acc9d9c0fec6f4", + "version" : "1.2.0" } }, { @@ -131,17 +131,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { - "revision" : "c9b40120a5f7b8ce1bab3f09f8417fdc9407f006", - "version" : "0.25.0" + "revision" : "632f4266d5ebd5b87b9eb52522f5117723fcd338", + "version" : "0.28.0" } }, { "identity" : "matrix-rich-text-editor-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/matrix-org/matrix-rich-text-editor-swift", + "location" : "https://github.com/element-hq/matrix-rich-text-editor-swift", "state" : { - "revision" : "30909decc48be8e06c93eacffa4ec71b539888a9", - "version" : "2.37.7" + "revision" : "b6583a47b5d14d2dc8405a0303ebd4041b877707", + "version" : "2.37.12" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "11c6d99d62035b02f4c448daebff4d63c962da20", - "version" : "1.0.52" + "revision" : "399cc70987856c73e24b8888ac1ecc0eecf1716b", + "version" : "1.0.65" } }, { @@ -284,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mxcl/Version", "state" : { - "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", - "version" : "2.0.1" + "revision" : "303a0f916772545e1e8667d3104f83be708a723c", + "version" : "2.1.0" } } ], diff --git a/ElementX/Resources/Localizations/be.lproj/Localizable.strings b/ElementX/Resources/Localizations/be.lproj/Localizable.strings index 299b8b732a..5e496bb337 100644 --- a/ElementX/Resources/Localizations/be.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/be.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Паўза"; "a11y_pin_field" = "Поле PIN-кода"; "a11y_play" = "Прайграць"; -"a11y_poll" = "Апытанне"; "a11y_poll_end" = "Апытанне скончана"; "a11y_react_with" = "Рэагаваць з %1$@"; "a11y_react_with_other_emojis" = "Рэагаваць з іншымі эмодзі"; @@ -33,14 +32,15 @@ "action_close" = "Закрыць"; "action_complete_verification" = "Праверка завершана"; "action_confirm" = "Пацвердзіць"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Пацвердзіць пароль"; "action_continue" = "Працягнуць"; "action_copy" = "Капіраваць"; "action_copy_link" = "Скапіраваць спасылку"; "action_copy_link_to_message" = "Скапіраваць спасылку на паведамленне"; "action_create" = "Стварыць"; "action_create_a_room" = "Стварыце пакой"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Дэактываваць"; +"action_deactivate_account" = "Дэактываваць уліковы запіс"; "action_decline" = "Адхіліць"; "action_delete_poll" = "Выдаліць апытанне"; "action_disable" = "Адключыць"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Забылі пароль?"; "action_forward" = "Пераслаць"; "action_go_back" = "Вярнуцца"; +"action_ignore" = "Ignore"; "action_invite" = "Запрасіць"; "action_invite_friends" = "Запрасіць карыстальнікаў"; "action_invite_friends_to_app" = "Запрасіць карыстальнікаў у %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Пакінуць"; "action_leave_conversation" = "Пакінуць размову"; "action_leave_room" = "Пакінуць пакой"; +"action_load_more" = "Загрузіць больш"; "action_manage_account" = "Кіраванне ўліковым запісам"; "action_manage_devices" = "Кіраванне прыладамі"; "action_message" = "Паведамленне"; @@ -93,6 +95,7 @@ "action_send_message" = "Адправіць паведамленне"; "action_share" = "Падзяліцца"; "action_share_link" = "Абагуліць спасылку"; +"action_show" = "Паказаць"; "action_sign_in_again" = "Увайдзіце яшчэ раз"; "action_signout" = "Выйсці"; "action_signout_anyway" = "Усё роўна выйсці"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Прагляд у хроніцы"; "action_view_source" = "Прагляд зыходнага кода"; "action_yes" = "Так"; -"action.load_more" = "Загрузіць больш"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Выйсці і абнавіць"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Наладзіць аднаўленне"; +"banner_migrate_to_native_sliding_sync_description" = "Ваш сервер зараз падтрымлівае новы, хутчэйшы пратакол. Выйдзіце з сістэмы і зноў увайдзіце, каб абнавіць яе. Гэта дапаможа вам пазбегнуць прымусовага выхаду з сістэмы, калі стары пратакол будзе пазней выдалены."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Ваш хатні сервер больш не падтрымлівае стары пратакол. Калі ласка, выйдзіце і ўвайдзіце зноў, каб працягнуць выкарыстанне праграмы."; +"banner_migrate_to_native_sliding_sync_title" = "Даступна абнаўленне"; +"banner_set_up_recovery_content" = "Стварыце новы ключ аднаўлення, які можна выкарыстоўваць для аднаўлення зашыфраванай гісторыі паведамленняў у выпадку страты доступу да вашых прылад."; +"banner_set_up_recovery_title" = "Наладзіць аднаўленне"; "common_about" = "Аб праграме"; "common_acceptable_use_policy" = "Палітыка дапушчальнага выкарыстання"; "common_advanced_settings" = "Пашыраныя налады"; @@ -133,10 +134,12 @@ "common_dark" = "Цёмная"; "common_decryption_error" = "Памылка расшыфроўкі"; "common_developer_options" = "Параметры распрацоўшчыка"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Прамы чат"; "common_edited_suffix" = "(Адрэдагавана)"; "common_editing" = "Рэдагаванне"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Шыфраванне ўключана"; "common_enter_your_pin" = "Увядзіце свой PIN-код"; "common_error" = "Памылка"; @@ -147,6 +150,7 @@ "common_favourited" = "Абранае"; "common_file" = "Файл"; "common_forward_message" = "Перасылка паведамлення"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Відарыс"; "common_in_reply_to" = "У адказ на %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Сучасны"; "common_mute" = "Адкл. гук"; "common_no_results" = "Вынікаў няма"; +"common_no_room_name" = "Няма назвы пакоя"; "common_offline" = "Па-за сеткай"; "common_optic_id_ios" = "Optic ID"; "common_or" = "або"; @@ -170,6 +175,8 @@ "common_permalink" = "Пастаянная спасылка"; "common_permission" = "Дазвол"; "common_please_wait" = "Калі ласка, пачакайце…"; +"common_poll_end_confirmation" = "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?"; +"common_poll_summary" = "Апытанне: %1$@"; "common_poll_total_votes" = "Усяго галасоў: %1$@"; "common_poll_undisclosed_text" = "Вынікі будуць паказаны пасля завяршэння апытання"; "common_privacy_policy" = "Палітыка прыватнасці"; @@ -200,6 +207,7 @@ "common_settings" = "Налады"; "common_shared_location" = "Абагулена месцазнаходжанне"; "common_signing_out" = "Выхад"; +"common_something_went_wrong" = "Нешта пайшло не так"; "common_starting_chat" = "Пачатак чата…"; "common_sticker" = "Стыкер"; "common_success" = "Поспех"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Пра што гэты пакой?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Немагчыма расшыфраваць"; +"common_unable_to_decrypt_no_access" = "У вас няма доступу да гэтага паведамлення"; "common_unable_to_invite_message" = "Не ўдалося адправіць запрашэнні аднаму або некалькім карыстальнікам."; "common_unable_to_invite_title" = "Немагчыма адправіць запрашэнне(я)"; "common_unlock" = "Разблакіраваць"; @@ -221,23 +230,30 @@ "common_username" = "Імя карыстальніка"; "common_verification_cancelled" = "Праверка адменена"; "common_verification_complete" = "Праверка завершана"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Праверце прыладу"; +"common_verify_identity" = "Verify identity"; "common_video" = "Відэа"; "common_voice_message" = "Галасавое паведамленне"; "common_waiting" = "Чакаем…"; "common_waiting_for_decryption_key" = "Чакаю гэта паведамленне"; +"common.copied_to_clipboard" = "Скапіравана ў буфер абмену"; "common.do_not_show_this_again" = "Не паказваць гэта зноў"; "common.open_source_licenses" = "Ліцэнзіі з адкрытым зыходным кодам"; "common.pinned" = "Замацаваны"; "common.send_to" = "Адправіць"; -"common_no_room_name" = "Няма назвы пакоя"; -"common_poll_end_confirmation" = "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?"; -"common_poll_summary" = "Апытанне: %1$@"; -"common_something_went_wrong" = "Нешта пайшло не так"; -"common_unable_to_decrypt_no_access" = "У вас няма доступу да гэтага паведамлення"; -"common_verify_device" = "Праверце прыладу"; +"common.you" = "Вы"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Увядзіце ключ аднаўлення"; "crash_detection_dialog_content" = "Пры апошнім выкарыстанні %1$@ адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Каб дазволіць праграме выкарыстоўваць камеру, дайце дазвол у наладах сістэмы."; "dialog_permission_generic" = "Калі ласка, дайце дазвол у наладах сістэмы."; "dialog_permission_location_description_ios" = "Дайце доступ у Наладах -> Месца знаходжання."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Усмешкі & Удзельнікі"; "emoji_picker_category_places" = "Падарожжы & Месцы"; "emoji_picker_category_symbols" = "Сімвалы"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Ваш хатні сервер неабходна абнавіць для падтрымкі Matrix Authentication Service і стварэння ўліковага запісу."; "error_failed_creating_the_permalink" = "Не атрымалася стварыць пастаянную спасылку"; "error_failed_loading_map" = "%1$@ не атрымалася загрузіць карту. Калі ласка паспрабуйце зноў пазней."; "error_failed_loading_messages" = "Не ўдалося загрузіць паведамленні"; @@ -269,7 +285,7 @@ "error_some_messages_have_not_been_sent" = "Некаторыя паведамленні не былі адпраўлены"; "error_unknown" = "Выбачце, адбылася памылка"; "event_shield_reason_authenticity_not_guaranteed" = "Сапраўднасць гэтага зашыфраванага паведамлення не можа быць гарантаваная на гэтай прыладзе."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; +"event_shield_reason_previously_verified" = "Зашыфравана раней правераным карыстальнікам."; "event_shield_reason_sent_in_clear" = "Не зашыфраваны."; "event_shield_reason_unknown_device" = "Зашыфравана невядомай ці выдаленай прыладай."; "event_shield_reason_unsigned_device" = "Зашыфравана прыладай, не пацверджанай яе ўладальнікам."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Ціхія апавяшчэнні"; "notification_incoming_call" = "Уваходны званок"; "notification_inline_reply_failed" = "** Не атрымалася даслаць - калі ласка, адкрыйце пакой"; -"notification_invitation_action_reject" = "Адхіліць"; "notification_invite_body" = "Запрасіў(-ла) вас у чат"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ запрасіў(-ла) вас у чат"; "notification_mentioned_you_body" = "Згадаў(-ла) вас: %1$@"; "notification_new_messages" = "Новыя паведамленні"; "notification_reaction_body" = "Адрэагаваў(-ла) на %1$@"; "notification_room_invite_body" = "Запрасіў(-ла) вас далучыцца да пакоя"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ запрасіў(-ла) вас далучыцца да пакоя"; "notification_sender_me" = "Я"; "notification_sender_mention_reply" = "%1$@ mentioned or replied"; "notification_test_push_notification_content" = "Вы праглядаеце апавяшчэнне! Націсніце мяне!"; @@ -329,14 +344,29 @@ "rich_text_editor_unindent" = "Без водступу"; "rich_text_editor_url_placeholder" = "Спасылка"; "rich_text_editor_a11y_add_attachment" = "Дадаць далучэнне"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Карыстальніцкі URL сервера Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Усталюйце карыстальніцкі асноўны URL для Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Адрас пазначаны няправільна, пераканайцеся, што вы ўказалі пратакол (http/https) і правільны адрас."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Хто заўгодна"; +"screen_create_room_access_section_header" = "Доступ у пакой"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Папрасіце далучыцца"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Націсніце на паведамленне і абярыце «%1$@ », каб уключыць сюды."; "screen_pinned_timeline_empty_state_headline" = "Замацуеце важныя паведамленні, каб іх можна было лёгка знайсці"; -"screen_pinned_timeline_screen_title_empty" = "Замацаваныя паведамленні"; "screen_reset_encryption_password_error" = "Адбылася невядомая памылка. Калі ласка, праверце правільнасць пароля вашага ўліковага запісу і паўтарыце спробу."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Адклікаць праверку і адправіць"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; "screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Усё роўна адправіць паведамленне"; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Загрузка паведамлення…"; "screen_room_pinned_banner_view_all_button_title" = "Паглядзець усе"; "screen_room_details_pinned_events_row_title" = "Замацаваныя паведамленні"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Змяніць правайдара ўліковага запісу"; "screen_account_provider_form_hint" = "Адрас хатняга сервера"; "screen_account_provider_form_notice" = "Увядзіце пошукавы запыт або адрас дамена."; "screen_account_provider_form_subtitle" = "Пошук кампаніі, супольнасці або прыватнага сервера."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Вы збіраецеся стварыць уліковы запіс на %@"; "screen_advanced_settings_developer_mode" = "Рэжым распрацоўшчыка"; "screen_advanced_settings_developer_mode_description" = "Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Адключыць рэдактар фарматаванага тэксту і ўключыць Markdown."; "screen_advanced_settings_send_read_receipts" = "Апавяшчэнні аб чытанні"; "screen_advanced_settings_send_read_receipts_description" = "Калі выключыць, вашы пасведчанні аб прачытанні нікому не будуць адпраўляцца. Вы па-ранейшаму будзеце атрымліваць пасведчанні аб прачытанні ад іншых карыстальнікаў."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Уключыце рэзервовае капіраванне"; "screen_chat_backup_key_backup_description" = "Рэзервовае капіраванне гарантуе, што вы не страціце сваю гісторыю паведамленняў. %1$@."; "screen_chat_backup_key_backup_title" = "Рэзервовая копія"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Змяніць ключ аднаўлення"; -"screen_chat_backup_recovery_action_confirm" = "Увядзіце ключ аднаўлення"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Ваша рэзервовая копія чата зараз не сінхранізавана."; -"screen_chat_backup_recovery_action_setup" = "Наладзьце аднаўленне"; "screen_chat_backup_recovery_action_setup_description" = "Атрымайце доступ да зашыфраваных паведамленняў, калі вы страціце ўсе свае прылады або выйдзеце з сістэмы %1$@ усюды."; "screen_create_account_title" = "Стварыць уліковы запіс"; "screen_create_new_recovery_key_list_item_1" = "Адкрыйце %1$@ на настольнай прыладзе"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Паказаць вынікі толькі пасля заканчэння апытання"; "screen_create_poll_anonymous_headline" = "Схаваць галасы"; "screen_create_poll_answer_hint" = "Варыянт %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Вашы змены не будуць захаваны"; "screen_create_poll_cancel_confirmation_title_ios" = "Адмяніць апытанне"; "screen_create_poll_question_desc" = "Пытанне або тэма"; "screen_create_poll_question_hint" = "Пра што апытанне?"; @@ -459,17 +492,17 @@ "screen_create_room_public_option_description" = "Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней."; "screen_create_room_public_option_title" = "Публічны пакой (для ўсіх)"; "screen_create_room_topic_label" = "Тэма (неабавязкова)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; +"screen_deactivate_account_confirmation_dialog_content" = "Калі ласка, пацвердзіце, што вы хочаце дэактываваць свой уліковы запіс. Гэта дзеянне нельга адмяніць."; +"screen_deactivate_account_delete_all_messages" = "Выдаліць усе мае паведамленні"; +"screen_deactivate_account_delete_all_messages_notice" = "Увага: будучыя карыстальнікі могуць бачыць няпоўныя размовы."; "screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; +"screen_deactivate_account_description_bold_part" = "незваротны"; "screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; +"screen_deactivate_account_list_item_1_bold_part" = "Назаўсёды адключыць"; +"screen_deactivate_account_list_item_2" = "Выдаліць вас з усіх чатаў."; +"screen_deactivate_account_list_item_3" = "Выдаліце інфармацыю аб сваім уліковым запісе з нашага сервера ідэнтыфікацыі."; "screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_title" = "Дэактываваць уліковы запіс"; "screen_edit_poll_delete_confirmation" = "Вы ўпэўнены, што хочаце выдаліць гэтае апытанне?"; "screen_edit_profile_display_name" = "Бачнае імя"; "screen_edit_profile_display_name_placeholder" = "Ваша бачнае імя"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Групавыя чаты"; "screen_notification_settings_invite_for_me_label" = "Запрашэнні"; "screen_notification_settings_mentions_only_disclaimer" = "Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."; -"screen_notification_settings_mentions_section_title" = "Згадванні"; "screen_notification_settings_mode_all" = "Усе"; "screen_notification_settings_mode_mentions" = "Згадванні"; "screen_notification_settings_notification_section_title" = "Апавясціць мяне"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Выберыце %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Звязаць новую прыладу”"; "screen_qr_code_login_initial_state_item_4" = "Адсканіруйце QR-код з дапамогай гэтай прылады"; +"screen_qr_code_login_initial_state_subtitle" = "Даступна толькі ў тым выпадку, калі ваш правайдар уліковага запісу гэта падтрымлівае."; "screen_qr_code_login_initial_state_title" = "Адкрыйце %1$@ на іншай прыладзе, каб атрымаць QR-код"; "screen_qr_code_login_invalid_scan_state_description" = "Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Няправільны QR-код"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Ваш код спраўджання"; "screen_recovery_key_change_description" = "Атрымайце новы ключ аднаўлення, калі вы страцілі існуючы. Пасля змены ключа аднаўлення ваш стары больш не будзе працаваць."; "screen_recovery_key_change_generate_key" = "Стварыць новы ключ аднаўлення"; -"screen_recovery_key_change_generate_key_description" = "Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы"; "screen_recovery_key_change_success" = "Ключ аднаўлення зменены"; "screen_recovery_key_change_title" = "Змяніць ключ аднаўлення?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Стварыць новы ключ аднаўлення"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Увесці..."; "screen_recovery_key_confirm_lost_recovery_key" = "Страцілі ключ аднаўлення?"; "screen_recovery_key_confirm_success" = "Ключ аднаўлення пацверджаны"; -"screen_recovery_key_confirm_title" = "Увядзіце ключ аднаўлення"; "screen_recovery_key_copied_to_clipboard" = "Ключ аднаўлення скапіраваны"; "screen_recovery_key_generating_key" = "Стварэнне…"; "screen_recovery_key_save_action" = "Захаваць ключ аднаўлення"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Так, скінуць зараз"; "screen_reset_encryption_confirmation_alert_subtitle" = "Гэты працэс незваротны."; "screen_reset_encryption_confirmation_alert_title" = "Вы ўпэўнены, што хочаце скінуць шыфраванне?"; -"screen_reset_encryption_password_placeholder" = "Увод..."; "screen_reset_encryption_password_subtitle" = "Пацвердзіце, што вы хочаце скінуць шыфраванне"; "screen_reset_encryption_password_title" = "Каб працягнуць, увядзіце пароль уліковага запісу"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Адміністратары аўтаматычна маюць права мадэратара"; "screen_room_change_role_moderators_title" = "Рэдагаваць мадэратараў"; "screen_room_change_role_unsaved_changes_description" = "У вас ёсць незахаваныя змены."; -"screen_room_change_role_unsaved_changes_title" = "Захаваць змены?"; "screen_room_details_add_topic_title" = "Дадаць тэму"; "screen_room_details_already_a_member" = "Ужо ўдзельнік"; "screen_room_details_already_invited" = "Ужо запрасілі"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Не ўдалося ўключыць гук у гэтым пакоі. Паўтарыце спробу."; "screen_room_details_notification_mode_custom" = "Уласныя"; "screen_room_details_notification_mode_default" = "Стандартныя"; -"screen_room_details_notification_title" = "Апавяшчэнні"; "screen_room_details_share_room_title" = "Падзяліцца пакоем"; "screen_room_details_title" = "Інфармацыя аб пакоі"; "screen_room_details_updating_room" = "Ідзе абнаўленне пакоя…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Разблакіраваць"; "screen_room_member_details_unblock_alert_description" = "Вы зноў зможаце ўбачыць усе паведамленні."; "screen_room_member_details_unblock_user" = "Разблакіраваць карыстальніка"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Заблакіраваць"; "screen_room_member_list_ban_member_confirmation_description" = "Яны не змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць."; "screen_room_member_list_ban_member_confirmation_title" = "Вы ўпэўнены, што хочаце заблакіраваць гэтага карыстальніка?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Блакіроўка %1$@"; "screen_room_member_list_manage_member_ban" = "Выдаліць і заблакіраваць удзельніка"; "screen_room_member_list_manage_member_remove" = "Выдаліць удзельніка з пакоя"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Выдаліць і заблакіраваць удзельніка"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Толькі выдаліць удзельніка"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Выдаліць удзельніка і забараніць далучацца ў будучыні?"; "screen_room_member_list_manage_member_unban_action" = "Разблакіраваць"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Паказаць менш"; "screen_room_timeline_message_copied" = "Паведамленне скапіравана"; "screen_room_timeline_no_permission_to_post" = "У Вас няма дазволу на публікацыю ў гэтым пакоі"; -"screen_room_timeline_reactions_show_less" = "Паказаць менш"; "screen_room_timeline_reactions_show_more" = "Паказаць больш"; "screen_room_timeline_read_marker_title" = "Новае"; "screen_room_title" = "Чат"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Пазначыць як прачытанае"; "screen_roomlist_mark_as_unread" = "Пазначыць як непрачытанае"; "screen_roomlist_room_directory_button_title" = "Праглядзець усе пакоі"; -"screen_server_confirmation_change_server" = "Змяніць правайдара ўліковага запісу"; "screen_server_confirmation_message_login_element_dot_io" = "Прыватны сервер для супрацоўнікаў Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."; "screen_server_confirmation_message_register" = "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Параўнайце лічбы"; "screen_session_verification_complete_subtitle" = "Ваш новы сеанс пацверджаны. Ён мае доступ да вашых зашыфраваных паведамленняў, і іншыя карыстальнікі будуць лічыць яго давераным."; "screen_session_verification_enter_recovery_key" = "Увядзіце ключ аднаўлення"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Дакажыце, што гэта вы, каб атрымаць доступ да вашай зашыфраванай гісторыі паведамленняў."; "screen_session_verification_open_existing_session_title" = "Адкрыйце існуючы сеанс"; "screen_session_verification_positive_button_canceled" = "Паўтарыце праверку"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Чаканне супадзення"; "screen_session_verification_ready_subtitle" = "Параўнайце ўнікальны набор эмодзі."; "screen_session_verification_request_accepted_subtitle" = "Параўнайце ўнікальныя эмодзі, пераканаўшыся, што яны размешчаны ў тым жа парадку."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Яны не супадаюць"; "screen_session_verification_they_match" = "Яны супадаюць"; "screen_session_verification_waiting_to_accept_subtitle" = "Для працягу працы прыміце запыт на запуск працэсу праверкі ў іншым сеансе."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."; "screen_signout_key_backup_disabled_title" = "Вы адключылі рэзервовае капіраванне"; "screen_signout_key_backup_offline_subtitle" = "Вашы ключы ўсё яшчэ захоўваліся, калі вы выйшлі з сеткі. Паўторна падключыцеся, каб можна было стварыць рэзервовую копію вашых ключоў перад выхадам."; -"screen_signout_key_backup_offline_title" = "Рэзервовае капіраванне ключоў усё яшчэ працягваецца"; "screen_signout_key_backup_ongoing_subtitle" = "Калі ласка, дачакайцеся завяршэння працэсу, перш чым выходзіць з сістэмы."; "screen_signout_key_backup_ongoing_title" = "Вашы ключы ўсё яшчэ ствараюцца"; "screen_signout_recovery_disabled_subtitle" = "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."; "screen_signout_recovery_disabled_title" = "Аднаўленне не наладжана"; "screen_signout_save_recovery_key_subtitle" = "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў."; -"screen_signout_save_recovery_key_title" = "Вы захавалі свой ключ аднаўлення?"; "screen_start_chat_error_starting_chat" = "Пры спробе пачаць чат адбылася памылка"; "screen_view_location_title" = "Месцазнаходжанне"; "screen_welcome_bullet_1" = "Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."; @@ -919,7 +952,6 @@ "test_language_identifier" = "be"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Выпраўленне непаладак"; -"troubleshoot_notifications_entry_point_title" = "Выпраўленне непаладак з апавяшчэннямі"; "troubleshoot_notifications_screen_action" = "Запусціць тэсты"; "troubleshoot_notifications_screen_action_again" = "Запусціце тэсты яшчэ раз"; "troubleshoot_notifications_screen_failure" = "Некаторыя тэсты не ўдаліся. Калі ласка, праглядзіце дэталі."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Пераканайцеся, што размеркавальнікі UnifiedPush даступныя."; "troubleshoot_notifications_test_unified_push_failure" = "Размеркавальнікі не знойдзены."; "troubleshoot_notifications_test_unified_push_title" = "Праверыць UnifiedPush"; +"a11y_poll" = "Апытанне"; +"banner_set_up_recovery_submit" = "Наладзьце аднаўленне"; "dialog_title_error" = "Памылка"; "dialog_title_success" = "Поспех"; "notification_fallback_content" = "Апавяшчэнне"; "notification_invitation_action_join" = "Далучыцца"; +"notification_invitation_action_reject" = "Адхіліць"; "notification_room_action_mark_as_read" = "Пазначыць як прачытанае"; "notification_room_action_quick_reply" = "Хуткі адказ"; +"screen_pinned_timeline_screen_title_empty" = "Замацаваныя паведамленні"; "screen_room_mentions_at_room_title" = "Усе"; +"screen_account_provider_change" = "Змяніць правайдара ўліковага запісу"; "screen_account_provider_signin_subtitle" = "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў."; "screen_account_provider_signup_subtitle" = "Тут будуць захоўвацца вашыя размовы - сапраўды гэтак жа, як вы выкарыстоўваеце паштовага правайдара для захоўвання сваіх лістоў."; "screen_analytics_settings_help_us_improve" = "Даваць ананімныя дадзеныя аб выкарыстанні, каб дапамагчы нам выявіць праблемы."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Вы зноў зможаце ўбачыць усе паведамленні."; "screen_blocked_users_unblock_alert_title" = "Разблакіраваць карыстальніка"; "screen_bug_report_rash_logs_alert_title" = "Пры апошнім выкарыстанні %1$@ адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?"; +"screen_chat_backup_recovery_action_confirm" = "Увядзіце ключ аднаўлення"; +"screen_chat_backup_recovery_action_setup" = "Наладзьце аднаўленне"; +"screen_create_poll_cancel_confirmation_content_ios" = "Вашы змены не будуць захаваны"; "screen_create_room_add_people_title" = "Запрасіць карыстальнікаў"; "screen_create_room_room_name_label" = "Назва пакоя"; "screen_create_room_title" = "Стварыце пакой"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Рэдагаваць апытанне"; "screen_identity_use_another_device" = "Выкарыстоўвайце іншую прыладу"; "screen_login_subtitle" = "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі."; +"screen_notification_settings_mentions_section_title" = "Згадванні"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Паўтарыць спробу"; +"screen_recovery_key_change_generate_key_description" = "Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Заблакіраваць карыстальніка"; +"screen_reset_encryption_password_placeholder" = "Увесці..."; "screen_room_attachment_source_camera_photo" = "Зрабіць фота"; "screen_room_change_permissions_everyone" = "Усе"; "screen_room_change_permissions_member_moderation" = "Мадэрацыя ўдзельнікаў"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Адміністратары"; "screen_room_change_role_section_moderators" = "Мадэратары"; "screen_room_change_role_section_users" = "Удзельнікі"; +"screen_room_change_role_unsaved_changes_title" = "Захаваць змены?"; "screen_room_details_invite_people_title" = "Запрасіць карыстальнікаў"; "screen_room_details_leave_conversation_title" = "Пакінуць размову"; "screen_room_details_leave_room_title" = "Пакінуць пакой"; +"screen_room_details_notification_title" = "Апавяшчэнні"; "screen_room_details_roles_and_permissions" = "Ролі і дазволы"; "screen_room_details_room_name_label" = "Назва пакоя"; "screen_room_details_security_title" = "Бяспека"; "screen_room_details_topic_title" = "Тэма"; "screen_room_error_failed_processing_media" = "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Выдаліць і заблакіраваць удзельніка"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Толькі згадванні і ключавыя словы"; +"screen_room_timeline_reactions_show_less" = "Паказаць менш"; "screen_roomlist_filter_people" = "Людзі"; +"screen_server_confirmation_change_server" = "Змяніць правайдара ўліковага запісу"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Выйсці"; "screen_signout_confirmation_dialog_title" = "Выйсці"; +"screen_signout_key_backup_offline_title" = "Вашы ключы ўсё яшчэ ствараюцца"; "screen_signout_preference_item" = "Выйсці"; +"screen_signout_save_recovery_key_title" = "Вы захавалі свой ключ аднаўлення?"; +"troubleshoot_notifications_entry_point_title" = "Выпраўленне непаладак з апавяшчэннямі"; diff --git a/ElementX/Resources/Localizations/bg.lproj/Localizable.strings b/ElementX/Resources/Localizations/bg.lproj/Localizable.strings index 357c221e5b..8e7a783ffe 100644 --- a/ElementX/Resources/Localizations/bg.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/bg.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Пауза"; "a11y_pin_field" = "PIN поле"; "a11y_play" = "Пускане"; -"a11y_poll" = "Анкета"; "a11y_poll_end" = "Приключила анкета"; "a11y_react_with" = "Реакция с %1$@"; "a11y_react_with_other_emojis" = "Реакция с други емоджита"; @@ -41,6 +40,7 @@ "action_create" = "Създаване"; "action_create_a_room" = "Създаване на стая"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Отхвърляне"; "action_delete_poll" = "Изтриване на анкетата"; "action_disable" = "Деактивиране"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Забравена парола?"; "action_forward" = "Препращане"; "action_go_back" = "Go back"; +"action_ignore" = "Ignore"; "action_invite" = "Поканване"; "action_invite_friends" = "Поканване на хора"; "action_invite_friends_to_app" = "Поканване на хора в %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Напускане"; "action_leave_conversation" = "Напускане на разговора"; "action_leave_room" = "Напускане на стаята"; +"action_load_more" = "Зареждане на още"; "action_manage_account" = "Управление на профила"; "action_manage_devices" = "Управление на устройства"; "action_message" = "Message"; @@ -93,6 +95,7 @@ "action_send_message" = "Изпращане на съобщение"; "action_share" = "Споделяне"; "action_share_link" = "Споделяне на връзката"; +"action_show" = "Show"; "action_sign_in_again" = "Влизане отново"; "action_signout" = "Изход"; "action_signout_anyway" = "Излизане въпреки това"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Преглед на източника"; "action_yes" = "Да"; -"action.load_more" = "Зареждане на още"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Относно"; "common_acceptable_use_policy" = "Acceptable use policy"; "common_advanced_settings" = "Разширени настройки"; @@ -133,10 +134,12 @@ "common_dark" = "Тъмен"; "common_decryption_error" = "Грешка при разшифроване"; "common_developer_options" = "Опции за разработчици"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Директен чат"; "common_edited_suffix" = "(редактирано)"; "common_editing" = "Редактиране"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Шифроването е включено"; "common_enter_your_pin" = "Въведете своя PIN"; "common_error" = "Грешка"; @@ -147,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "Файл"; "common_forward_message" = "Препращане на съобщението"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Изображение"; "common_in_reply_to" = "В отговор на %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Модерно"; "common_mute" = "Заглушаване"; "common_no_results" = "Няма резултати"; +"common_no_room_name" = "No room name"; "common_offline" = "Офлайн"; "common_optic_id_ios" = "Optic ID"; "common_or" = "или"; @@ -170,6 +175,8 @@ "common_permalink" = "Постоянна връзка"; "common_permission" = "Разрешение"; "common_please_wait" = "Please wait…"; +"common_poll_end_confirmation" = "Сигурни ли сте, че искате да приключите тази анкета?"; +"common_poll_summary" = "Анкета: %1$@"; "common_poll_total_votes" = "Общо гласове: %1$@"; "common_poll_undisclosed_text" = "Резултатите ще се покажат след приключване на анкетата"; "common_privacy_policy" = "Политика за поверителност"; @@ -200,6 +207,7 @@ "common_settings" = "Настройки"; "common_shared_location" = "Споделено местоположение"; "common_signing_out" = "Излизате"; +"common_something_went_wrong" = "Something went wrong"; "common_starting_chat" = "Започване на чат…"; "common_sticker" = "Sticker"; "common_success" = "Успешно"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "За какво се отнася тази стая?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Не може да се разшифрова"; +"common_unable_to_decrypt_no_access" = "You don't have access to this message"; "common_unable_to_invite_message" = "Invites couldn't be sent to one or more users."; "common_unable_to_invite_title" = "Не може да се изпрати покана(и)"; "common_unlock" = "Отключване"; @@ -221,23 +230,30 @@ "common_username" = "Потребителско име"; "common_verification_cancelled" = "Потвърждаването е отменено"; "common_verification_complete" = "Потвърждаването е завършено"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Потвърждаване на устройството"; +"common_verify_identity" = "Verify identity"; "common_video" = "Видео"; "common_voice_message" = "Гласово съобщение"; "common_waiting" = "Waiting…"; "common_waiting_for_decryption_key" = "В очакване на това съобщение"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Send to"; -"common_no_room_name" = "No room name"; -"common_poll_end_confirmation" = "Сигурни ли сте, че искате да приключите тази анкета?"; -"common_poll_summary" = "Анкета: %1$@"; -"common_something_went_wrong" = "Something went wrong"; -"common_unable_to_decrypt_no_access" = "You don't have access to this message"; -"common_verify_device" = "Потвърждаване на устройството"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Резервното копие на чатовете ви в момента не е синхронизирано. Въведете ключа си за възстановяване, за да потвърдите достъпа до резервното копие на чатовете си."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Потвърдете ключа си за възстановяване"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; "dialog_permission_generic" = "Please grant the permission in the system settings."; "dialog_permission_location_description_ios" = "Grant access in Settings -> Location."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Безшумни известия"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** Неуспешно изпращане - моля, отворете стаята"; -"notification_invitation_action_reject" = "Reject"; "notification_invite_body" = "Invited you to chat"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Ви спомена: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Unindent"; "rich_text_editor_url_placeholder" = "Връзка"; "rich_text_editor_a11y_add_attachment" = "Прикачване на файл"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Промяна на доставчика на акаунт"; "screen_account_provider_form_hint" = "Homeserver address"; "screen_account_provider_form_notice" = "Въведете термин за търсене или адрес на домейн."; "screen_account_provider_form_subtitle" = "Потърсете компания, общност или частен сървър."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "На път сте да създадете акаунт в %@"; "screen_advanced_settings_developer_mode" = "Developer mode"; "screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Disable the rich text editor to type Markdown manually."; "screen_advanced_settings_send_read_receipts" = "Потвърждения за прочитане"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Включване на резервните копия"; "screen_chat_backup_key_backup_description" = "Резервното копие гарантира, че няма да загубите хронологията на съобщенията си. %1$@."; "screen_chat_backup_key_backup_title" = "Резервно копие"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Промяна на ключа за възстановяване"; -"screen_chat_backup_recovery_action_confirm" = "Потвърждаване на ключа за възстановяване"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Резервното копие на чатовете ви в момента не е синхронизирано."; -"screen_chat_backup_recovery_action_setup" = "Set up recovery"; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Показване на резултатите само след приключване на анкетата"; "screen_create_poll_anonymous_headline" = "Скриване на гласовете"; "screen_create_poll_answer_hint" = "Опция %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Промените ви няма да бъдат запазени"; "screen_create_poll_cancel_confirmation_title_ios" = "Cancel Poll"; "screen_create_poll_question_desc" = "Въпрос или тема"; "screen_create_poll_question_hint" = "За какво се отнася анкетата?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Обновяване на профила…"; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "Няма покани"; "screen_invites_invited_you" = "%1$@ (%2$@) ви покани"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -509,10 +542,10 @@ "screen_key_backup_disable_confirmation_action_turn_off" = "Изключване"; "screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; "screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; -"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; -"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; -"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; -"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"; +"screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?"; "screen_login_error_deactivated_account" = "Този акаунт бе деактивиран."; "screen_login_error_invalid_credentials" = "Неправилно потребителско име и/или парола"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Групови чатове"; "screen_notification_settings_invite_for_me_label" = "Покани"; "screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; -"screen_notification_settings_mentions_section_title" = "Споменавания"; "screen_notification_settings_mode_all" = "All"; "screen_notification_settings_mode_mentions" = "Споменавания"; "screen_notification_settings_notification_section_title" = "Да бъда известяван за"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_generate_key" = "Генериране на нов ключ за възстановяване"; -"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; "screen_recovery_key_change_success" = "Recovery key changed"; "screen_recovery_key_change_title" = "Промяна на ключа за възстановяване?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Уверете се, че никой не може да види този екран!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Неправилен ключ за възстановяване"; "screen_recovery_key_confirm_key_description" = "Въведете 48-символния код."; "screen_recovery_key_confirm_key_placeholder" = "Въведете…"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Ключът за възстановяване е потвърден"; -"screen_recovery_key_confirm_title" = "Потвърдете ключа си за възстановяване"; "screen_recovery_key_copied_to_clipboard" = "Копиран ключ за възстановяване"; "screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_save_action" = "Запазване на ключа за възстановяване"; -"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_description" = "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe."; "screen_recovery_key_save_key_description" = "Tap to copy recovery key"; -"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key somewhere safe"; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; -"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_description" = "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."; "screen_recovery_key_setup_generate_key" = "Generate your recovery key"; -"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_title" = "Set up recovery"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Edit Moderators"; "screen_room_change_role_unsaved_changes_description" = "You have unsaved changes."; -"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_add_topic_title" = "Добавяне на тема"; "screen_room_details_already_a_member" = "Вече е член"; "screen_room_details_already_invited" = "Вече е бил поканен"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Failed unmuting this room, please try again."; "screen_room_details_notification_mode_custom" = "Персонализирани"; "screen_room_details_notification_mode_default" = "По подразбиране"; -"screen_room_details_notification_title" = "Известия"; "screen_room_details_share_room_title" = "Споделяне на стаята"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "Обновяване на стаята…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Отблокиране"; "screen_room_member_details_unblock_alert_description" = "You'll be able to see all messages from them again."; "screen_room_member_details_unblock_user" = "Отблокиране на потребителя"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Ban"; "screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; "screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banning %1$@"; "screen_room_member_list_manage_member_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove" = "Remove from room"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; "screen_room_member_list_manage_member_unban_action" = "Unban"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Показване на по-малко"; "screen_room_timeline_message_copied" = "Съобщението е копирано"; "screen_room_timeline_no_permission_to_post" = "You do not have permission to post to this room"; -"screen_room_timeline_reactions_show_less" = "Показване на по-малко"; "screen_room_timeline_reactions_show_more" = "Показване на повече"; "screen_room_timeline_read_marker_title" = "New"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Отбелязване като прочетено"; "screen_roomlist_mark_as_unread" = "Отбелязване като непрочетено"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "Промяна на доставчика на акаунт"; "screen_server_confirmation_message_login_element_dot_io" = "A private server for Element employees."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix е отворена мрежа за сигурна, децентрализирана комуникация."; "screen_server_confirmation_message_register" = "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Compare numbers"; "screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."; "screen_session_verification_enter_recovery_key" = "Въвеждане на ключ за възстановяване"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Докажете, че сте вие, за да получите достъп до хронологията на шифрованите си съобщения."; "screen_session_verification_open_existing_session_title" = "Отворете съществуваща сесия"; "screen_session_verification_positive_button_canceled" = "Повторен опит за потвърждаване"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "В очакване на съвпадение"; "screen_session_verification_ready_subtitle" = "Сравнете уникален набор от емоджита."; "screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Те не съвпадат"; "screen_session_verification_they_match" = "Те съвпадат"; "screen_session_verification_waiting_to_accept_subtitle" = "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; "screen_signout_key_backup_disabled_title" = "You have turned off backup"; "screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; -"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; "screen_signout_key_backup_ongoing_title" = "Your keys are still being backed up"; "screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; "screen_signout_recovery_disabled_title" = "Recovery not set up"; "screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; -"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_view_location_title" = "Местоположение"; "screen_welcome_bullet_1" = "Calls, polls, search and more will be added later this year."; @@ -919,7 +952,6 @@ "test_language_identifier" = "bg"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "Анкета"; +"banner_set_up_recovery_submit" = "Set up recovery"; "dialog_title_error" = "Грешка"; "dialog_title_success" = "Успешно"; "notification_fallback_content" = "Известие"; "notification_invitation_action_join" = "Присъединяване"; +"notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Отбелязване като прочетено"; "notification_room_action_quick_reply" = "Бърз отговор"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Everyone"; +"screen_account_provider_change" = "Промяна на доставчика на акаунт"; "screen_account_provider_signin_subtitle" = "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."; "screen_account_provider_signup_subtitle" = "Това е мястото, където ще живеят вашите разговори — точно както бихте използвали имейл доставчик, за да съхранявате вашите имейли."; "screen_analytics_settings_help_us_improve" = "Споделяне на анонимни данни за използване, за да ни помогнете да идентифицираме проблеми."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "You'll be able to see all messages from them again."; "screen_blocked_users_unblock_alert_title" = "Отблокиране на потребителя"; "screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"screen_chat_backup_recovery_action_confirm" = "Въвеждане на ключ за възстановяване"; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "Поканване на хора"; "screen_create_room_room_name_label" = "Име на стаята"; "screen_create_room_title" = "Създаване на стая"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Редактиране на анкетата"; "screen_identity_use_another_device" = "Use another device"; "screen_login_subtitle" = "Matrix е отворена мрежа за сигурна, децентрализирана комуникация."; +"screen_notification_settings_mentions_section_title" = "Споменавания"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Повторен опит"; +"screen_recovery_key_change_generate_key_description" = "Do not share this with anyone!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Блокиране на потребителя"; +"screen_reset_encryption_password_placeholder" = "Въведете…"; "screen_room_attachment_source_camera_photo" = "Снимка"; "screen_room_change_permissions_everyone" = "Everyone"; "screen_room_change_permissions_member_moderation" = "Member moderation"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Admins"; "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Членове"; +"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_invite_people_title" = "Поканване на хора"; "screen_room_details_leave_conversation_title" = "Напускане на разговора"; "screen_room_details_leave_room_title" = "Напускане на стаята"; +"screen_room_details_notification_title" = "Известия"; "screen_room_details_roles_and_permissions" = "Roles and permissions"; "screen_room_details_room_name_label" = "Име на стаята"; "screen_room_details_security_title" = "Защита"; "screen_room_details_topic_title" = "Тема"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Само споменавания и ключови думи"; +"screen_room_timeline_reactions_show_less" = "Показване на по-малко"; "screen_roomlist_filter_people" = "Хора"; +"screen_server_confirmation_change_server" = "Промяна на доставчика на акаунт"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Изход"; "screen_signout_confirmation_dialog_title" = "Изход"; +"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_preference_item" = "Изход"; +"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/cs.lproj/Localizable.strings b/ElementX/Resources/Localizations/cs.lproj/Localizable.strings index 0966a7b0cf..bc72b44767 100644 --- a/ElementX/Resources/Localizations/cs.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/cs.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pozastavit"; "a11y_pin_field" = "Pole pro PIN"; "a11y_play" = "Přehrát"; -"a11y_poll" = "Hlasování"; "a11y_poll_end" = "Hlasování ukončeno"; "a11y_react_with" = "Reagovat s %1$@"; "a11y_react_with_other_emojis" = "Reagovat s dalšími emoji"; @@ -41,6 +40,7 @@ "action_create" = "Vytvořit"; "action_create_a_room" = "Vytvořit místnost"; "action_deactivate" = "Deaktivovat"; +"action_deactivate_account" = "Deaktivovat účet"; "action_decline" = "Odmítnout"; "action_delete_poll" = "Odstranit hlasování"; "action_disable" = "Zakázat"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Zapomněli jste heslo?"; "action_forward" = "Přeposlat"; "action_go_back" = "Přejít zpět"; +"action_ignore" = "Ignorovat"; "action_invite" = "Pozvat"; "action_invite_friends" = "Pozvat přátele"; "action_invite_friends_to_app" = "Pozvat přátele do %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Odejít"; "action_leave_conversation" = "Opustit konverzaci"; "action_leave_room" = "Opustit místnost"; +"action_load_more" = "Načíst více"; "action_manage_account" = "Spravovat účet"; "action_manage_devices" = "Spravovat zařízení"; "action_message" = "Zpráva"; @@ -93,6 +95,7 @@ "action_send_message" = "Odeslat zprávu"; "action_share" = "Sdílet"; "action_share_link" = "Sdílet odkaz"; +"action_show" = "Zobrazit"; "action_sign_in_again" = "Přihlásit se znovu"; "action_signout" = "Odhlásit se"; "action_signout_anyway" = "Přesto se odhlásit"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Zobrazit na časové ose"; "action_view_source" = "Zobrazit zdroj"; "action_yes" = "Ano"; -"action.load_more" = "Načíst více"; -"action_deactivate_account" = "Deaktivovat účet"; "banner_migrate_to_native_sliding_sync_action" = "Odhlásit se a upgradovat"; "banner_migrate_to_native_sliding_sync_description" = "Váš server nyní podporuje nový, rychlejší protokol. Chcete-li upgradovat, odhlaste se a znovu se přihlaste. Pokud to uděláte nyní, pomůže vám vyhnout se nucenému odhlášení, když bude starý protokol později odstraněn."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade k dispozici"; -"banner.set_up_recovery.content" = "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."; -"banner.set_up_recovery.title" = "Nastavení obnovy"; +"banner_set_up_recovery_content" = "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."; +"banner_set_up_recovery_title" = "Nastavení obnovy"; "common_about" = "O aplikaci"; "common_acceptable_use_policy" = "Zásady používání"; "common_advanced_settings" = "Pokročilá nastavení"; @@ -133,10 +134,12 @@ "common_dark" = "Tmavé"; "common_decryption_error" = "Chyba dešifrování"; "common_developer_options" = "Možnosti pro vývojáře"; +"common_device_id" = "ID zařízení"; "common_direct_chat" = "Přímý chat"; "common_edited_suffix" = "(upraveno)"; "common_editing" = "Úpravy"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Šifrování"; "common_encryption_enabled" = "Šifrování povoleno"; "common_enter_your_pin" = "Zadejte svůj PIN"; "common_error" = "Chyba"; @@ -147,6 +150,7 @@ "common_favourited" = "Oblíbené"; "common_file" = "Soubor"; "common_forward_message" = "Přeposlat zprávu"; +"common_frequently_used" = "Často používané"; "common_gif" = "GIF"; "common_image" = "Obrázek"; "common_in_reply_to" = "V odpovědi na %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderní"; "common_mute" = "Ztlumit"; "common_no_results" = "Žádné výsledky"; +"common_no_room_name" = "Žádný název místnosti"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "nebo"; @@ -170,6 +175,8 @@ "common_permalink" = "Trvalý odkaz"; "common_permission" = "Oprávnění"; "common_please_wait" = "Počkejte prosím..."; +"common_poll_end_confirmation" = "Opravdu chcete ukončit toto hlasování?"; +"common_poll_summary" = "Hlasování: %1$@"; "common_poll_total_votes" = "Celkový počet hlasů: %1$@"; "common_poll_undisclosed_text" = "Výsledky se zobrazí po skončení hlasování"; "common_privacy_policy" = "Zásady ochrany osobních údajů"; @@ -200,6 +207,7 @@ "common_settings" = "Nastavení"; "common_shared_location" = "Sdílená poloha"; "common_signing_out" = "Odhlašování"; +"common_something_went_wrong" = "Něco se pokazilo"; "common_starting_chat" = "Zahajování chatu…"; "common_sticker" = "Nálepka"; "common_success" = "Úspěch"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "O čem je tato místnost?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Nelze dešifrovat"; +"common_unable_to_decrypt_no_access" = "Nemáte přístup k této zprávě"; "common_unable_to_invite_message" = "Pozvánky nebylo možné odeslat jednomu nebo více uživatelům."; "common_unable_to_invite_title" = "Nelze odeslat pozvánky"; "common_unlock" = "Odemknout"; @@ -221,23 +230,30 @@ "common_username" = "Uživatelské jméno"; "common_verification_cancelled" = "Ověření zrušeno"; "common_verification_complete" = "Ověření dokončeno"; +"common_verification_failed" = "Ověření se nezdařilo"; +"common_verified" = "Ověřeno"; +"common_verify_device" = "Ověřit zařízení"; +"common_verify_identity" = "Ověření totožnosti"; "common_video" = "Video"; "common_voice_message" = "Hlasová zpráva"; "common_waiting" = "Čekání..."; "common_waiting_for_decryption_key" = "Čekání na dešifrovací klíč"; +"common.copied_to_clipboard" = "Zkopírováno do schránky"; "common.do_not_show_this_again" = "Znovu nezobrazovat"; "common.open_source_licenses" = "Licence s otevřeným zdrojovým kódem"; "common.pinned" = "Připnuto"; "common.send_to" = "Odeslat do"; -"common_no_room_name" = "Žádný název místnosti"; -"common_poll_end_confirmation" = "Opravdu chcete ukončit toto hlasování?"; -"common_poll_summary" = "Hlasování: %1$@"; -"common_something_went_wrong" = "Něco se pokazilo"; -"common_unable_to_decrypt_no_access" = "Nemáte přístup k této zprávě"; -"common_verify_device" = "Ověřit zařízení"; +"common.you" = "Vy"; +"common_unable_to_decrypt_insecure_device" = "Šifrováno nezabezpečeným zařízením"; +"common_unable_to_decrypt_verification_violation" = "Ověřená identita odesílatele se změnila"; "confirm_recovery_key_banner_message" = "Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Potvrďte klíč pro obnovení"; "crash_detection_dialog_content" = "%1$@ havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"; +"crypto_identity_change_pin_violation" = "Zdá se, že se identita %1$@ změnila. %2$@"; +"crypto_identity_change_pin_violation_new" = "Zdá se, že identita %1$@ %2$@ se změnila. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Aby mohla aplikace používat fotoaparát, udělte prosím oprávnění v nastavení systému."; "dialog_permission_generic" = "Udělte prosím oprávnění v nastavení systému."; "dialog_permission_location_description_ios" = "Udělte přístup v Nastavení -> Poloha."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Tichá oznámení"; "notification_incoming_call" = "Příchozí hovor"; "notification_inline_reply_failed" = "** Nepodařilo se odeslat - otevřete prosím místnost"; -"notification_invitation_action_reject" = "Odmítnout"; "notification_invite_body" = "Vás pozval(a) do chatu"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ vás pozval(a) do chatu"; "notification_mentioned_you_body" = "Zmínili vás: %1$@"; "notification_new_messages" = "Nové zprávy"; "notification_reaction_body" = "Reagoval(a) s %1$@"; "notification_room_invite_body" = "Vás pozval(a) do místnosti"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ vás pozval(a) do místnosti"; "notification_sender_me" = "Já"; "notification_sender_mention_reply" = "%1$@ zmínil(a) nebo odpověděl(a)"; "notification_test_push_notification_content" = "Prohlížíte si oznámení! Klikněte na mě!"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Zrušit odsazení"; "rich_text_editor_url_placeholder" = "Odkaz"; "rich_text_editor_a11y_add_attachment" = "Přidat přílohu"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Vlastní URL pro Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Nastavte vlastní URL pro Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Neplatné URL, ujistěte se, že jste uvedli protokol (http/https) a správnou adresu."; +"screen_create_room_room_address_section_footer" = "Aby byla tato místnost viditelná v adresáři veřejných místností, budete potřebovat adresu místnosti."; +"screen_create_room_room_address_section_title" = "Adresa místnosti"; +"screen_create_room_room_visibility_section_title" = "Viditelnost místnosti"; +"screen_create_room_access_section_anyone_option_description" = "Do této místnosti může vstoupit kdokoli"; +"screen_create_room_access_section_anyone_option_title" = "Kdokoliv"; +"screen_create_room_access_section_header" = "Přístup do místnosti"; +"screen_create_room_access_section_knocking_option_description" = "Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout"; +"screen_create_room_access_section_knocking_option_title" = "Požádat o připojení"; +"screen_join_room_cancel_knock_action" = "Zrušit žádost"; +"screen_join_room_cancel_knock_alert_confirmation" = "Ano, zrušit"; +"screen_join_room_cancel_knock_alert_description" = "Opravdu chcete zrušit svou žádost o vstup do této místnosti?"; +"screen_join_room_cancel_knock_alert_title" = "Zrušit žádost o vstup"; +"screen_join_room_knock_message_description" = "Zpráva (nepovinné)"; +"screen_join_room_knock_sent_description" = "Pokud bude váš požadavek přijat, obdržíte pozvánku na vstup do místnosti."; +"screen_join_room_knock_sent_title" = "Žádost o vstup odeslána"; "screen_pinned_timeline_empty_state_description" = "Přidržte zprávu a vyberte „%1$@“, kterou chcete zahrnout sem."; "screen_pinned_timeline_empty_state_headline" = "Připněte důležité zprávy, aby je bylo možné snadno najít"; -"screen_pinned_timeline_screen_title_empty" = "Připnuté zprávy"; "screen_reset_encryption_password_error" = "Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Zrušit ověření a odeslat"; "screen_resolve_send_failure_changed_identity_subtitle" = "Ověření můžete zrušit a přesto odeslat tuto zprávu, nebo můžete prozatím zrušit a zkusit to znovu později po opětovném ověření %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Načítání zprávy..."; "screen_room_pinned_banner_view_all_button_title" = "Zobrazit vše"; "screen_room_details_pinned_events_row_title" = "Připnuté zprávy"; +"screen_roomlist_knock_event_sent_description" = "Žádost o vstup odeslána"; "screen_timeline_item_menu_send_failure_changed_identity" = "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$@ se změnila."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Zpráva nebyla odeslána, protože%1$@ neověřil(a) všechna zařízení."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení."; -"screen_account_provider_change" = "Změna poskytovatele účtu"; "screen_account_provider_form_hint" = "Adresa domovského serveru"; "screen_account_provider_form_notice" = "Zadejte hledaný výraz nebo adresu domény."; "screen_account_provider_form_subtitle" = "Vyhledejte společnost, komunitu nebo soukromý server."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Chystáte se vytvořit účet na %@"; "screen_advanced_settings_developer_mode" = "Vývojářský režim"; "screen_advanced_settings_developer_mode_description" = "Povolením získáte přístup k funkcím a funkcím pro vývojáře."; +"screen_advanced_settings_media_compression_description" = "Optimalizovat pro nahrávání"; +"screen_advanced_settings_media_compression_title" = "Média"; "screen_advanced_settings_rich_text_editor_description" = "Vypněte editor formátovaného textu pro ruční zadání Markdown."; "screen_advanced_settings_send_read_receipts" = "Potvrzení o přečtení"; "screen_advanced_settings_send_read_receipts_description" = "Pokud je vypnuto, potvrzení o přečtení se nikomu neodesílají. Stále budete dostávat potvrzení o přečtení od ostatních uživatelů."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Vyberte váš server"; "screen_chat_backup_key_backup_action_disable" = "Vypnout zálohování"; "screen_chat_backup_key_backup_action_enable" = "Zapnout zálohování"; -"screen_chat_backup_key_backup_description" = "Zálohování zajistí, že neztratíte historii zpráv. %1$@."; -"screen_chat_backup_key_backup_title" = "Záloha"; +"screen_chat_backup_key_backup_description" = "Bezpečně uložte svou kryptografickou identitu a klíče zpráv na serveru. To vám umožní zobrazit historii zpráv na všech nových zařízeních. %1$@."; +"screen_chat_backup_key_backup_title" = "Úložiště klíčů"; +"screen_chat_backup_key_storage_disabled_error" = "Pro nastavení obnovení musí být zapnuto úložiště klíčů."; +"screen_chat_backup_key_storage_toggle_description" = "Nahrát klíče z tohoto zařízení"; +"screen_chat_backup_key_storage_toggle_title" = "Povolit ukládání klíčů"; "screen_chat_backup_recovery_action_change" = "Změnit klíč pro obnovení"; -"screen_chat_backup_recovery_action_confirm" = "Potvrďte klíč pro obnovení"; +"screen_chat_backup_recovery_action_change_description" = "Obnovte svou kryptografickou identitu a historii zpráv pomocí klíče pro obnovení, pokud jste ztratili všechna stávající zařízení."; "screen_chat_backup_recovery_action_confirm_description" = "Vaše záloha chatu není aktuálně synchronizována."; -"screen_chat_backup_recovery_action_setup" = "Nastavení obnovy"; "screen_chat_backup_recovery_action_setup_description" = "Získejte přístup ke svým zašifrovaným zprávám, pokud ztratíte všechna zařízení nebo jste všude odhlášeni z %1$@."; "screen_create_account_title" = "Vytvořit účet"; "screen_create_new_recovery_key_list_item_1" = "Otevřít %1$@ na stolním počítači"; @@ -447,17 +481,16 @@ "screen_create_poll_anonymous_desc" = "Zobrazit výsledky až po skončení hlasování"; "screen_create_poll_anonymous_headline" = "Anonymní hlasování"; "screen_create_poll_answer_hint" = "Volba %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Vaše změny nebudou uloženy"; "screen_create_poll_cancel_confirmation_title_ios" = "Zrušit hlasování"; "screen_create_poll_question_desc" = "Otázka nebo téma"; "screen_create_poll_question_hint" = "Čeho se hlasování týká?"; "screen_create_poll_title" = "Vytvořit hlasování"; "screen_create_room_action_create_room" = "Nová místnost"; "screen_create_room_error_creating_room" = "Při vytváření místnosti došlo k chybě"; -"screen_create_room_private_option_description" = "Zprávy v této místnosti jsou šifrované. Šifrování nelze později vypnout."; -"screen_create_room_private_option_title" = "Soukromá místnost (jen pro pozvané)"; -"screen_create_room_public_option_description" = "Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později."; -"screen_create_room_public_option_title" = "Veřejná místnost (kdokoli)"; +"screen_create_room_private_option_description" = "Do této místnosti mají přístup pouze pozvaní lidé. Všechny zprávy jsou koncově šifrovány."; +"screen_create_room_private_option_title" = "Soukromá místnost"; +"screen_create_room_public_option_description" = "Tuto místnost může najít kdokoli.\nTo můžete kdykoli změnit v nastavení místnosti."; +"screen_create_room_public_option_title" = "Veřejná místnost"; "screen_create_room_topic_label" = "Téma (nepovinné)"; "screen_deactivate_account_confirmation_dialog_content" = "Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět."; "screen_deactivate_account_delete_all_messages" = "Smazat všechny mé zprávy"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Skupinové chaty"; "screen_notification_settings_invite_for_me_label" = "Pozvánky"; "screen_notification_settings_mentions_only_disclaimer" = "Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."; -"screen_notification_settings_mentions_section_title" = "Zmínky"; "screen_notification_settings_mode_all" = "Vše"; "screen_notification_settings_mode_mentions" = "Zmínky"; "screen_notification_settings_notification_section_title" = "Upozornit mě na"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Vybrat %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "\"Připojit nové zařízení\""; "screen_qr_code_login_initial_state_item_4" = "Naskenujte QR kód pomocí tohoto zařízení"; +"screen_qr_code_login_initial_state_subtitle" = "Dostupné pouze v případě, že to váš poskytovatel účtu podporuje."; "screen_qr_code_login_initial_state_title" = "Otevřete %1$@ na jiném zařízení pro získání QR kódu"; "screen_qr_code_login_invalid_scan_state_description" = "Použijte QR kód zobrazený na druhém zařízení."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Špatný QR kód"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Váš ověřovací kód"; "screen_recovery_key_change_description" = "Získejte nový klíč pro obnovení, pokud jste ztratili stávající klíč. Po změně klíče pro obnovení již váš starý klíč nebude fungovat."; "screen_recovery_key_change_generate_key" = "Vygenerovat nový klíč pro obnovení"; -"screen_recovery_key_change_generate_key_description" = "Ujistěte se, že můžete klíč pro obnovení uložit někde v bezpečí"; "screen_recovery_key_change_success" = "Klíč pro obnovení byl změněn"; "screen_recovery_key_change_title" = "Změnit klíč pro obnovení?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Vytvořit nový klíč pro obnovení"; @@ -616,18 +648,17 @@ "screen_recovery_key_confirm_key_placeholder" = "Zadejte..."; "screen_recovery_key_confirm_lost_recovery_key" = "Ztratili jste klíč pro obnovení?"; "screen_recovery_key_confirm_success" = "Klíč pro obnovení potvrzen"; -"screen_recovery_key_confirm_title" = "Zadejte klíč pro obnovení"; "screen_recovery_key_copied_to_clipboard" = "Klíč pro obnovení zkopírován"; "screen_recovery_key_generating_key" = "Generování..."; "screen_recovery_key_save_action" = "Uložit klíč pro obnovení"; -"screen_recovery_key_save_description" = "Zapište si klíč pro obnovení na bezpečné místo nebo jej uložte do správce hesel."; +"screen_recovery_key_save_description" = "Zapište si tento obnovovací klíč na bezpečné místo, jako je správce hesel, zašifrovaná poznámka nebo fyzický trezor."; "screen_recovery_key_save_key_description" = "Klepnutím zkopírujte klíč pro obnovení"; "screen_recovery_key_save_title" = "Uložte si klíč pro obnovení"; "screen_recovery_key_setup_confirmation_description" = "Po tomto kroku nebudete mít přístup k novému klíči pro obnovení."; "screen_recovery_key_setup_confirmation_title" = "Uložili jste si klíč pro obnovení?"; "screen_recovery_key_setup_description" = "Záloha chatu je chráněna klíčem pro obnovení. Pokud potřebujete nový klíč pro obnovení po nastavení, můžete jej znovu vytvořit výběrem možnosti „Změnit klíč pro obnovení“."; "screen_recovery_key_setup_generate_key" = "Vygenerovat klíč pro obnovení"; -"screen_recovery_key_setup_generate_key_description" = "Ujistěte se, že můžete klíč pro obnovení uložit někde v bezpečí"; +"screen_recovery_key_setup_generate_key_description" = "Toto s nikým nesdílejte!"; "screen_recovery_key_setup_success" = "Nastavení obnovení bylo úspěšné"; "screen_recovery_key_setup_title" = "Nastavení obnovy"; "screen_report_content_block_user_hint" = "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Ano, resetovat nyní"; "screen_reset_encryption_confirmation_alert_subtitle" = "Tento proces je nevratný."; "screen_reset_encryption_confirmation_alert_title" = "Opravdu chcete obnovit šifrování?"; -"screen_reset_encryption_password_placeholder" = "Zadat..."; "screen_reset_encryption_password_subtitle" = "Potvrďte, že chcete obnovit šifrování."; "screen_reset_encryption_password_title" = "Pro pokračování zadejte heslo k účtu"; "screen_reset_identity_confirmation_subtitle" = "Chystáte se přejít na svůj %1$@ účet a obnovit svou identitu. Poté budete přesměrováni zpět do aplikace."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Správci mají automaticky oprávnění moderátora"; "screen_room_change_role_moderators_title" = "Upravit moderátory"; "screen_room_change_role_unsaved_changes_description" = "Máte neuložené změny."; -"screen_room_change_role_unsaved_changes_title" = "Uložit změny?"; "screen_room_details_add_topic_title" = "Přidat téma"; "screen_room_details_already_a_member" = "Již členem"; "screen_room_details_already_invited" = "Již pozván(a)"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."; "screen_room_details_notification_mode_custom" = "Vlastní"; "screen_room_details_notification_mode_default" = "Výchozí"; -"screen_room_details_notification_title" = "Oznámení"; "screen_room_details_share_room_title" = "Sdílet místnost"; "screen_room_details_title" = "Informace o místnosti"; "screen_room_details_updating_room" = "Aktualizace místnosti..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Odblokovat"; "screen_room_member_details_unblock_alert_description" = "Znovu uvidíte všechny zprávy od nich."; "screen_room_member_details_unblock_user" = "Odblokovat uživatele"; +"screen_room_member_details_verify_button_subtitle" = "K ověření tohoto uživatele použijte webovou aplikaci."; +"screen_room_member_details_verify_button_title" = "Ověřit %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Vykázat"; "screen_room_member_list_ban_member_confirmation_description" = "Nebudou se moci znovu připojit k této místnosti, pokud budou pozváni."; "screen_room_member_list_ban_member_confirmation_title" = "Jste si jisti, že chcete vykázat tohoto člena?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Vykazování %1$@"; "screen_room_member_list_manage_member_ban" = "Odebrat a vykázat člena"; "screen_room_member_list_manage_member_remove" = "Odebrat z místnosti"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Odebrat a vykázat člena"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Pouze odebrat člena"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Odebrat člena a zakázat mu připojení v budoucnu?"; "screen_room_member_list_manage_member_unban_action" = "Zrušit vykázání"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Zobrazit méně"; "screen_room_timeline_message_copied" = "Zpráva zkopírována"; "screen_room_timeline_no_permission_to_post" = "Nemáte oprávnění zveřejňovat příspěvky v této místnosti"; -"screen_room_timeline_reactions_show_less" = "Zobrazit méně"; "screen_room_timeline_reactions_show_more" = "Zobrazit více"; "screen_room_timeline_read_marker_title" = "Nové"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Označit jako přečtené"; "screen_roomlist_mark_as_unread" = "Označit jako nepřečtené"; "screen_roomlist_room_directory_button_title" = "Procházet všechny místnosti"; -"screen_server_confirmation_change_server" = "Změnit poskytovatele účtu"; "screen_server_confirmation_message_login_element_dot_io" = "Soukromý server pro zaměstnance Elementu."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."; "screen_server_confirmation_message_register" = "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Porovnejte čísla"; "screen_session_verification_complete_subtitle" = "Vaše nová relace je nyní ověřena. Má přístup k vašim zašifrovaným zprávám a ostatní uživatelé ji uvidí jako důvěryhodnou."; "screen_session_verification_enter_recovery_key" = "Zadejte klíč pro obnovení"; +"screen_session_verification_failed_subtitle" = "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření."; "screen_session_verification_open_existing_session_subtitle" = "Pro přístup k historii zašifrovaných zpráv prokažte, že jste to vy."; "screen_session_verification_open_existing_session_title" = "Otevřete existující relaci"; "screen_session_verification_positive_button_canceled" = "Opakovat ověření"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Čekání na shodu"; "screen_session_verification_ready_subtitle" = "Porovnejte jedinečnou sadu emotikonů."; "screen_session_verification_request_accepted_subtitle" = "Porovnejte jedinečné emotikony a ujistěte se, že jsou zobrazeny ve stejném pořadí."; +"screen_session_verification_request_details_timestamp" = "Přihlášen"; +"screen_session_verification_request_failure_title" = "Ověření se nezdařilo"; +"screen_session_verification_request_footer" = "Pokračujte, pouze pokud jste toto ověření zahájili."; +"screen_session_verification_request_subtitle" = "Ověřte druhé zařízení, aby byla vaše historie zpráv zabezpečená."; +"screen_session_verification_request_success_subtitle" = "Nyní můžete bezpečně číst nebo odesílat zprávy na svém druhém zařízení."; +"screen_session_verification_request_success_title" = "Zařízení ověřeno"; +"screen_session_verification_request_title" = "Požadováno ověření"; "screen_session_verification_they_dont_match" = "Neshodují se"; "screen_session_verification_they_match" = "Shodují se"; "screen_session_verification_waiting_to_accept_subtitle" = "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."; "screen_signout_key_backup_disabled_title" = "Vypnuli jste zálohování"; "screen_signout_key_backup_offline_subtitle" = "Když jste přešli do režimu offline, vaše klíče se ještě stále zálohovaly. Znovu se připojte, aby bylo možné před odhlášením zálohovat vaše klíče."; -"screen_signout_key_backup_offline_title" = "Vaše klíče jsou stále zálohovány"; "screen_signout_key_backup_ongoing_subtitle" = "Před odhlášením prosím počkejte na dokončení."; "screen_signout_key_backup_ongoing_title" = "Vaše klíče jsou stále zálohovány"; "screen_signout_recovery_disabled_subtitle" = "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."; "screen_signout_recovery_disabled_title" = "Obnovení není nastaveno"; "screen_signout_save_recovery_key_subtitle" = "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám."; -"screen_signout_save_recovery_key_title" = "Uložili jste si klíč pro obnovení?"; "screen_start_chat_error_starting_chat" = "Při pokusu o zahájení chatu došlo k chybě"; "screen_view_location_title" = "Poloha"; "screen_welcome_bullet_1" = "Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."; @@ -919,7 +952,6 @@ "test_language_identifier" = "en"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Odstraňování problémů"; -"troubleshoot_notifications_entry_point_title" = "Odstraňování problémů s upozorněními"; "troubleshoot_notifications_screen_action" = "Spustit testy"; "troubleshoot_notifications_screen_action_again" = "Spustit testy znovu"; "troubleshoot_notifications_screen_failure" = "Některé testy selhaly. Zkontrolujte prosím podrobnosti."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ujistěte se, že jsou k dispozici distributoři UnifiedPush."; "troubleshoot_notifications_test_unified_push_failure" = "Nebyli nalezeni žádní push distributoři."; "troubleshoot_notifications_test_unified_push_title" = "Zkontrolovat UnifiedPush"; +"a11y_poll" = "Hlasování"; +"banner_set_up_recovery_submit" = "Nastavení obnovy"; "dialog_title_error" = "Chyba"; "dialog_title_success" = "Úspěch"; "notification_fallback_content" = "Oznámení"; "notification_invitation_action_join" = "Vstoupit"; +"notification_invitation_action_reject" = "Odmítnout"; "notification_room_action_mark_as_read" = "Označit jako přečtené"; "notification_room_action_quick_reply" = "Rychlá odpověď"; +"screen_pinned_timeline_screen_title_empty" = "Připnuté zprávy"; "screen_room_mentions_at_room_title" = "Všichni"; +"screen_account_provider_change" = "Změnit poskytovatele účtu"; "screen_account_provider_signin_subtitle" = "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."; "screen_account_provider_signup_subtitle" = "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily."; "screen_analytics_settings_help_us_improve" = "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Znovu uvidíte všechny zprávy od nich."; "screen_blocked_users_unblock_alert_title" = "Odblokovat uživatele"; "screen_bug_report_rash_logs_alert_title" = "%1$@ havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"; +"screen_chat_backup_recovery_action_confirm" = "Zadejte klíč pro obnovení"; +"screen_chat_backup_recovery_action_setup" = "Nastavení obnovy"; +"screen_create_poll_cancel_confirmation_content_ios" = "Vaše změny nebudou uloženy"; "screen_create_room_add_people_title" = "Pozvat přátele"; "screen_create_room_room_name_label" = "Název místnosti"; "screen_create_room_title" = "Vytvořit místnost"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Upravit hlasování"; "screen_identity_use_another_device" = "Použít jiné zařízení"; "screen_login_subtitle" = "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci."; +"screen_notification_settings_mentions_section_title" = "Zmínky"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Zkusit znovu"; +"screen_recovery_key_change_generate_key_description" = "Toto s nikým nesdílejte!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Zablokovat uživatele"; +"screen_reset_encryption_password_placeholder" = "Zadejte..."; "screen_room_attachment_source_camera_photo" = "Vyfotit"; "screen_room_change_permissions_everyone" = "Všichni"; "screen_room_change_permissions_member_moderation" = "Moderování členů"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Správci"; "screen_room_change_role_section_moderators" = "Moderátoři"; "screen_room_change_role_section_users" = "Členové"; +"screen_room_change_role_unsaved_changes_title" = "Uložit změny?"; "screen_room_details_invite_people_title" = "Pozvat přátele"; "screen_room_details_leave_conversation_title" = "Opustit konverzaci"; "screen_room_details_leave_room_title" = "Opustit místnost"; +"screen_room_details_notification_title" = "Oznámení"; "screen_room_details_roles_and_permissions" = "Role a oprávnění"; "screen_room_details_room_name_label" = "Název místnosti"; "screen_room_details_security_title" = "Zabezpečení"; "screen_room_details_topic_title" = "Téma"; "screen_room_error_failed_processing_media" = "Nahrání média se nezdařilo, zkuste to prosím znovu."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Odebrat a vykázat člena"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Pouze zmínky a klíčová slova"; +"screen_room_timeline_reactions_show_less" = "Zobrazit méně"; "screen_roomlist_filter_people" = "Lidé"; +"screen_server_confirmation_change_server" = "Změnit poskytovatele účtu"; +"screen_session_verification_request_failure_subtitle" = "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření."; "screen_signout_confirmation_dialog_submit" = "Odhlásit se"; "screen_signout_confirmation_dialog_title" = "Odhlásit se"; +"screen_signout_key_backup_offline_title" = "Vaše klíče jsou stále zálohovány"; "screen_signout_preference_item" = "Odhlásit se"; +"screen_signout_save_recovery_key_title" = "Uložili jste si klíč pro obnovení?"; +"troubleshoot_notifications_entry_point_title" = "Odstraňování problémů s upozorněními"; diff --git a/ElementX/Resources/Localizations/de.lproj/Localizable.strings b/ElementX/Resources/Localizations/de.lproj/Localizable.strings index aea080e28b..0ba57e7b95 100644 --- a/ElementX/Resources/Localizations/de.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/de.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausieren"; "a11y_pin_field" = "PIN-Feld"; "a11y_play" = "Abspielen"; -"a11y_poll" = "Umfrage"; "a11y_poll_end" = "Umfrage beendet"; "a11y_react_with" = "Reagiere mit %1$@"; "a11y_react_with_other_emojis" = "Mit anderen Emojis reagieren"; @@ -41,6 +40,7 @@ "action_create" = "Erstellen"; "action_create_a_room" = "Raum erstellen"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Ablehnen"; "action_delete_poll" = "Umfrage löschen"; "action_disable" = "Deaktivieren"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Passwort vergessen?"; "action_forward" = "Weiterleiten"; "action_go_back" = "Zurück"; +"action_ignore" = "Ignore"; "action_invite" = "Einladen"; "action_invite_friends" = "Personen einladen"; "action_invite_friends_to_app" = "Zu %1$@ einladen"; @@ -64,6 +65,7 @@ "action_leave" = "Verlassen"; "action_leave_conversation" = "Unterhaltung verlassen"; "action_leave_room" = "Verlassen"; +"action_load_more" = "Mehr laden ..."; "action_manage_account" = "Konto verwalten"; "action_manage_devices" = "Geräte verwalten"; "action_message" = "Nachricht"; @@ -84,7 +86,7 @@ "action_report_bug" = "Fehler melden"; "action_report_content" = "Inhalt melden"; "action_reset" = "Zurücksetzen"; -"action_reset_identity" = "Reset identity"; +"action_reset_identity" = "Identität zurücksetzen"; "action_retry" = "Erneut versuchen"; "action_retry_decryption" = "Entschlüsselung wiederholen"; "action_save" = "Speichern"; @@ -93,6 +95,7 @@ "action_send_message" = "Nachricht senden"; "action_share" = "Teilen"; "action_share_link" = "Link teilen"; +"action_show" = "Show"; "action_sign_in_again" = "Erneut anmelden"; "action_signout" = "Abmelden"; "action_signout_anyway" = "Trotzdem abmelden"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Im Chatverlauf anzeigen"; "action_view_source" = "Quellcode anzeigen"; "action_yes" = "Ja"; -"action.load_more" = "Mehr laden ..."; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Abmelden und aktualisieren"; "banner_migrate_to_native_sliding_sync_description" = "Dein Server unterstützt jetzt ein neues, schnelleres Protokoll. Melde dich ab und melde dich wieder an, um zu aktualisieren. Wenn du das jetzt tust, vermeidest du eine erzwungene Abmeldung, wenn das alte Protokoll später entfernt wird."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."; "banner_migrate_to_native_sliding_sync_title" = "Aktualisierung verfügbar"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Wiederherstellung einrichten"; +"banner_set_up_recovery_content" = "Erstelle einen neuen Wiederherstellungsschlüssel, mit dem du deinen verschlüsselten Nachrichtenverlauf wiederherstellen kannst, wenn du dich an einem neuen Gerät anmeldest."; +"banner_set_up_recovery_title" = "Wiederherstellung einrichten"; "common_about" = "Über"; "common_acceptable_use_policy" = "Nutzungsrichtlinie"; "common_advanced_settings" = "Erweiterte Einstellungen"; @@ -133,10 +134,12 @@ "common_dark" = "Dunkel"; "common_decryption_error" = "Dekodierungsfehler"; "common_developer_options" = "Entwickleroptionen"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Direktnachricht"; "common_edited_suffix" = "(bearbeitet)"; "common_editing" = "Bearbeitung"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Verschlüsselung aktiviert"; "common_enter_your_pin" = "PIN eingeben"; "common_error" = "Fehler"; @@ -147,6 +150,7 @@ "common_favourited" = "Favorit"; "common_file" = "Datei"; "common_forward_message" = "Nachricht weiterleiten"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Bild"; "common_in_reply_to" = "Als Antwort auf %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Stummschalten"; "common_no_results" = "Keine Ergebnisse"; +"common_no_room_name" = "Kein Raumname"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "oder"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalink"; "common_permission" = "Erlaubnis"; "common_please_wait" = "Bitte warten ..."; +"common_poll_end_confirmation" = "Bist du sicher, dass du diese Umfrage beenden möchtest?"; +"common_poll_summary" = "Umfrage: %1$@"; "common_poll_total_votes" = "Stimmen insgesamt: %1$@"; "common_poll_undisclosed_text" = "Die Ergebnisse werden nach Ende der Umfrage angezeigt"; "common_privacy_policy" = "Datenschutz­erklärung"; @@ -200,6 +207,7 @@ "common_settings" = "Einstellungen"; "common_shared_location" = "Geteilter Standort"; "common_signing_out" = "Abmelden"; +"common_something_went_wrong" = "Es ist ein Fehler aufgetreten."; "common_starting_chat" = "Chat wird gestartet..."; "common_sticker" = "Sticker"; "common_success" = "Erfolg"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Worum geht es in diesem Raum?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Entschlüsselung nicht möglich"; +"common_unable_to_decrypt_no_access" = "Du hast kein Recht diese Nachricht zu lesen."; "common_unable_to_invite_message" = "Einladungen konnten nicht an einen oder mehrere Benutzer gesendet werden."; "common_unable_to_invite_title" = "Einladung(en) konnte(n) nicht gesendet werden"; "common_unlock" = "Entsperren"; @@ -221,23 +230,30 @@ "common_username" = "Benutzername"; "common_verification_cancelled" = "Verifizierung abgebrochen"; "common_verification_complete" = "Verifizierung abgeschlossen"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Gerät verifizieren"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Sprachnachricht"; "common_waiting" = "Warten…"; "common_waiting_for_decryption_key" = "Warte auf diese Nachricht"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Nicht mehr anzeigen"; "common.open_source_licenses" = "Open-Source-Lizenzen"; "common.pinned" = "Fixiert"; "common.send_to" = "Senden an"; -"common_no_room_name" = "Kein Raumname"; -"common_poll_end_confirmation" = "Bist du sicher, dass du diese Umfrage beenden möchtest?"; -"common_poll_summary" = "Umfrage: %1$@"; -"common_something_went_wrong" = "Es ist ein Fehler aufgetreten."; -"common_unable_to_decrypt_no_access" = "Du hast kein Recht diese Nachricht zu lesen."; -"common_verify_device" = "Gerät verifizieren"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Dein Chat-Backup ist derzeit nicht synchronisiert. Du musst deinen Wiederherstellungsschlüssel bestätigen, um Zugriff auf dein Chat-Backup zu erhalten."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Wiederherstellungsschlüssel bestätigen."; "crash_detection_dialog_content" = "%1$@ ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Damit die Anwendung die Kamera verwenden kann, erteile bitte die Erlaubnis in den Systemeinstellungen."; "dialog_permission_generic" = "Bitte erteile die Erlaubnis in den Systemeinstellungen."; "dialog_permission_location_description_ios" = "Erlaube den Zugriff unter Einstellungen -> Standort."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Stumme Benachrichtigungen"; "notification_incoming_call" = "Eingehender Anruf"; "notification_inline_reply_failed" = "** Fehler beim Senden - bitte Raum öffnen"; -"notification_invitation_action_reject" = "Ablehnen"; "notification_invite_body" = "Du wurdest zu einem Chat eingeladen"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Hat Dich erwähnt: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Ohne Einrückung"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Anhang hinzufügen"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Benutzerdefinierte Element-Aufruf-Basis-URL"; "screen_advanced_settings_element_call_base_url_description" = "Lege eine eigene Basis-URL für Element Call fest."; "screen_advanced_settings_element_call_base_url_validation_error" = "Ungültige URL, bitte stelle sicher, dass du das Protokoll (http/https) und die richtige Adresse angibst."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Drücke auf eine Nachricht und wähle “%1$@”, um sie hier einzufügen."; "screen_pinned_timeline_empty_state_headline" = "Fixiere wichtige Nachrichten, so dass sie leicht gefunden werden können"; -"screen_pinned_timeline_screen_title_empty" = "Fixierte Nachrichten"; "screen_reset_encryption_password_error" = "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Verifizierung zurückziehen und senden"; "screen_resolve_send_failure_changed_identity_subtitle" = "Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst sie vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$@ erneut verifiziert hast."; @@ -346,14 +376,14 @@ "screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; "screen_room_mentions_at_room_subtitle" = "Alle Mitglieder benachrichtigen"; "screen_room_pinned_banner_indicator" = "%1$@ von %2$@"; -"screen_room_pinned_banner_indicator_description" = "%1$@ Angepinnte Nachrichten"; +"screen_room_pinned_banner_indicator_description" = "%1$@ fixierte Nachrichten"; "screen_room_pinned_banner_loading_description" = "Nachricht wird geladen…"; "screen_room_pinned_banner_view_all_button_title" = "Alle anzeigen"; "screen_room_details_pinned_events_row_title" = "Fixierte Nachrichten"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$@ geändert hat."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Nachricht wurde nicht gesendet, weil %1$@ nicht alle Geräte verifiziert hat"; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Kontoanbieter ändern"; "screen_account_provider_form_hint" = "Homeserver-Adresse"; "screen_account_provider_form_notice" = "Gib einen Suchbegriff oder eine Domainadresse ein."; "screen_account_provider_form_subtitle" = "Suche nach einem Unternehmen, einer Community oder einem privaten Server."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Du bist dabei, ein Konto bei %@ zu erstellen"; "screen_advanced_settings_developer_mode" = "Entwickler-Modus"; "screen_advanced_settings_developer_mode_description" = "Aktivieren, um Zugriff auf Features und Funktionen für Entwickler zu aktivieren."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Deaktiviere den Rich-Text-Editor, um Markdown manuell einzugeben."; "screen_advanced_settings_send_read_receipts" = "Lesebestätigungen"; "screen_advanced_settings_send_read_receipts_description" = "Wenn diese Option deaktiviert ist, werden Ihre Lesebestätigungen an niemanden gesendet. Du erhältst weiterhin Lesebestätigungen von anderen Benutzern."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Backup aktivieren"; "screen_chat_backup_key_backup_description" = "Das Backup stellt sicher, dass du deinen Nachrichtenverlauf nicht verlierst. %1$@."; "screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Wiederherstellungsschlüssel ändern"; -"screen_chat_backup_recovery_action_confirm" = "Wiederherstellungsschlüssel bestätigen"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Dein Chat-Backup ist derzeit nicht synchronisiert."; -"screen_chat_backup_recovery_action_setup" = "Wiederherstellung einrichten"; "screen_chat_backup_recovery_action_setup_description" = "Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verlierst oder von %1$@ überall abgemeldet bist."; "screen_create_account_title" = "Konto erstellen"; "screen_create_new_recovery_key_list_item_1" = "Öffne %1$@ auf einem Desktop-Gerät"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Ergebnisse erst nach Ende der Umfrage anzeigen"; "screen_create_poll_anonymous_headline" = "Anonyme Umfrage"; "screen_create_poll_answer_hint" = "Option %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Deine Änderungen werden nicht gespeichert"; "screen_create_poll_cancel_confirmation_title_ios" = "Umfrage abbrechen"; "screen_create_poll_question_desc" = "Frage oder Thema"; "screen_create_poll_question_hint" = "Worum geht es bei der Umfrage?"; @@ -478,9 +511,9 @@ "screen_edit_profile_title" = "Profil bearbeiten"; "screen_edit_profile_updating_details" = "Profil wird aktualisiert..."; "screen_encryption_reset_action_continue_reset" = "Zurücksetzen fortsetzen"; -"screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; -"screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; +"screen_encryption_reset_bullet_1" = "Deine Kontodaten, Kontakte, Einstellungen und die Liste der Chats bleiben erhalten"; +"screen_encryption_reset_bullet_2" = "Du verlierst alle deine bisherigen Nachrichten sofern sie nicht auf einem anderen Gerät vorliegen"; +"screen_encryption_reset_bullet_3" = "Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren."; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; "screen_identity_confirmation_cannot_confirm" = "Can't confirm?"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Gruppenchats"; "screen_notification_settings_invite_for_me_label" = "Einladungen"; "screen_notification_settings_mentions_only_disclaimer" = "Dein Homeserver unterstützt diese Option in verschlüsselten Chat nicht. In einigen Chats wirst du möglicherweise nicht benachrichtigt."; -"screen_notification_settings_mentions_section_title" = "Erwähnungen"; "screen_notification_settings_mode_all" = "Alle"; "screen_notification_settings_mode_mentions" = "Erwähnungen"; "screen_notification_settings_notification_section_title" = "Benachrichtige mich bei"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Wähle %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "\"Neues Gerät verknüpfen\""; "screen_qr_code_login_initial_state_item_4" = "Scanne den QR-Code mit diesem Gerät"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Öffne %1$@ auf einem anderen Gerät, um den QR-Code zu erhalten"; "screen_qr_code_login_invalid_scan_state_description" = "Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Falscher QR-Code"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Dein Verifizierungscode"; "screen_recovery_key_change_description" = "Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr."; "screen_recovery_key_change_generate_key" = "Wiederherstellungsschlüssel erstellen"; -"screen_recovery_key_change_generate_key_description" = "Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"; "screen_recovery_key_change_success" = "Wiederherstellungsschlüssel geändert"; "screen_recovery_key_change_title" = "Wiederherstellungsschlüssel ändern?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Neuen Wiederherstellungsschlüssel erstellen"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Eingeben..."; "screen_recovery_key_confirm_lost_recovery_key" = "Hast du deinen Wiederherstellungschlüssel vergessen?"; "screen_recovery_key_confirm_success" = "Wiederherstellungsschlüssel bestätigt"; -"screen_recovery_key_confirm_title" = "Bitte Wiederherstellungsschlüssel eingeben"; "screen_recovery_key_copied_to_clipboard" = "Wiederherstellungsschlüssel kopiert"; "screen_recovery_key_generating_key" = "Generieren…"; "screen_recovery_key_save_action" = "Wiederherstellungsschlüssel speichern"; @@ -633,12 +664,11 @@ "screen_report_content_block_user_hint" = "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"; "screen_report_content_explanation" = "Diese Meldung wird an den Administrator deines Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen."; "screen_report_content_hint" = "Grund für die Meldung dieses Inhalts"; -"screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; -"screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; -"screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Eingeben..."; -"screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; -"screen_reset_encryption_password_title" = "Enter your account password to continue"; +"screen_reset_encryption_confirmation_alert_action" = "Ja, zurücksetzen"; +"screen_reset_encryption_confirmation_alert_subtitle" = "Das Zurücksetzen kann nicht rückgängig gemacht werden."; +"screen_reset_encryption_confirmation_alert_title" = "Bist du sicher, dass du deine Identität zurücksetzen möchtest?"; +"screen_reset_encryption_password_subtitle" = "Bestätige, dass du deine Identität zurücksetzen möchtest."; +"screen_reset_encryption_password_title" = "Gib dein Passwort ein, um fortzufahren"; "screen_reset_identity_confirmation_subtitle" = "Du wirst jetzt zu deinem %1$@ Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht."; "screen_reset_identity_confirmation_title" = "Kannst du das nicht bestätigen? Gehe zu deinem Konto, um deine Identität zurückzusetzen."; "screen_room_alias_resolver_resolve_alias_failure" = "Der Raum-Alias konnte nicht ermittelt werden."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Administratoren haben automatisch Moderatorenrechte"; "screen_room_change_role_moderators_title" = "Moderatoren bearbeiten"; "screen_room_change_role_unsaved_changes_description" = "Du hast nicht gespeicherte Änderungen."; -"screen_room_change_role_unsaved_changes_title" = "Änderungen speichern?"; "screen_room_details_add_topic_title" = "Thema hinzufügen"; "screen_room_details_already_a_member" = "Bereits Mitglied"; "screen_room_details_already_invited" = "Bereits eingeladen"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Die Deaktivierung der Stummschaltung ist fehlgeschlagen, bitte versuche es erneut."; "screen_room_details_notification_mode_custom" = "Benutzerdefiniert"; "screen_room_details_notification_mode_default" = "Standard"; -"screen_room_details_notification_title" = "Benachrichtigungen"; "screen_room_details_share_room_title" = "Teilen"; "screen_room_details_title" = "Informationen"; "screen_room_details_updating_room" = "Raum wird aktualisiert…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Blockierung aufheben"; "screen_room_member_details_unblock_alert_description" = "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt."; "screen_room_member_details_unblock_user" = "Blockierung aufheben"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Sperren"; "screen_room_member_list_ban_member_confirmation_description" = "Sie können dem Raum nicht mehr beitreten, selbst wenn sie eingeladen werden."; "screen_room_member_list_ban_member_confirmation_title" = "Bist du sicher, dass du dieses Mitglied sperren möchtest?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "%1$@ wird gesperrt."; "screen_room_member_list_manage_member_ban" = "Mitglied entfernen und sperren"; "screen_room_member_list_manage_member_remove" = "Mitglied entfernen"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Mitglied entfernen und sperren"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Mitglied nur entfernen"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Mitglied entfernen und den erneuten Beitritt sperren?"; "screen_room_member_list_manage_member_unban_action" = "Sperre aufheben"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Weniger anzeigen"; "screen_room_timeline_message_copied" = "Nachricht wurde kopiert"; "screen_room_timeline_no_permission_to_post" = "Du bist nicht berechtigt, in diesem Raum zu schreiben"; -"screen_room_timeline_reactions_show_less" = "Weniger anzeigen"; "screen_room_timeline_reactions_show_more" = "Mehr anzeigen"; "screen_room_timeline_read_marker_title" = "Neu"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Als gelesen markieren"; "screen_roomlist_mark_as_unread" = "Als ungelesen markieren"; "screen_roomlist_room_directory_button_title" = "Alle Räume durchsuchen"; -"screen_server_confirmation_change_server" = "Kontoanbieter wechseln"; "screen_server_confirmation_message_login_element_dot_io" = "Ein privater Server für die Mitarbeiter von Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."; "screen_server_confirmation_message_register" = "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Vergleiche die Zahlen"; "screen_session_verification_complete_subtitle" = "Deine neue Session ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und wird von anderen Benutzern als vertrauenswürdig eingestuft."; "screen_session_verification_enter_recovery_key" = "Wiederherstellungsschlüssel eingeben"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Beweise deine Identität, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."; "screen_session_verification_open_existing_session_title" = "Öffne eine bestehende Session"; "screen_session_verification_positive_button_canceled" = "Verifizierung wiederholen"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Warten auf eine Übereinstimmung"; "screen_session_verification_ready_subtitle" = "Vergleiche eine spezielle Reihe von Emojis."; "screen_session_verification_request_accepted_subtitle" = "Vergleiche die einzelnen Emojis und stelle sicher, dass sie in der gleichen Reihenfolge erscheinen."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Sie stimmen nicht überein"; "screen_session_verification_they_match" = "Sie stimmen überein"; "screen_session_verification_waiting_to_accept_subtitle" = "Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."; "screen_signout_key_backup_disabled_title" = "Du hast das Backup deaktiviert."; "screen_signout_key_backup_offline_subtitle" = "Deine Schlüssel wurden noch gesichert, als du offline gegangen bist. Stelle die Verbindung wieder her, damit deine Schlüssel gesichert werden können, bevor du dich abmeldest."; -"screen_signout_key_backup_offline_title" = "Deine Schlüssel werden noch gesichert"; "screen_signout_key_backup_ongoing_subtitle" = "Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dich abmeldest."; "screen_signout_key_backup_ongoing_title" = "Deine Schlüssel werden noch gesichert"; "screen_signout_recovery_disabled_subtitle" = "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."; "screen_signout_recovery_disabled_title" = "Wiederherstellung nicht eingerichtet"; "screen_signout_save_recovery_key_subtitle" = "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du möglicherweise den Zugriff auf deine verschlüsselten Nachrichten."; -"screen_signout_save_recovery_key_title" = "Hast du deinen Wiederherstellungsschlüssel gespeichert?"; "screen_start_chat_error_starting_chat" = "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"; "screen_view_location_title" = "Standort"; "screen_welcome_bullet_1" = "Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."; @@ -919,7 +952,6 @@ "test_language_identifier" = "en"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Fehlerbehebung"; -"troubleshoot_notifications_entry_point_title" = "Fehlerbehebung für Benachrichtigungen"; "troubleshoot_notifications_screen_action" = "Tests durchführen"; "troubleshoot_notifications_screen_action_again" = "Tests erneut durchführen"; "troubleshoot_notifications_screen_failure" = "Einige Tests sind fehlgeschlagen. Bitte überprüfe die Details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Stelle sicher, dass UnifiedPush-Verteiler verfügbar sind."; "troubleshoot_notifications_test_unified_push_failure" = "Keine Push-Verteiler gefunden."; "troubleshoot_notifications_test_unified_push_title" = "UnifiedPush prüfen"; +"a11y_poll" = "Umfrage"; +"banner_set_up_recovery_submit" = "Wiederherstellung einrichten"; "dialog_title_error" = "Fehler"; "dialog_title_success" = "Erfolg"; "notification_fallback_content" = "Mitteilung"; "notification_invitation_action_join" = "Beitreten"; +"notification_invitation_action_reject" = "Ablehnen"; "notification_room_action_mark_as_read" = "Als gelesen markieren"; "notification_room_action_quick_reply" = "Schnelle Antwort"; +"screen_pinned_timeline_screen_title_empty" = "Fixierte Nachrichten"; "screen_room_mentions_at_room_title" = "Alle"; +"screen_account_provider_change" = "Kontoanbieter wechseln"; "screen_account_provider_signin_subtitle" = "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."; "screen_account_provider_signup_subtitle" = "Hier werden deine Gespräche gespeichert - so wie du deine E-Mails bei einem E-Mail-Anbieter aufbewahren würden."; "screen_analytics_settings_help_us_improve" = "Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt."; "screen_blocked_users_unblock_alert_title" = "Blockierung aufheben"; "screen_bug_report_rash_logs_alert_title" = "%1$@ ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?"; +"screen_chat_backup_recovery_action_confirm" = "Wiederherstellungsschlüssel eingeben"; +"screen_chat_backup_recovery_action_setup" = "Wiederherstellung einrichten"; +"screen_create_poll_cancel_confirmation_content_ios" = "Deine Änderungen werden nicht gespeichert"; "screen_create_room_add_people_title" = "Personen einladen"; "screen_create_room_room_name_label" = "Raumname"; "screen_create_room_title" = "Raum erstellen"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Umfrage bearbeiten"; "screen_identity_use_another_device" = "Ein anderes Gerät verwenden"; "screen_login_subtitle" = "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation."; +"screen_notification_settings_mentions_section_title" = "Erwähnungen"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Erneut versuchen"; +"screen_recovery_key_change_generate_key_description" = "Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Benutzer blockieren"; +"screen_reset_encryption_password_placeholder" = "Eingeben..."; "screen_room_attachment_source_camera_photo" = "Foto aufnehmen"; "screen_room_change_permissions_everyone" = "Alle"; "screen_room_change_permissions_member_moderation" = "Moderation der Mitglieder"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administratoren"; "screen_room_change_role_section_moderators" = "Moderatoren"; "screen_room_change_role_section_users" = "Mitglieder"; +"screen_room_change_role_unsaved_changes_title" = "Änderungen speichern?"; "screen_room_details_invite_people_title" = "Personen einladen"; "screen_room_details_leave_conversation_title" = "Unterhaltung verlassen"; "screen_room_details_leave_room_title" = "Verlassen"; +"screen_room_details_notification_title" = "Benachrichtigungen"; "screen_room_details_roles_and_permissions" = "Rollen und Berechtigungen"; "screen_room_details_room_name_label" = "Raumname"; "screen_room_details_security_title" = "Sicherheit"; "screen_room_details_topic_title" = "Thema"; "screen_room_error_failed_processing_media" = "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Mitglied entfernen und sperren"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Nur Erwähnungen und Schlüsselwörter"; +"screen_room_timeline_reactions_show_less" = "Weniger anzeigen"; "screen_roomlist_filter_people" = "Personen"; +"screen_server_confirmation_change_server" = "Kontoanbieter wechseln"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Abmelden"; "screen_signout_confirmation_dialog_title" = "Abmelden"; +"screen_signout_key_backup_offline_title" = "Deine Schlüssel werden noch gesichert"; "screen_signout_preference_item" = "Abmelden"; +"screen_signout_save_recovery_key_title" = "Hast du deinen Wiederherstellungsschlüssel gespeichert?"; +"troubleshoot_notifications_entry_point_title" = "Fehlerbehebung für Benachrichtigungen"; diff --git a/ElementX/Resources/Localizations/el.lproj/Localizable.strings b/ElementX/Resources/Localizations/el.lproj/Localizable.strings index b4570d38a9..9c73cfbee0 100644 --- a/ElementX/Resources/Localizations/el.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/el.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Παύση"; "a11y_pin_field" = "Πεδίο PIN"; "a11y_play" = "Αναπαραγωγή"; -"a11y_poll" = "Δημοσκόπηση"; "a11y_poll_end" = "Ολοκληρωμένη δημοσκόπηση"; "a11y_react_with" = "Αντέδρασε με %1$@"; "a11y_react_with_other_emojis" = "Αντέδρασε με άλλα emoji"; @@ -41,6 +40,7 @@ "action_create" = "Δημιουργία"; "action_create_a_room" = "Δημιούργησε ένα δωμάτιο"; "action_deactivate" = "Απενεργοποίηση"; +"action_deactivate_account" = "Απενεργοποίηση λογαριασμού"; "action_decline" = "Απόρριψη"; "action_delete_poll" = "Διαγραφή Δημοσκόπησης"; "action_disable" = "Απενεργοποίηση"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Ξέχασες τον κωδικό πρόσβασης;"; "action_forward" = "Προώθηση"; "action_go_back" = "Πήγαινε πίσω"; +"action_ignore" = "Παράβλεψη"; "action_invite" = "Πρόσκληση"; "action_invite_friends" = "Πρόσκληση ατόμων"; "action_invite_friends_to_app" = "Πρόσκληση ατόμων στο %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Αποχώρηση"; "action_leave_conversation" = "Αποχώρηση από τη συζήτηση"; "action_leave_room" = "Αποχώρηση από το δωμάτιο"; +"action_load_more" = "Φόρτωσε περισσότερα"; "action_manage_account" = "Διαχείριση λογαριασμού"; "action_manage_devices" = "Διαχείριση συσκευών"; "action_message" = "Στείλε"; @@ -93,6 +95,7 @@ "action_send_message" = "Αποστολή μηνύματος"; "action_share" = "Κοινή χρήση"; "action_share_link" = "Κοινή χρήση συνδέσμου"; +"action_show" = "Εμφάνιση"; "action_sign_in_again" = "Συνδέσου ξανά"; "action_signout" = "Αποσύνδεση"; "action_signout_anyway" = "Αποσύνδεση ούτως ή άλλως"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Προβολή στο χρονοδιάγραμμα"; "action_view_source" = "Προβολή πηγής"; "action_yes" = "Ναι"; -"action.load_more" = "Φόρτωσε περισσότερα"; -"action_deactivate_account" = "Απενεργοποίηση λογαριασμού"; -"banner_migrate_to_native_sliding_sync_action" = "Αποσύνδεση & Αναβάθμιση"; +"banner_migrate_to_native_sliding_sync_action" = "Αποσύνδεση & Αναβάθμιση"; "banner_migrate_to_native_sliding_sync_description" = "Ο διακομιστής σου υποστηρίζει τώρα ένα νέο, ταχύτερο πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για αναβάθμιση τώρα. Κάνοντας αυτό τώρα θα σε βοηθήσει να αποφύγεις μια αναγκαστική αποσύνδεση όταν το παλιό πρωτόκολλο καταργηθεί αργότερα."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."; "banner_migrate_to_native_sliding_sync_title" = "Διαθέσιμη αναβάθμιση"; -"banner.set_up_recovery.content" = "Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου."; -"banner.set_up_recovery.title" = "Ρύθμιση ανάκτησης"; +"banner_set_up_recovery_content" = "Δημιούργησε ένα νέο κλειδί ανάκτησης που μπορεί να χρησιμοποιηθεί για την επαναφορά του ιστορικού των κρυπτογραφημένων μηνυμάτων σου σε περίπτωση που χάσεις την πρόσβαση στις συσκευές σου."; +"banner_set_up_recovery_title" = "Ρύθμιση ανάκτησης"; "common_about" = "Σχετικά"; "common_acceptable_use_policy" = "Πολιτική αποδεκτής χρήσης"; "common_advanced_settings" = "Ρυθμίσεις για προχωρημένους"; @@ -133,10 +134,12 @@ "common_dark" = "Σκοτεινό"; "common_decryption_error" = "Σφάλμα αποκρυπτογράφησης"; "common_developer_options" = "Επιλογές προγραμματιστή"; +"common_device_id" = "ID συσκευής"; "common_direct_chat" = "Άμεση συνομιλία"; "common_edited_suffix" = "(επεξεργάστηκε)"; "common_editing" = "Επεξεργάζεται"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Η κρυπτογράφηση ενεργοποιήθηκε"; "common_enter_your_pin" = "Εισήγαγε το PIN σου"; "common_error" = "Σφάλμα"; @@ -147,6 +150,7 @@ "common_favourited" = "Είναι αγαπημένο"; "common_file" = "Αρχείο"; "common_forward_message" = "Προώθηση μηνύματος"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Εικόνα"; "common_in_reply_to" = "Σε απάντηση στον χρήστη %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Σύγχρονη"; "common_mute" = "Σίγαση"; "common_no_results" = "Κανένα αποτέλεσμα"; +"common_no_room_name" = "Χωρίς όνομα δωματίου"; "common_offline" = "Εκτός σύνδεσης"; "common_optic_id_ios" = "Optic ID"; "common_or" = "ή"; @@ -170,6 +175,8 @@ "common_permalink" = "Μόνιμος σύνδεσμος"; "common_permission" = "Αδεια"; "common_please_wait" = "Παρακαλώ περίμενε..."; +"common_poll_end_confirmation" = "Θες σίγουρα να τερματίσεις αυτή τη δημοσκόπηση;"; +"common_poll_summary" = "Δημοσκόπηση: %1$@"; "common_poll_total_votes" = "Σύνολο ψήφων: %1$@"; "common_poll_undisclosed_text" = "Τα αποτελέσματα θα εμφανιστούν μετά τη λήξη της ψηφοφορίας"; "common_privacy_policy" = "Πολιτική απορρήτου"; @@ -200,6 +207,7 @@ "common_settings" = "Ρυθμίσεις"; "common_shared_location" = "Κοινόχρηστη τοποθεσία"; "common_signing_out" = "Αποσύνδεση"; +"common_something_went_wrong" = "Κάτι πήγε στραβά"; "common_starting_chat" = "Έναρξη συνομιλίας..."; "common_sticker" = "Αυτοκόλλητο"; "common_success" = "Επιτυχία"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Τί αφορά το δωμάτιο;"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Δεν είναι δυνατή η αποκρυπτογράφηση"; +"common_unable_to_decrypt_no_access" = "Δεν έχεις πρόσβαση σε αυτό το μήνυμα"; "common_unable_to_invite_message" = "Δεν ήταν δυνατή η αποστολή προσκλήσεων σε έναν ή περισσότερους χρήστες."; "common_unable_to_invite_title" = "Δεν είναι δυνατή η αποστολή προσκλήσεων"; "common_unlock" = "Ξεκλείδωμα"; @@ -221,23 +230,30 @@ "common_username" = "Όνομα χρήστη"; "common_verification_cancelled" = "Η επαλήθευση ακυρώθηκε"; "common_verification_complete" = "Η επαλήθευση ολοκληρώθηκε"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Επαληθεύτηκε"; +"common_verify_device" = "Επαλήθευση συσκευής"; +"common_verify_identity" = "Verify identity"; "common_video" = "Βίντεο"; "common_voice_message" = "Φωνητικό μήνυμα"; "common_waiting" = "Αναμονή…"; "common_waiting_for_decryption_key" = "Αναμονή για αυτό το μήνυμα"; +"common.copied_to_clipboard" = "Αντιγράφηκε στο πρόχειρο"; "common.do_not_show_this_again" = "Να μην εμφανιστεί ξανά"; "common.open_source_licenses" = "Άδειες ανοιχτού κώδικα"; "common.pinned" = "Καρφιτσωμένο"; "common.send_to" = "Αποστολή σε"; -"common_no_room_name" = "Χωρίς όνομα δωματίου"; -"common_poll_end_confirmation" = "Θες σίγουρα να τερματίσεις αυτή τη δημοσκόπηση;"; -"common_poll_summary" = "Δημοσκόπηση: %1$@"; -"common_something_went_wrong" = "Κάτι πήγε στραβά"; -"common_unable_to_decrypt_no_access" = "Δεν έχεις πρόσβαση σε αυτό το μήνυμα"; -"common_verify_device" = "Επαλήθευση συσκευής"; +"common.you" = "Εσύ"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Το αντίγραφο ασφαλείας της συνομιλίας σου δεν είναι συγχρονισμένο αυτήν τη στιγμή. Πρέπει να εισαγάγεις το κλειδί ανάκτησης για να διατηρήσεις την πρόσβαση στο αντίγραφο ασφαλείας της συνομιλίας σου."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Εισήγαγε το κλειδί ανάκτησης"; "crash_detection_dialog_content" = "Το %1$@ διακόπηκε την τελευταία φορά που χρησιμοποιήθηκε. Θα 'θελες να μοιραστείς μια αναφορά σφάλματος μαζί μας;"; +"crypto_identity_change_pin_violation" = "Η ταυτότητα του χρήστη %1$@ φαίνεται να έχει αλλάξει. %2$@"; +"crypto_identity_change_pin_violation_new" = "Η ταυτότητα του %1$@ %2$@ φαίνεται να έχει αλλάξει. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Για να επιτρέψεις στην εφαρμογή να χρησιμοποιεί την κάμερα, παραχώρησε την άδεια στις ρυθμίσεις συστήματος."; "dialog_permission_generic" = "Παρακαλώ παραχώρησε την άδεια στις ρυθμίσεις συστήματος."; "dialog_permission_location_description_ios" = "Παραχώρησε πρόσβαση στις Ρυθμίσεις -> Τοποθεσία."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Αθόρυβες ειδοποιήσεις"; "notification_incoming_call" = "Εισερχόμενη κλήση"; "notification_inline_reply_failed" = "** Αποτυχία αποστολής - παρακαλώ άνοιξε το δωμάτιο"; -"notification_invitation_action_reject" = "Απόρριψη"; "notification_invite_body" = "Σε προσκάλεσε να συνομιλήσετε"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "Ο χρήστης %1$@ σε προσκάλεσε σε συνομιλία"; "notification_mentioned_you_body" = "Σέ ανέφερε: %1$@"; "notification_new_messages" = "Νέα Μηνύματα"; "notification_reaction_body" = "Αντέδρασε με %1$@"; "notification_room_invite_body" = "Σέ προσκάλεσε να συμμετάσχεις στο δωμάτιο"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "Ο χρήστης %1$@ σε προσκάλεσε να συμμετάσχεις στο δωμάτιο"; "notification_sender_me" = "Εγώ"; "notification_sender_mention_reply" = "Ο χρήστης %1$@ αναφέρθηκε ή απάντησε"; "notification_test_push_notification_content" = "Βλέπεις την ειδοποίηση! Κάνε μου κλικ!"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Χωρίς εσοχή"; "rich_text_editor_url_placeholder" = "Σύνδεσμος"; "rich_text_editor_a11y_add_attachment" = "Προσθήκη συνημμένου"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Προσαρμοσμένο URL βάσης κλήσεων Element"; "screen_advanced_settings_element_call_base_url_description" = "Όρισε μια προσαρμοσμένη διεύθυνση βάσης URL για κλήση Element."; "screen_advanced_settings_element_call_base_url_validation_error" = "Μη έγκυρη διεύθυνση URL, βεβαιώσου ότι έχεις συμπεριλάβει το πρωτόκολλο (http/https) και τη σωστή διεύθυνση."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο"; +"screen_create_room_access_section_anyone_option_title" = "Οποιοσδήποτε"; +"screen_create_room_access_section_header" = "Πρόσβαση Δωματίου"; +"screen_create_room_access_section_knocking_option_description" = "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"; +"screen_create_room_access_section_knocking_option_title" = "Αίτημα συμμετοχής"; +"screen_join_room_cancel_knock_action" = "Ακύρωση αιτήματος"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Μήνυμα (προαιρετικό)"; +"screen_join_room_knock_sent_description" = "Θα λάβεις πρόσκληση για συμμετοχή στο δωμάτιο εάν το αίτημά σου γίνει αποδεκτό."; +"screen_join_room_knock_sent_title" = "Το αίτημα συμμετοχής στάλθηκε"; "screen_pinned_timeline_empty_state_description" = "Πάτα σε ένα μήνυμα και επέλεξε «%1$@» για να συμπεριληφθεί εδώ."; "screen_pinned_timeline_empty_state_headline" = "Καρφίτσωσε σημαντικά μηνύματα, ώστε να μπορούν να εντοπιστούν εύκολα"; -"screen_pinned_timeline_screen_title_empty" = "Καρφιτσωμένα μηνύματα"; "screen_reset_encryption_password_error" = "Συνέβη ένα άγνωστο σφάλμα. Έλεγξε ότι ο κωδικός πρόσβασης του λογαριασμού σου είναι σωστός και δοκίμασε ξανά."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Ανάκληση επαλήθευσης και αποστολή"; "screen_resolve_send_failure_changed_identity_subtitle" = "Μπορείτε να ανακαλέσεις την επαλήθευσή σου και να στείλεις αυτό το μήνυμα όπως και να 'χει ή μπορείς να το ακυρώσεις προς το παρόν και να προσπαθήσεις ξανά αργότερα μετά την επαλήθευση του χρήστη %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Φόρτωση μηνύματος..."; "screen_room_pinned_banner_view_all_button_title" = "Προβολή Όλων"; "screen_room_details_pinned_events_row_title" = "Καρφιτσωμένα μηνύματα"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Το μήνυμα δεν στάλθηκε επειδή η επαληθευμένη ταυτότητα του χρήστη %1$@ έχει αλλάξει."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Το μήνυμα δεν στάλθηκε επειδή ο χρήστης %1$@ δεν έχει επαληθεύσει όλες τις συσκευές."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Το μήνυμα δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου."; -"screen_account_provider_change" = "Αλλαγή παρόχου λογαριασμού"; "screen_account_provider_form_hint" = "Διεύθυνση οικιακού διακομιστή"; "screen_account_provider_form_notice" = "Εισήγαγε έναν όρο αναζήτησης ή μια διεύθυνση τομέα."; "screen_account_provider_form_subtitle" = "Αναζήτησε μια εταιρεία, κοινότητα ή ιδιωτικό διακομιστή."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Πρόκειται να δημιουργήσεις έναν λογαριασμό στο %@"; "screen_advanced_settings_developer_mode" = "Λειτουργία προγραμματιστή"; "screen_advanced_settings_developer_mode_description" = "Ενεργοποίησε την πρόσβαση σε δυνατότητες και λειτουργικότητα για προγραμματιστές."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Απενεργοποίησε τον επεξεργαστή εμπλουτισμένου κειμένου για να πληκτρολογήσεις Markdown χειροκίνητα."; "screen_advanced_settings_send_read_receipts" = "Αποδεικτικά ανάγνωσης"; "screen_advanced_settings_send_read_receipts_description" = "Εάν απενεργοποιηθεί, τα αποδεικτικά ανάγνωσης δεν θα στέλνονται σε κανέναν. Θα εξακολουθείς να λαμβάνεις αποδεικτικά ανάγνωσης από άλλους χρήστες."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Επέλεξε το διακομιστή σου"; "screen_chat_backup_key_backup_action_disable" = "Απενεργοποίηση αντιγράφων ασφαλείας"; "screen_chat_backup_key_backup_action_enable" = "Ενεργοποίηση αντιγράφων ασφαλείας"; -"screen_chat_backup_key_backup_description" = "Η δημιουργία αντιγράφων ασφαλείας διασφαλίζει ότι δεν θα χάσεις το ιστορικό μηνυμάτων σου. %1$@."; -"screen_chat_backup_key_backup_title" = "Αντίγραφο ασφαλείας"; +"screen_chat_backup_key_backup_description" = "Αποθήκευσε την κρυπτογραφική σου ταυτότητα και τα κλειδιά μηνυμάτων με ασφάλεια στον διακομιστή. Αυτό θα σου επιτρέψει να δεις το ιστορικό μηνυμάτων σου σε οποιεσδήποτε νέες συσκευές. %1$@."; +"screen_chat_backup_key_backup_title" = "Χώρος αποθήκευσης κλειδιού"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Αλλαγή κλειδιού ανάκτησης"; -"screen_chat_backup_recovery_action_confirm" = "Εισαγωγή κλειδιού ανάκτησης"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Το αντίγραφο ασφαλείας της συνομιλίας σου δεν είναι συγχρονισμένο αυτήν τη στιγμή."; -"screen_chat_backup_recovery_action_setup" = "Ρύθμιση ανάκτησης"; "screen_chat_backup_recovery_action_setup_description" = "Απόκτησε πρόσβαση στα κρυπτογραφημένα σου μηνύματα εάν χάσεις όλες τις συσκευές σου ή έχεις αποσυνδεθεί από το %1$@ παντού."; "screen_create_account_title" = "Δημιουργία λογαριασμού"; "screen_create_new_recovery_key_list_item_1" = "Άνοιγμα %1$@ σε συσκευή υπολογιστή"; @@ -447,17 +481,16 @@ "screen_create_poll_anonymous_desc" = "Εμφάνιση αποτελεσμάτων μόνο μετά τη λήξη της ψηφοφορίας"; "screen_create_poll_anonymous_headline" = "Απόκρυψη ψήφων"; "screen_create_poll_answer_hint" = "Επιλογή %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Οι αλλαγές σου δεν θα αποθηκευτούν"; "screen_create_poll_cancel_confirmation_title_ios" = "Ακύρωση Δημοσκόπησης"; "screen_create_poll_question_desc" = "Ερώτηση ή θέμα"; "screen_create_poll_question_hint" = "Τί αφορά η δημοσκόπηση;"; "screen_create_poll_title" = "Δημιουργία Δημοσκόπησης"; "screen_create_room_action_create_room" = "Νέο δωμάτιο"; "screen_create_room_error_creating_room" = "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του δωματίου"; -"screen_create_room_private_option_description" = "Τα μηνύματα σε αυτό το δωμάτιο είναι κρυπτογραφημένα. Η κρυπτογράφηση δεν μπορεί να απενεργοποιηθεί αργότερα."; -"screen_create_room_private_option_title" = "Ιδιωτικό δωμάτιο (μόνο με πρόσκληση)"; -"screen_create_room_public_option_description" = "Τα μηνύματα δεν είναι κρυπτογραφημένα και ο καθένας μπορεί να τα διαβάσει. Μπορείς να ενεργοποιήσεις την κρυπτογράφηση αργότερα."; -"screen_create_room_public_option_title" = "Δημόσιο δωμάτιο (οποιοσδήποτε)"; +"screen_create_room_private_option_description" = "Μόνο άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτό το δωμάτιο. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο."; +"screen_create_room_private_option_title" = "Ιδιωτικό δωμάτιο"; +"screen_create_room_public_option_description" = "Ο καθένας μπορεί να βρει αυτό το δωμάτιο.\nΜπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου."; +"screen_create_room_public_option_title" = "Δημόσιο δωμάτιο"; "screen_create_room_topic_label" = "Θέμα (προαιρετικό)"; "screen_deactivate_account_confirmation_dialog_content" = "Παρακαλώ επιβεβαίωσε ότι θες να απενεργοποιήσεις τον λογαριασμό σου. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."; "screen_deactivate_account_delete_all_messages" = "Διαγραφή όλων των μηνυμάτων μου"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Ομαδικές συνομιλίες"; "screen_notification_settings_invite_for_me_label" = "Προσκλήσεις"; "screen_notification_settings_mentions_only_disclaimer" = "Ο οικιακός διακομιστής σου δεν υποστηρίζει αυτήν την επιλογή σε κρυπτογραφημένα δωμάτια, ενδέχεται να μην λάβεις ειδοποίηση σε ορισμένα δωμάτια."; -"screen_notification_settings_mentions_section_title" = "Αναφορές"; "screen_notification_settings_mode_all" = "Όλα"; "screen_notification_settings_mode_mentions" = "Αναφορές"; "screen_notification_settings_notification_section_title" = "Ειδοποίησέ με για"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Επιλογή %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "«Σύνδεση νέας συσκευής»"; "screen_qr_code_login_initial_state_item_4" = "Σάρωσε τον κωδικό QR με αυτήν τη συσκευή"; +"screen_qr_code_login_initial_state_subtitle" = "Διατίθεται μόνο εάν ο πάροχος του λογαριασμού σου το υποστηρίζει."; "screen_qr_code_login_initial_state_title" = "Άνοιγμα %1$@ σε άλλη συσκευή για να λήψη κωδικού QR"; "screen_qr_code_login_invalid_scan_state_description" = "Χρησιμοποίησε τον κωδικό QR που εμφανίζεται στην άλλη συσκευή."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Λάθος κωδικός QR"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Ο κωδικός επαλήθευσής σου"; "screen_recovery_key_change_description" = "Απόκτησε ένα νέο κλειδί ανάκτησης εάν έχεις χάσει το υπάρχον. Αφού αλλάξεις το κλειδί ανάκτησης, το παλιό δεν θα λειτουργεί πλέον."; "screen_recovery_key_change_generate_key" = "Δημιουργία νέου κλειδιού ανάκτησης"; -"screen_recovery_key_change_generate_key_description" = "Βεβαιώσου ότι μπορείς να αποθηκεύσεις το κλειδί ανάκτησης σε ασφαλές μέρος"; "screen_recovery_key_change_success" = "Το κλειδί ανάκτησης άλλαξε"; "screen_recovery_key_change_title" = "Αλλαγή κλειδιού ανάκτησης;"; "screen_recovery_key_confirm_create_new_recovery_key" = "Δημιουργία νέου κλειδιού ανάκτησης"; @@ -616,18 +648,17 @@ "screen_recovery_key_confirm_key_placeholder" = "Εισαγωγή..."; "screen_recovery_key_confirm_lost_recovery_key" = "Έχασες το κλειδί ανάκτησης;"; "screen_recovery_key_confirm_success" = "Επιβεβαιώθηκε το κλειδί ανάκτησης"; -"screen_recovery_key_confirm_title" = "Εισήγαγε το κλειδί ανάκτησης"; "screen_recovery_key_copied_to_clipboard" = "Αντιγράφηκε το κλειδί ανάκτησης"; "screen_recovery_key_generating_key" = "Δημιουργία..."; "screen_recovery_key_save_action" = "Αποθήκευση κλειδιού ανάκτησης"; -"screen_recovery_key_save_description" = "Γράψε το κλειδί ανάκτησης κάπου ασφαλές ή αποθήκευσέ το σε έναν διαχειριστή κωδικών πρόσβασης."; +"screen_recovery_key_save_description" = "Γράψε αυτό το κλειδί ανάκτησης κάπου ασφαλές, όπως έναν διαχειριστή κωδικών πρόσβασης, μια κρυπτογραφημένη σημείωση ή ένα φυσικό χρηματοκιβώτιο."; "screen_recovery_key_save_key_description" = "Πάτα για να αντιγράψεις το κλειδί ανάκτησης"; "screen_recovery_key_save_title" = "Αποθήκευσε το κλειδί ανάκτησης"; "screen_recovery_key_setup_confirmation_description" = "Δεν θα μπορείς να αποκτήσεις πρόσβαση στο νέο κλειδί ανάκτησης μετά από αυτό το βήμα."; "screen_recovery_key_setup_confirmation_title" = "Έχεις αποθηκεύσει το κλειδί ανάκτησης;"; "screen_recovery_key_setup_description" = "Το αντίγραφο ασφαλείας της συνομιλίας σου προστατεύεται από ένα κλειδί ανάκτησης. Εάν χρειαστείς ένα νέο κλειδί ανάκτησης μετά την εγκατάσταση, μπορείς να δημιουργήσεις ξανά επιλέγοντας «Αλλαγή κλειδιού ανάκτησης»."; "screen_recovery_key_setup_generate_key" = "Δημιουργία κλειδιού ανάκτησης"; -"screen_recovery_key_setup_generate_key_description" = "Βεβαιώσου ότι μπορείς να αποθηκεύσεις το κλειδί ανάκτησης κάπου ασφαλές"; +"screen_recovery_key_setup_generate_key_description" = "Μην το μοιραστείς με κανέναν!"; "screen_recovery_key_setup_success" = "Επιτυχής ρύθμιση ανάκτησης"; "screen_recovery_key_setup_title" = "Ρύθμιση ανάκτησης"; "screen_report_content_block_user_hint" = "Επέλεξε εάν θες να αποκρύψεις όλα τα τρέχοντα και μελλοντικά μηνύματα από αυτόν τον χρήστη"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Ναι, επαναφορά τώρα"; "screen_reset_encryption_confirmation_alert_subtitle" = "Η διαδικασία είναι μη αναστρέψιμη."; "screen_reset_encryption_confirmation_alert_title" = "Σίγουρα θες να επαναφέρεις την ταυτότητά σου;"; -"screen_reset_encryption_password_placeholder" = "Εισαγωγή..."; "screen_reset_encryption_password_subtitle" = "Επιβεβαίωσε ότι θες να επαναφέρεις την ταυτότητά σου."; "screen_reset_encryption_password_title" = "Εισήγαγε τον κωδικό πρόσβασης του λογαριασμού σου για να συνεχίσεις"; "screen_reset_identity_confirmation_subtitle" = "Πρόκειται να μεταβείς στον λογαριασμό σου %1$@ για να επαναφέρεις την ταυτότητά σου. Στη συνέχεια, θα επιστρέψεις στην εφαρμογή."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Οι διαχειριστές έχουν αυτόματα δικαιώματα συντονιστή"; "screen_room_change_role_moderators_title" = "Επεξεργασία Συντονιστών"; "screen_room_change_role_unsaved_changes_description" = "Έχεις μη αποθηκευμένες αλλαγές."; -"screen_room_change_role_unsaved_changes_title" = "Αποθήκευση αλλαγών;"; "screen_room_details_add_topic_title" = "Προσθήκη θέματος"; "screen_room_details_already_a_member" = "Ήδη μέλος"; "screen_room_details_already_invited" = "Ήδη προσκεκλημένος"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Αποτυχία κατάργησης σίγασης αυτού του δωματίου, δοκίμασε ξανά."; "screen_room_details_notification_mode_custom" = "Προσαρμοσμένο"; "screen_room_details_notification_mode_default" = "Προεπιλογή"; -"screen_room_details_notification_title" = "Ειδοποιήσεις"; "screen_room_details_share_room_title" = "Κοινή χρήση δωματίου"; "screen_room_details_title" = "Πληροφορίες δωματίου"; "screen_room_details_updating_room" = "Ενημέρωση δωματίου..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Άρση αποκλεισμού"; "screen_room_member_details_unblock_alert_description" = "Θα μπορείς να δεις ξανά όλα τα μηνύματα του."; "screen_room_member_details_unblock_user" = "Κατάργηση αποκλεισμού χρήστη"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Αποκλεισμός"; "screen_room_member_list_ban_member_confirmation_description" = "Δεν θα μπορεί να συμμετέχει ξανά σε αυτό το δωμάτιο εάν προσκληθεί."; "screen_room_member_list_ban_member_confirmation_title" = "Θες σίγουρα να αποκλείσεις αυτό το μέλος;"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Αποκλεισμός %1$@"; "screen_room_member_list_manage_member_ban" = "Αφαίρεση και αποκλεισμός μέλους"; "screen_room_member_list_manage_member_remove" = "Αφαίρεση από το δωμάτιο"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Αφαίρεση και αποκλεισμός μέλους"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Μόνο αφαίρεση μέλους"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Αφαίρεση μέλους και απαγόρευση συμμετοχής στο μέλλον;"; "screen_room_member_list_manage_member_unban_action" = "Αναίρεση αποκλεισμού"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Εμφάνιση λιγότερων"; "screen_room_timeline_message_copied" = "Το μήνυμα αντιγράφηκε"; "screen_room_timeline_no_permission_to_post" = "Δεν έχεις άδεια να δημοσιεύσεις σε αυτό το δωμάτιο"; -"screen_room_timeline_reactions_show_less" = "Εμφάνιση λιγότερων"; "screen_room_timeline_reactions_show_more" = "Εμφάνιση περισσότερων"; "screen_room_timeline_read_marker_title" = "Νέο"; "screen_room_title" = "Συνομιλία"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Επισήμανση ως αναγνωσμένου"; "screen_roomlist_mark_as_unread" = "Επισήμανση ως μη αναγνωσμένου"; "screen_roomlist_room_directory_button_title" = "Περιήγηση σε όλα τα δωμάτια"; -"screen_server_confirmation_change_server" = "Αλλαγή παρόχου λογαριασμού"; "screen_server_confirmation_message_login_element_dot_io" = "Ένας ιδιωτικός διακομιστής για υπαλλήλους του Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία."; "screen_server_confirmation_message_register" = "Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Σύγκριση αριθμών"; "screen_session_verification_complete_subtitle" = "Η νέα σου συνεδρία έχει πλέον επαληθευτεί. Έχει πρόσβαση στα κρυπτογραφημένα μηνύματά σας και άλλοι χρήστες θα το βλέπουν ως αξιόπιστο."; "screen_session_verification_enter_recovery_key" = "Εισαγωγή κλειδιού ανάκτησης"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Απέδειξε ότι είσαι εσύ για να αποκτήσεις πρόσβαση στο κρυπτογραφημένο ιστορικό μηνυμάτων σου."; "screen_session_verification_open_existing_session_title" = "Άνοιξε μια υπάρχουσα συνεδρία"; "screen_session_verification_positive_button_canceled" = "Επανάληψη επαλήθευσης"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Αναμονή για αντιστοίχιση"; "screen_session_verification_ready_subtitle" = "Σύγκρινε ένα μοναδικό σύνολο emojis."; "screen_session_verification_request_accepted_subtitle" = "Σύγκρινε τα μοναδικά emoji και σιγουρέψου ότι εμφανίζονται με την ίδια σειρά."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Ζητήθηκε επαλήθευση"; "screen_session_verification_they_dont_match" = "Δεν ταιριάζουν"; "screen_session_verification_they_match" = "Ταιριάζουν"; "screen_session_verification_waiting_to_accept_subtitle" = "Αποδέξου το αίτημα για να ξεκινήσεις τη διαδικασία επαλήθευσης στην άλλη συνεδρία σου για να συνεχίσεις."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Πρόκειται να αποσυνδεθείς από την τελευταία σου συνεδρία. Εάν αποσυνδεθείς τώρα, θα χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."; "screen_signout_key_backup_disabled_title" = "Έχεις απενεργοποιήσει τη δημιουργία αντιγράφων ασφαλείας"; "screen_signout_key_backup_offline_subtitle" = "Εξακολουθούσε να δημιουργείται αντίγραφο ασφαλείας των κλειδιών σου όταν βρέθηκες εκτός σύνδεσης. Επανασυνδέσου, ώστε να είναι δυνατή η δημιουργία αντιγράφων ασφαλείας των κλειδιών σου πριν αποσυνδεθείς."; -"screen_signout_key_backup_offline_title" = "Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"; "screen_signout_key_backup_ongoing_subtitle" = "Περίμενε να ολοκληρωθεί πριν αποσυνδεθείς."; "screen_signout_key_backup_ongoing_title" = "Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"; "screen_signout_recovery_disabled_subtitle" = "Πρόκειται να αποσυνδεθείς από την τελευταία σου συνεδρία. Εάν αποσυνδεθείς τώρα, θα χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."; "screen_signout_recovery_disabled_title" = "Η ανάκτηση δεν έχει ρυθμιστεί"; "screen_signout_save_recovery_key_subtitle" = "Πρόκειται να αποσυνδεθείς από την τελευταία σας συνεδρία. Εάν αποσυνδεθείς τώρα, ενδέχεται να χάσεις την πρόσβαση στα κρυπτογραφημένα μηνύματά σου."; -"screen_signout_save_recovery_key_title" = "Έχεις αποθηκεύσει το κλειδί ανάκτησης;"; "screen_start_chat_error_starting_chat" = "Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"; "screen_view_location_title" = "Τοποθεσία"; "screen_welcome_bullet_1" = "Κλήσεις, δημοσκοπήσεις, αναζήτηση και άλλα, θα προστεθούν αργότερα φέτος."; @@ -919,7 +952,6 @@ "test_language_identifier" = "el"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Αντιμετώπιση προβλημάτων"; -"troubleshoot_notifications_entry_point_title" = "Αντιμετώπιση προβλημάτων ειδοποιήσεων"; "troubleshoot_notifications_screen_action" = "Εκτέλεση δοκιμών"; "troubleshoot_notifications_screen_action_again" = "Επανεκτέλεση δοκιμών"; "troubleshoot_notifications_screen_failure" = "Ορισμένες δοκιμές απέτυχαν. Παρακαλώ έλεγξε τις λεπτομέρειες."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Βεβαιώσου ότι οι διανομείς UnifiedPush είναι διαθέσιμοι."; "troubleshoot_notifications_test_unified_push_failure" = "Δεν βρέθηκαν διανομείς push."; "troubleshoot_notifications_test_unified_push_title" = "Έλεγχος UnifiedPush"; +"a11y_poll" = "Δημοσκόπηση"; +"banner_set_up_recovery_submit" = "Ρύθμιση ανάκτησης"; "dialog_title_error" = "Σφάλμα"; "dialog_title_success" = "Επιτυχία"; "notification_fallback_content" = "Γνωστοποίηση"; "notification_invitation_action_join" = "Συμμετοχή"; +"notification_invitation_action_reject" = "Απόρριψη"; "notification_room_action_mark_as_read" = "Επισήμανση ως αναγνωσμένου"; "notification_room_action_quick_reply" = "Γρήγορη απάντηση"; +"screen_pinned_timeline_screen_title_empty" = "Καρφιτσωμένα μηνύματα"; "screen_room_mentions_at_room_title" = "Όλοι"; +"screen_account_provider_change" = "Αλλαγή παρόχου λογαριασμού"; "screen_account_provider_signin_subtitle" = "Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου."; "screen_account_provider_signup_subtitle" = "Εδώ θα ζουν οι συνομιλίες σου - όπως θα χρησιμοποιούσες έναν πάροχο email για να διατηρήσεις τα email σου."; "screen_analytics_settings_help_us_improve" = "Μοιράσου ανώνυμα δεδομένα χρήσης για να μάς βοηθήσεις να εντοπίσουμε προβλήματα."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Θα μπορείς να δεις ξανά όλα τα μηνύματα του."; "screen_blocked_users_unblock_alert_title" = "Κατάργηση αποκλεισμού χρήστη"; "screen_bug_report_rash_logs_alert_title" = "Το %1$@ διακόπηκε την τελευταία φορά που χρησιμοποιήθηκε. Θα 'θελες να μοιραστείς μια αναφορά σφάλματος μαζί μας;"; +"screen_chat_backup_recovery_action_confirm" = "Εισαγωγή κλειδιού ανάκτησης"; +"screen_chat_backup_recovery_action_setup" = "Ρύθμιση ανάκτησης"; +"screen_create_poll_cancel_confirmation_content_ios" = "Οι αλλαγές σου δεν θα αποθηκευτούν"; "screen_create_room_add_people_title" = "Πρόσκληση ατόμων"; "screen_create_room_room_name_label" = "Όνομα δωματίου"; "screen_create_room_title" = "Δημιούργησε ένα δωμάτιο"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Επεξεργασία δημοσκόπησης"; "screen_identity_use_another_device" = "Χρήση άλλης συσκευής"; "screen_login_subtitle" = "Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία."; +"screen_notification_settings_mentions_section_title" = "Αναφορές"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Προσπάθησε ξανά"; +"screen_recovery_key_change_generate_key_description" = "Μην το μοιραστείς με κανέναν!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Αποκλεισμός χρήστη"; +"screen_reset_encryption_password_placeholder" = "Εισαγωγή..."; "screen_room_attachment_source_camera_photo" = "Τράβηξε φωτογραφία"; "screen_room_change_permissions_everyone" = "Όλοι"; "screen_room_change_permissions_member_moderation" = "Συντονισμός μελών"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Διαχειριστές"; "screen_room_change_role_section_moderators" = "Συντονιστές"; "screen_room_change_role_section_users" = "Μέλη"; +"screen_room_change_role_unsaved_changes_title" = "Αποθήκευση αλλαγών;"; "screen_room_details_invite_people_title" = "Πρόσκληση ατόμων"; "screen_room_details_leave_conversation_title" = "Αποχώρηση από τη συζήτηση"; "screen_room_details_leave_room_title" = "Αποχώρηση από το δωμάτιο"; +"screen_room_details_notification_title" = "Ειδοποιήσεις"; "screen_room_details_roles_and_permissions" = "Ρόλοι και δικαιώματα"; "screen_room_details_room_name_label" = "Όνομα δωματίου"; "screen_room_details_security_title" = "Ασφάλεια"; "screen_room_details_topic_title" = "Θέμα"; "screen_room_error_failed_processing_media" = "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Αφαίρεση και αποκλεισμός μέλους"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Μόνο αναφορές και λέξεις-κλειδιά"; +"screen_room_timeline_reactions_show_less" = "Εμφάνιση λιγότερων"; "screen_roomlist_filter_people" = "Άτομα"; +"screen_server_confirmation_change_server" = "Αλλαγή παρόχου λογαριασμού"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Αποσύνδεση"; "screen_signout_confirmation_dialog_title" = "Αποσύνδεση"; +"screen_signout_key_backup_offline_title" = "Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"; "screen_signout_preference_item" = "Αποσύνδεση"; +"screen_signout_save_recovery_key_title" = "Έχεις αποθηκεύσει το κλειδί ανάκτησης;"; +"troubleshoot_notifications_entry_point_title" = "Αντιμετώπιση προβλημάτων ειδοποιήσεων"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index ad6dae5baa..4200cde50f 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pause"; "a11y_pin_field" = "PIN field"; "a11y_play" = "Play"; -"a11y_poll" = "Poll"; "a11y_poll_end" = "Ended poll"; "a11y_react_with" = "React with %1$@"; "a11y_react_with_other_emojis" = "React with other emojis"; @@ -41,6 +40,7 @@ "action_create" = "Create"; "action_create_a_room" = "Create a room"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Decline"; "action_delete_poll" = "Delete Poll"; "action_disable" = "Disable"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Forgot password?"; "action_forward" = "Forward"; "action_go_back" = "Go back"; +"action_ignore" = "Ignore"; "action_invite" = "Invite"; "action_invite_friends" = "Invite people"; "action_invite_friends_to_app" = "Invite people to %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Leave"; "action_leave_conversation" = "Leave conversation"; "action_leave_room" = "Leave room"; +"action_load_more" = "Load more"; "action_manage_account" = "Manage account"; "action_manage_devices" = "Manage devices"; "action_message" = "Message"; @@ -93,6 +95,7 @@ "action_send_message" = "Send message"; "action_share" = "Share"; "action_share_link" = "Share link"; +"action_show" = "Show"; "action_sign_in_again" = "Sign in again"; "action_signout" = "Sign out"; "action_signout_anyway" = "Sign out anyway"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "View source"; "action_yes" = "Yes"; -"action.load_more" = "Load more"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "About"; "common_acceptable_use_policy" = "Acceptable use policy"; "common_advanced_settings" = "Advanced settings"; @@ -133,10 +134,12 @@ "common_dark" = "Dark"; "common_decryption_error" = "Decryption error"; "common_developer_options" = "Developer options"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Direct chat"; "common_edited_suffix" = "(edited)"; "common_editing" = "Editing"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryption enabled"; "common_enter_your_pin" = "Enter your PIN"; "common_error" = "Error"; @@ -147,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "File"; "common_forward_message" = "Forward message"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Image"; "common_in_reply_to" = "In reply to %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Mute"; "common_no_results" = "No results"; +"common_no_room_name" = "No room name"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "or"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalink"; "common_permission" = "Permission"; "common_please_wait" = "Please wait…"; +"common_poll_end_confirmation" = "Are you sure you want to end this poll?"; +"common_poll_summary" = "Poll: %1$@"; "common_poll_total_votes" = "Total votes: %1$@"; "common_poll_undisclosed_text" = "Results will show after the poll has ended"; "common_privacy_policy" = "Privacy policy"; @@ -200,6 +207,7 @@ "common_settings" = "Settings"; "common_shared_location" = "Shared location"; "common_signing_out" = "Signing out"; +"common_something_went_wrong" = "Something went wrong"; "common_starting_chat" = "Starting chat…"; "common_sticker" = "Sticker"; "common_success" = "Success"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "What is this room about?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Unable to decrypt"; +"common_unable_to_decrypt_no_access" = "You don't have access to this message"; "common_unable_to_invite_message" = "Invites couldn't be sent to one or more users."; "common_unable_to_invite_title" = "Unable to send invite(s)"; "common_unlock" = "Unlock"; @@ -221,23 +230,30 @@ "common_username" = "Username"; "common_verification_cancelled" = "Verification cancelled"; "common_verification_complete" = "Verification complete"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verify device"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Voice message"; "common_waiting" = "Waiting…"; "common_waiting_for_decryption_key" = "Waiting for this message"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Send to"; -"common_no_room_name" = "No room name"; -"common_poll_end_confirmation" = "Are you sure you want to end this poll?"; -"common_poll_summary" = "Poll: %1$@"; -"common_something_went_wrong" = "Something went wrong"; -"common_unable_to_decrypt_no_access" = "You don't have access to this message"; -"common_verify_device" = "Verify device"; -"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; -"confirm_recovery_key_banner_title" = "Enter your recovery key"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; +"confirm_recovery_key_banner_title" = "Your key storage is out of sync"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; "dialog_permission_generic" = "Please grant the permission in the system settings."; "dialog_permission_location_description_ios" = "Grant access in Settings -> Location."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Silent notifications"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** Failed to send - please open room"; -"notification_invitation_action_reject" = "Reject"; "notification_invite_body" = "Invited you to chat"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Mentioned you: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Unindent"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Add attachment"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Change account provider"; "screen_account_provider_form_hint" = "Homeserver address"; "screen_account_provider_form_notice" = "Enter a search term or a domain address."; "screen_account_provider_form_subtitle" = "Search for a company, community, or private server."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "You’re about to create an account on %@"; "screen_advanced_settings_developer_mode" = "Developer mode"; "screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Disable the rich text editor to type Markdown manually."; "screen_advanced_settings_send_read_receipts" = "Read receipts"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -426,14 +458,16 @@ "screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@"; "screen_change_server_subtitle" = "What is the address of your server?"; "screen_change_server_title" = "Select your server"; -"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; +"screen_chat_backup_key_backup_action_disable" = "Delete key storage"; "screen_chat_backup_key_backup_action_enable" = "Turn on backup"; -"screen_chat_backup_key_backup_description" = "Backup ensures that you don't lose your message history. %1$@."; -"screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@."; +"screen_chat_backup_key_backup_title" = "Key storage"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Change recovery key"; -"screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; -"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; -"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; +"screen_chat_backup_recovery_action_confirm_description" = "Your key storage is currently out of sync."; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,17 +481,16 @@ "screen_create_poll_anonymous_desc" = "Show results only after poll ends"; "screen_create_poll_anonymous_headline" = "Hide votes"; "screen_create_poll_answer_hint" = "Option %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_poll_cancel_confirmation_title_ios" = "Cancel Poll"; "screen_create_poll_question_desc" = "Question or topic"; "screen_create_poll_question_hint" = "What is the poll about?"; "screen_create_poll_title" = "Create Poll"; "screen_create_room_action_create_room" = "New room"; "screen_create_room_error_creating_room" = "An error occurred when creating the room"; -"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards."; -"screen_create_room_private_option_title" = "Private room (invite only)"; -"screen_create_room_public_option_description" = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date."; -"screen_create_room_public_option_title" = "Public room (anyone)"; +"screen_create_room_private_option_description" = "Only people invited can access this room. All messages are end-to-end encrypted."; +"screen_create_room_private_option_title" = "Private room"; +"screen_create_room_public_option_description" = "Anyone can find this room.\nYou can change this anytime in room settings."; +"screen_create_room_public_option_title" = "Public room"; "screen_create_room_topic_label" = "Topic (optional)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; "screen_deactivate_account_delete_all_messages" = "Delete all my messages"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Updating profile…"; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "No Invites"; "screen_invites_invited_you" = "%1$@ (%2$@) invited you"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -509,10 +542,10 @@ "screen_key_backup_disable_confirmation_action_turn_off" = "Turn off"; "screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; "screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; -"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; -"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; -"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; -"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"; +"screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?"; "screen_login_error_deactivated_account" = "This account has been deactivated."; "screen_login_error_invalid_credentials" = "Incorrect username and/or password"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Group chats"; "screen_notification_settings_invite_for_me_label" = "Invitations"; "screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; -"screen_notification_settings_mentions_section_title" = "Mentions"; "screen_notification_settings_mode_all" = "All"; "screen_notification_settings_mode_mentions" = "Mentions"; "screen_notification_settings_notification_section_title" = "Notify me for"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_generate_key" = "Generate a new recovery key"; -"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; "screen_recovery_key_change_success" = "Recovery key changed"; "screen_recovery_key_change_title" = "Change recovery key?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Make sure nobody can see this screen!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Incorrect recovery key"; "screen_recovery_key_confirm_key_description" = "If you have a security key or security phrase, this will work too."; "screen_recovery_key_confirm_key_placeholder" = "Enter…"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Recovery key confirmed"; -"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; "screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_save_action" = "Save recovery key"; -"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_description" = "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe."; "screen_recovery_key_save_key_description" = "Tap to copy recovery key"; -"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key somewhere safe"; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; -"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_description" = "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."; "screen_recovery_key_setup_generate_key" = "Generate your recovery key"; -"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_title" = "Set up recovery"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Edit Moderators"; "screen_room_change_role_unsaved_changes_description" = "You have unsaved changes."; -"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_add_topic_title" = "Add topic"; "screen_room_details_already_a_member" = "Already a member"; "screen_room_details_already_invited" = "Already invited"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Failed unmuting this room, please try again."; "screen_room_details_notification_mode_custom" = "Custom"; "screen_room_details_notification_mode_default" = "Default"; -"screen_room_details_notification_title" = "Notifications"; "screen_room_details_share_room_title" = "Share room"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "Updating room…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Unblock"; "screen_room_member_details_unblock_alert_description" = "You'll be able to see all messages from them again."; "screen_room_member_details_unblock_user" = "Unblock user"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Ban"; "screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; "screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banning %1$@"; "screen_room_member_list_manage_member_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove" = "Remove from room"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; "screen_room_member_list_manage_member_unban_action" = "Unban"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Show less"; "screen_room_timeline_message_copied" = "Message copied"; "screen_room_timeline_no_permission_to_post" = "You do not have permission to post to this room"; -"screen_room_timeline_reactions_show_less" = "Show less"; "screen_room_timeline_reactions_show_more" = "Show more"; "screen_room_timeline_read_marker_title" = "New"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Mark as read"; "screen_roomlist_mark_as_unread" = "Mark as unread"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "Change account provider"; "screen_server_confirmation_message_login_element_dot_io" = "A private server for Element employees."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix is an open network for secure, decentralised communication."; "screen_server_confirmation_message_register" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; @@ -803,13 +830,21 @@ "screen_session_verification_compare_numbers_title" = "Compare numbers"; "screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."; "screen_session_verification_enter_recovery_key" = "Enter recovery key"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Prove it’s you in order to access your encrypted message history."; "screen_session_verification_open_existing_session_title" = "Open an existing session"; "screen_session_verification_positive_button_canceled" = "Retry verification"; "screen_session_verification_positive_button_initial" = "I am ready"; -"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match"; +"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match…"; "screen_session_verification_ready_subtitle" = "Compare a unique set of emojis."; "screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "They don’t match"; "screen_session_verification_they_match" = "They match"; "screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; "screen_signout_key_backup_disabled_title" = "You have turned off backup"; "screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; -"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; "screen_signout_key_backup_ongoing_title" = "Your keys are still being backed up"; "screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; "screen_signout_recovery_disabled_title" = "Recovery not set up"; "screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; -"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_view_location_title" = "Location"; "screen_welcome_bullet_1" = "Calls, polls, search and more will be added later this year."; @@ -919,7 +952,6 @@ "test_language_identifier" = "en"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "Poll"; +"banner_set_up_recovery_submit" = "Set up recovery"; "dialog_title_error" = "Error"; "dialog_title_success" = "Success"; "notification_fallback_content" = "Notification"; "notification_invitation_action_join" = "Join"; +"notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Mark as read"; "notification_room_action_quick_reply" = "Quick reply"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Everyone"; +"screen_account_provider_change" = "Change account provider"; "screen_account_provider_signin_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; "screen_account_provider_signup_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; "screen_analytics_settings_help_us_improve" = "Share anonymous usage data to help us identify issues."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "You'll be able to see all messages from them again."; "screen_blocked_users_unblock_alert_title" = "Unblock user"; "screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "Invite people"; "screen_create_room_room_name_label" = "Room name"; "screen_create_room_title" = "Create a room"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Edit poll"; "screen_identity_use_another_device" = "Use another device"; "screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication."; +"screen_notification_settings_mentions_section_title" = "Mentions"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Try again"; +"screen_recovery_key_change_generate_key_description" = "Do not share this with anyone!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Block user"; +"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_room_attachment_source_camera_photo" = "Take photo"; "screen_room_change_permissions_everyone" = "Everyone"; "screen_room_change_permissions_member_moderation" = "Member moderation"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Admins"; "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Members"; +"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_invite_people_title" = "Invite people"; "screen_room_details_leave_conversation_title" = "Leave conversation"; "screen_room_details_leave_room_title" = "Leave room"; +"screen_room_details_notification_title" = "Notifications"; "screen_room_details_roles_and_permissions" = "Roles and permissions"; "screen_room_details_room_name_label" = "Room name"; "screen_room_details_security_title" = "Security"; "screen_room_details_topic_title" = "Topic"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; +"screen_room_timeline_reactions_show_less" = "Show less"; "screen_roomlist_filter_people" = "People"; +"screen_server_confirmation_change_server" = "Change account provider"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Sign out"; "screen_signout_confirmation_dialog_title" = "Sign out"; +"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_preference_item" = "Sign out"; +"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/es.lproj/Localizable.strings b/ElementX/Resources/Localizations/es.lproj/Localizable.strings index 7a6f58f87c..ccbcbd2cdb 100644 --- a/ElementX/Resources/Localizations/es.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/es.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausar"; "a11y_pin_field" = "Campo PIN"; "a11y_play" = "Reproducir"; -"a11y_poll" = "Encuesta"; "a11y_poll_end" = "Encuesta finalizada"; "a11y_react_with" = "Reacciona con %1$@"; "a11y_react_with_other_emojis" = "Reacciona con otros emojis"; @@ -41,6 +40,7 @@ "action_create" = "Crear"; "action_create_a_room" = "Crear una sala"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Rechazar"; "action_delete_poll" = "Eliminar encuesta"; "action_disable" = "Desactivar"; @@ -54,6 +54,7 @@ "action_forgot_password" = "¿Olvidaste tu contraseña?"; "action_forward" = "Reenviar"; "action_go_back" = "Volver atrás"; +"action_ignore" = "Ignore"; "action_invite" = "Invitar"; "action_invite_friends" = "Invitar personas"; "action_invite_friends_to_app" = "Invita a alguien a %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Salir"; "action_leave_conversation" = "Salir de la conversación"; "action_leave_room" = "Salir de la sala"; +"action_load_more" = "Cargar más"; "action_manage_account" = "Gestionar cuenta"; "action_manage_devices" = "Administrar dispositivos"; "action_message" = "Message"; @@ -93,6 +95,7 @@ "action_send_message" = "Enviar mensaje"; "action_share" = "Compartir"; "action_share_link" = "Compartir enlace"; +"action_show" = "Show"; "action_sign_in_again" = "Inicia sesión de nuevo"; "action_signout" = "Cerrar sesión"; "action_signout_anyway" = "Cerrar sesión de todos modos"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Ver Fuente"; "action_yes" = "Sí"; -"action.load_more" = "Cargar más"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Acerca de"; "common_acceptable_use_policy" = "Política de uso aceptable"; "common_advanced_settings" = "Ajustes avanzados"; @@ -133,10 +134,12 @@ "common_dark" = "Oscuro"; "common_decryption_error" = "Error de descifrado"; "common_developer_options" = "Opciones de desarrollador"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Chat directo"; "common_edited_suffix" = "(editado)"; "common_editing" = "Edición"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Cifrado activado"; "common_enter_your_pin" = "Introduce tu PIN"; "common_error" = "Error"; @@ -147,6 +150,7 @@ "common_favourited" = "Marcado como favorito"; "common_file" = "Archivo"; "common_forward_message" = "Reenviar mensaje"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Imagen"; "common_in_reply_to" = "En respuesta a %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderno"; "common_mute" = "Silenciar"; "common_no_results" = "No hay resultados"; +"common_no_room_name" = "No room name"; "common_offline" = "Sin conexión"; "common_optic_id_ios" = "Optic ID"; "common_or" = "o"; @@ -170,6 +175,8 @@ "common_permalink" = "Enlace permanente"; "common_permission" = "Permiso"; "common_please_wait" = "Please wait…"; +"common_poll_end_confirmation" = "¿Estás seguro de que quieres finalizar esta encuesta?"; +"common_poll_summary" = "Encuesta: %1$@"; "common_poll_total_votes" = "Total de votos: %1$@"; "common_poll_undisclosed_text" = "Los resultados se mostrarán una vez finalizada la encuesta"; "common_privacy_policy" = "Política de privacidad"; @@ -200,6 +207,7 @@ "common_settings" = "Ajustes"; "common_shared_location" = "Ubicación compartida"; "common_signing_out" = "Cerrando sesión"; +"common_something_went_wrong" = "Something went wrong"; "common_starting_chat" = "Iniciando chat…"; "common_sticker" = "Sticker"; "common_success" = "Terminado"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "¿De qué trata esta sala?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "No se puede descifrar"; +"common_unable_to_decrypt_no_access" = "You don't have access to this message"; "common_unable_to_invite_message" = "Las invitaciones no se pudieron enviar a uno o más usuarios."; "common_unable_to_invite_title" = "No se pudo enviar la(s) invitación(es)"; "common_unlock" = "Desbloquear"; @@ -221,23 +230,30 @@ "common_username" = "Usuario"; "common_verification_cancelled" = "Verificación cancelada"; "common_verification_complete" = "Verificación completada"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verificar dispositivo"; +"common_verify_identity" = "Verify identity"; "common_video" = "Vídeo"; "common_voice_message" = "Mensaje de voz"; "common_waiting" = "Esperando…"; "common_waiting_for_decryption_key" = "Esperando este mensaje"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Send to"; -"common_no_room_name" = "No room name"; -"common_poll_end_confirmation" = "¿Estás seguro de que quieres finalizar esta encuesta?"; -"common_poll_summary" = "Encuesta: %1$@"; -"common_something_went_wrong" = "Something went wrong"; -"common_unable_to_decrypt_no_access" = "You don't have access to this message"; -"common_verify_device" = "Verificar dispositivo"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "La copia de seguridad del chat no está sincronizada en este momento. Debes confirmar tu clave de recuperación para mantener el acceso a la copia de seguridad del chat."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Confirma tu clave de recuperación"; "crash_detection_dialog_content" = "%1$@ se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Para permitir que la aplicación utilice la cámara, por favor concede el permiso en los ajustes del sistema."; "dialog_permission_generic" = "Por favor concede el permiso en los ajustes del sistema."; "dialog_permission_location_description_ios" = "Concede el permiso en Configuración -> Ubicación."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Notificaciones silenciosas"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** No se ha podido enviar - por favor, abre la sala"; -"notification_invitation_action_reject" = "Rechazar"; "notification_invite_body" = "Te invitó a chatear"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Te mencionó: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Quitar sangría"; "rich_text_editor_url_placeholder" = "Enlace"; "rich_text_editor_a11y_add_attachment" = "Adjuntar archivo"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "URL base personalizada de Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Define una URL base personalizada para Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL no válida, asegúrate de incluir el protocolo (http/https) y la dirección correcta."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Cambiar el proveedor de la cuenta"; "screen_account_provider_form_hint" = "Dirección del servidor principal"; "screen_account_provider_form_notice" = "Introduzca un término de búsqueda o una dirección de dominio."; "screen_account_provider_form_subtitle" = "Busca una empresa, comunidad o servidor privado."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Estás a punto de crear una cuenta en %@"; "screen_advanced_settings_developer_mode" = "Modo desarrollador"; "screen_advanced_settings_developer_mode_description" = "Habilita para tener acceso a características y funcionalidades para desarrolladores."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Desactiva el editor de texto enriquecido para escribir Markdown manualmente."; "screen_advanced_settings_send_read_receipts" = "Confirmaciones de lectura"; "screen_advanced_settings_send_read_receipts_description" = "Si se desactiva, las confirmaciones de lectura no se enviarán a nadie. Seguirás recibiendo confirmaciones de lectura de otros usuarios."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Activar copia de seguridad"; "screen_chat_backup_key_backup_description" = "La copia de seguridad garantiza que no pierdas tu historial de mensajes. %1$@."; "screen_chat_backup_key_backup_title" = "Copia de seguridad"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Cambiar la clave de recuperación"; -"screen_chat_backup_recovery_action_confirm" = "Confirmar clave de recuperación"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "La copia de seguridad de tus chats no está sincronizada ahora mismo."; -"screen_chat_backup_recovery_action_setup" = "Configurar la clave de recuperación"; "screen_chat_backup_recovery_action_setup_description" = "Accede a tus mensajes cifrados si pierdes todos tus dispositivos o cierras sesión de %1$@ en cualquier lugar."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Mostrar los resultados solo después de que finalice la encuesta"; "screen_create_poll_anonymous_headline" = "Ocultar votos"; "screen_create_poll_answer_hint" = "Opción %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Tus cambios no se guardarán"; "screen_create_poll_cancel_confirmation_title_ios" = "Cancelar Encuesta"; "screen_create_poll_question_desc" = "Pregunta o tema"; "screen_create_poll_question_hint" = "¿De qué trata la encuesta?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Actualizando perfil..."; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "Sin invitaciones"; "screen_invites_invited_you" = "%1$@ (%2$@) te invitó"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Chats grupales"; "screen_notification_settings_invite_for_me_label" = "Invitaciones"; "screen_notification_settings_mentions_only_disclaimer" = "Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas."; -"screen_notification_settings_mentions_section_title" = "Menciones"; "screen_notification_settings_mode_all" = "Todos"; "screen_notification_settings_mode_mentions" = "Menciones"; "screen_notification_settings_notification_section_title" = "Notificarme para"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Obtén una nueva clave de recuperación si has perdido la que tenías. Después de cambiar la clave de recuperación, la anterior dejará de funcionar."; "screen_recovery_key_change_generate_key" = "Generar una nueva clave de recuperación"; -"screen_recovery_key_change_generate_key_description" = "Asegúrate de poder guardar su clave de recuperación en un lugar seguro"; "screen_recovery_key_change_success" = "Clave de recuperación cambiada"; "screen_recovery_key_change_title" = "¿Cambiar la clave de recuperación?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Ingresar..."; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Clave de recuperación confirmada"; -"screen_recovery_key_confirm_title" = "Confirma tu clave de recuperación"; "screen_recovery_key_copied_to_clipboard" = "Clave de recuperación copiada"; "screen_recovery_key_generating_key" = "Generando…"; "screen_recovery_key_save_action" = "Guardar clave de recuperación"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Editar moderadores"; "screen_room_change_role_unsaved_changes_description" = "Tienes cambios sin guardar."; -"screen_room_change_role_unsaved_changes_title" = "¿Guardar cambios?"; "screen_room_details_add_topic_title" = "Añadir tema"; "screen_room_details_already_a_member" = "Ya eres miembro"; "screen_room_details_already_invited" = "Ya estás invitado"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Error al dejar de silenciar esta sala, por favor inténtalo de nuevo."; "screen_room_details_notification_mode_custom" = "Personalizado"; "screen_room_details_notification_mode_default" = "Por defecto"; -"screen_room_details_notification_title" = "Notificaciones"; "screen_room_details_share_room_title" = "Compartir sala"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "Actualizando la sala..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Desbloquear"; "screen_room_member_details_unblock_alert_description" = "Podrás ver todos sus mensajes de nuevo."; "screen_room_member_details_unblock_user" = "Desbloquear usuario"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Prohibir"; "screen_room_member_list_ban_member_confirmation_description" = "No podrán volver a unirse a esta sala si son invitados."; "screen_room_member_list_ban_member_confirmation_title" = "¿Estás seguro de que quieres prohibir a este miembro?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Prohibiendo %1$@"; "screen_room_member_list_manage_member_ban" = "Eliminar y prohibir a un miembro"; "screen_room_member_list_manage_member_remove" = "Remover de la sala"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eliminar y prohibir miembro"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Solo eliminar miembro"; "screen_room_member_list_manage_member_remove_confirmation_title" = "¿Eliminar al miembro y prohibirle unirse en el futuro?"; "screen_room_member_list_manage_member_unban_action" = "Anular la prohibición"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Mostrar menos"; "screen_room_timeline_message_copied" = "Mensaje copiado"; "screen_room_timeline_no_permission_to_post" = "No tienes permiso para publicar en esta sala"; -"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_room_timeline_reactions_show_more" = "Mostrar más"; "screen_room_timeline_read_marker_title" = "Nuevos"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Marcar como leído"; "screen_roomlist_mark_as_unread" = "Marcar como no leído"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "Cambiar el proveedor de la cuenta"; "screen_server_confirmation_message_login_element_dot_io" = "Un servidor privado para los empleados de Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix es una red abierta para una comunicación segura y descentralizada."; "screen_server_confirmation_message_register" = "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Comparar números"; "screen_session_verification_complete_subtitle" = "Tu nueva sesión ya está verificada. Tienes acceso a tus mensajes cifrados y otros usuarios lo considerarán de confianza."; "screen_session_verification_enter_recovery_key" = "Introduzca la clave de recuperación"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Demuestra que eres tú para acceder a tu historial de mensajes cifrados."; "screen_session_verification_open_existing_session_title" = "Abrir una sesión existente"; "screen_session_verification_positive_button_canceled" = "Reintentar la verificación"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Esperando a que coincida"; "screen_session_verification_ready_subtitle" = "Compara un conjunto único de emojis."; "screen_session_verification_request_accepted_subtitle" = "Compara los emoji, asegurándote de que aparecen en el mismo orden."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "No coinciden"; "screen_session_verification_they_match" = "Coinciden"; "screen_session_verification_waiting_to_accept_subtitle" = "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."; "screen_signout_key_backup_disabled_title" = "Has desactivado la copia de seguridad"; "screen_signout_key_backup_offline_subtitle" = "Se estaba haciendo una copia de seguridad de tus claves cuando te desconectaste. Vuelve a conectarte para que se haga una copia de seguridad de tus claves antes de desconectarte."; -"screen_signout_key_backup_offline_title" = "Se está guardando una copia de seguridad de tus claves"; "screen_signout_key_backup_ongoing_subtitle" = "Espera a que se complete antes de cerrar sesión."; "screen_signout_key_backup_ongoing_title" = "Se sigue guardando una copia de seguridad de tus claves"; "screen_signout_recovery_disabled_subtitle" = "Estás a punto de cerrar tu última sesión. Si cierras sesión ahora, perderás el acceso a tus mensajes cifrados."; "screen_signout_recovery_disabled_title" = "La recuperación no está configurada"; "screen_signout_save_recovery_key_subtitle" = "Estás a punto de cerrar tu última sesión. Si cierras la sesión ahora, podrías perder el acceso a tus mensajes cifrados."; -"screen_signout_save_recovery_key_title" = "¿Has guardado tu clave de recuperación?"; "screen_start_chat_error_starting_chat" = "Se ha producido un error al intentar iniciar un chat"; "screen_view_location_title" = "Ubicación"; "screen_welcome_bullet_1" = "Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."; @@ -919,7 +952,6 @@ "test_language_identifier" = "es"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "Encuesta"; +"banner_set_up_recovery_submit" = "Configurar la recuperación"; "dialog_title_error" = "Error"; "dialog_title_success" = "Terminado"; "notification_fallback_content" = "Notificación"; "notification_invitation_action_join" = "Unirse"; +"notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Marcar como leído"; "notification_room_action_quick_reply" = "Respuesta rápida"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Todos"; +"screen_account_provider_change" = "Cambiar el proveedor de la cuenta"; "screen_account_provider_signin_subtitle" = "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos."; "screen_account_provider_signup_subtitle" = "Aquí es donde se alojarán tus conversaciones — justo como utilizarías un proveedor de correo electrónico para guardar tus correos electrónicos."; "screen_analytics_settings_help_us_improve" = "Compartir datos de uso anónimos para ayudarnos a identificar problemas."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Podrás ver todos sus mensajes de nuevo."; "screen_blocked_users_unblock_alert_title" = "Desbloquear usuario"; "screen_bug_report_rash_logs_alert_title" = "%1$@ se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?"; +"screen_chat_backup_recovery_action_confirm" = "Introduzca la clave de recuperación"; +"screen_chat_backup_recovery_action_setup" = "Configurar la recuperación"; +"screen_create_poll_cancel_confirmation_content_ios" = "Tus cambios no se guardarán"; "screen_create_room_add_people_title" = "Invitar personas"; "screen_create_room_room_name_label" = "Nombre de la sala"; "screen_create_room_title" = "Crear una sala"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Editar encuesta"; "screen_identity_use_another_device" = "Usar otro dispositivo"; "screen_login_subtitle" = "Matrix es una red abierta para una comunicación segura y descentralizada."; +"screen_notification_settings_mentions_section_title" = "Menciones"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Inténtalo de nuevo"; +"screen_recovery_key_change_generate_key_description" = "Asegúrate de que puedes guardar tu clave de recuperación en algún lugar seguro"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Bloquear usuario"; +"screen_reset_encryption_password_placeholder" = "Ingresar..."; "screen_room_attachment_source_camera_photo" = "Hacer foto"; "screen_room_change_permissions_everyone" = "Todos"; "screen_room_change_permissions_member_moderation" = "Moderación de miembros"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administradores"; "screen_room_change_role_section_moderators" = "Moderadores"; "screen_room_change_role_section_users" = "Miembros"; +"screen_room_change_role_unsaved_changes_title" = "¿Guardar cambios?"; "screen_room_details_invite_people_title" = "Invitar personas"; "screen_room_details_leave_conversation_title" = "Salir de la conversación"; "screen_room_details_leave_room_title" = "Salir de la sala"; +"screen_room_details_notification_title" = "Notificaciones"; "screen_room_details_roles_and_permissions" = "Roles y permisos"; "screen_room_details_room_name_label" = "Nombre de la sala"; "screen_room_details_security_title" = "Seguridad"; "screen_room_details_topic_title" = "Tema"; "screen_room_error_failed_processing_media" = "Error al procesar el contenido multimedia, por favor inténtalo de nuevo."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eliminar y prohibir a un miembro"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Únicamente Menciones y Palabras clave"; +"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_roomlist_filter_people" = "Personas"; +"screen_server_confirmation_change_server" = "Cambiar el proveedor de la cuenta"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Cerrar sesión"; "screen_signout_confirmation_dialog_title" = "Cerrar sesión"; +"screen_signout_key_backup_offline_title" = "Se sigue guardando una copia de seguridad de tus claves"; "screen_signout_preference_item" = "Cerrar sesión"; +"screen_signout_save_recovery_key_title" = "¿Has guardado tu clave de recuperación?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/et.lproj/Localizable.strings b/ElementX/Resources/Localizations/et.lproj/Localizable.strings index fdc0703ecb..abf2da5bbd 100644 --- a/ElementX/Resources/Localizations/et.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/et.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Peata"; "a11y_pin_field" = "PIN-koodi väli"; "a11y_play" = "Esita"; -"a11y_poll" = "Küsitlus"; "a11y_poll_end" = "Lõppenud küsitlus"; "a11y_react_with" = "Reageeri emotikoniga %1$@"; "a11y_react_with_other_emojis" = "Reageeri mõne muu emotikoniga"; @@ -33,14 +32,15 @@ "action_close" = "Sulge"; "action_complete_verification" = "Tee verifitseerimine lõpuni"; "action_confirm" = "Kinnita"; -"action_confirm_password" = "Korda salasõna"; +"action_confirm_password" = "Kinnita otsust oma salasõnaga"; "action_continue" = "Jätka"; "action_copy" = "Kopeeri"; "action_copy_link" = "Kopeeri link"; "action_copy_link_to_message" = "Kopeeri sõnumi link"; "action_create" = "Loo"; "action_create_a_room" = "Loo jututuba"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Eemalda konto"; +"action_deactivate_account" = "Eemalda konto kasutusest"; "action_decline" = "Keeldu"; "action_delete_poll" = "Kustuta küsitlus"; "action_disable" = "Lülita välja"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Kas unustasid salasõna?"; "action_forward" = "Edasta"; "action_go_back" = "Tagasi eelmisesse vaatesse"; +"action_ignore" = "Eira"; "action_invite" = "Kutsu"; "action_invite_friends" = "Kutsu osalejaid"; "action_invite_friends_to_app" = "Kutsu huvilisi kasutama rakendust %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Lahku"; "action_leave_conversation" = "Lahku vestlusest"; "action_leave_room" = "Lahku jututoast"; +"action_load_more" = "Näita veel"; "action_manage_account" = "Halda kasutajakontot"; "action_manage_devices" = "Halda seadmeid"; "action_message" = "Saada sõnum"; @@ -93,6 +95,7 @@ "action_send_message" = "Saada sõnum"; "action_share" = "Jaga"; "action_share_link" = "Jaga linki"; +"action_show" = "Näita"; "action_sign_in_again" = "Logi uuesti sisse"; "action_signout" = "Logi välja"; "action_signout_anyway" = "Ikkagi logi välja"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Vaata ajajoonel"; "action_view_source" = "Vaata lähtekoodi"; "action_yes" = "Jah"; -"action.load_more" = "Näita veel"; -"action_deactivate_account" = "Eemalda konto kasutusest"; "banner_migrate_to_native_sliding_sync_action" = "Logi välja ja uuenda"; "banner_migrate_to_native_sliding_sync_description" = "Sinu koduserver toetab uut ja kiiremat protokolli. Uuendamiseks logi korraks rakendusest välja ja siis tagasi. Mingil hetkel tulevikus vana protokoll eemaldatakse kasutusest ja tehes uuenduse nüüd väldid hilisemat sundkorras uuendust."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Sinu koduserver enam ei toeta vana protokolli. Jätkamaks rakenduse kasutamist palun logi välja ning seejärel tagasi."; "banner_migrate_to_native_sliding_sync_title" = "Saadaval on uuendus"; -"banner.set_up_recovery.content" = "Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."; -"banner.set_up_recovery.title" = "Seadista taastamine"; +"banner_set_up_recovery_content" = "Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele."; +"banner_set_up_recovery_title" = "Seadista taastamine"; "common_about" = "Rakenduse teave"; "common_acceptable_use_policy" = "Vastuvõetava kasutamise põhimõtted"; "common_advanced_settings" = "Täiendavad seadistused"; @@ -133,10 +134,12 @@ "common_dark" = "Tume"; "common_decryption_error" = "Dekrüptimisviga"; "common_developer_options" = "Arendaja valikud"; +"common_device_id" = "Seadme tunnus"; "common_direct_chat" = "Otsevestlus"; "common_edited_suffix" = "(muudetud)"; "common_editing" = "Muutmine"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Krüptimine"; "common_encryption_enabled" = "Krüptimine on kasutusel"; "common_enter_your_pin" = "Sisesta oma PIN-kood"; "common_error" = "Viga"; @@ -147,6 +150,7 @@ "common_favourited" = "Lemmikuks määratud"; "common_file" = "Fail"; "common_forward_message" = "Edasta sõnum"; +"common_frequently_used" = "Sagedasti kasutatud"; "common_gif" = "GIF"; "common_image" = "Pilt"; "common_in_reply_to" = "Vastuseks kasutajale %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Kaasaegne"; "common_mute" = "Summutatud"; "common_no_results" = "Otsingul pole tulemusi"; +"common_no_room_name" = "Jututoal puudub nimi"; "common_offline" = "Võrgust väljas"; "common_optic_id_ios" = "Optic ID"; "common_or" = "või"; @@ -170,6 +175,8 @@ "common_permalink" = "Püsilink"; "common_permission" = "Õigus"; "common_please_wait" = "Palun oota…"; +"common_poll_end_confirmation" = "Kas oled kindel, et soovid selle küsitluse lõpetada?"; +"common_poll_summary" = "Küsitlus: %1$@"; "common_poll_total_votes" = "Hääli kokku: %1$@"; "common_poll_undisclosed_text" = "Tulemused on näha peale küsitluse lõppemist"; "common_privacy_policy" = "Privaatsuspoliitika"; @@ -200,6 +207,7 @@ "common_settings" = "Seadistused"; "common_shared_location" = "Jagatud asukoht"; "common_signing_out" = "Logime välja"; +"common_something_went_wrong" = "Midagi läks valesti"; "common_starting_chat" = "Alustame vestlust…"; "common_sticker" = "Kleeps"; "common_success" = "Õnnestus"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Mis on selle jututoa mõte?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Dekrüptimine ei olnud võimalik"; +"common_unable_to_decrypt_no_access" = "Sul pole ligipääsu antud sõnumile"; "common_unable_to_invite_message" = "Kutset polnud võimalik saata ühele või enamale kasutajale."; "common_unable_to_invite_title" = "Kutse(te) saatmine ei õnnestunud"; "common_unlock" = "Eemalda lukustus"; @@ -221,23 +230,30 @@ "common_username" = "Kasutajanimi"; "common_verification_cancelled" = "Verifitseerimine on katkestatud"; "common_verification_complete" = "Verifitseerimine on tehtud"; +"common_verification_failed" = "Verifitseerimine ei õnnestunud"; +"common_verified" = "Verifitseeritud"; +"common_verify_device" = "Verifitseeri seade"; +"common_verify_identity" = "Verifitseeri võrguidentiteet"; "common_video" = "Video"; "common_voice_message" = "Häälsõnum"; "common_waiting" = "Ootame…"; "common_waiting_for_decryption_key" = "Ootame selle sõnumi dekrüptimisvõtit"; +"common.copied_to_clipboard" = "Kopeeritud lõikelauale"; "common.do_not_show_this_again" = "Ära enam näita seda uuesti"; "common.open_source_licenses" = "Avatud lähtekoodiga litsentsid"; "common.pinned" = "Esiletõstetud"; "common.send_to" = "Saada kasutajale"; -"common_no_room_name" = "Jututoal puudub nimi"; -"common_poll_end_confirmation" = "Kas oled kindel, et soovid selle küsitluse lõpetada?"; -"common_poll_summary" = "Küsitlus: %1$@"; -"common_something_went_wrong" = "Midagi läks valesti"; -"common_unable_to_decrypt_no_access" = "Sul pole ligipääsu antud sõnumile"; -"common_verify_device" = "Verifitseeri seade"; -"confirm_recovery_key_banner_message" = "Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti."; -"confirm_recovery_key_banner_title" = "Sisesta oma taastevõti"; +"common.you" = "Sina"; +"common_unable_to_decrypt_insecure_device" = "Saadetud ebaturvalisest seadmest"; +"common_unable_to_decrypt_verification_violation" = "Saatja verifitseeritud identiteet on muutunud"; +"confirm_recovery_key_banner_message" = "Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti."; +"confirm_recovery_key_banner_primary_button_title" = "Sisesta oma taastevõti"; +"confirm_recovery_key_banner_secondary_button_title" = "Kas unustasid oma taastevõtme?"; +"confirm_recovery_key_banner_title" = "Sinu võtmehoidla pole sünkroonis"; "crash_detection_dialog_content" = "%1$@ jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"; +"crypto_identity_change_pin_violation" = "Kasutaja %1$@ võrguidentiteet tundub olema muutunud. %2$@"; +"crypto_identity_change_pin_violation_new" = "Kasutaja %1$@ %2$@ võrguidentiteet tundub olema muutunud. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Selleks, et rakendus saaks kaamerat kasutada, palun luba see süsteemi seadistuses."; "dialog_permission_generic" = "Palun luba süsteemi seadistustest vajalikud õigused."; "dialog_permission_location_description_ios" = "Luba õigused Seadistused -> Asukoht valikust."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Vaiksed teavitused"; "notification_incoming_call" = "Sissetulev kõne"; "notification_inline_reply_failed" = "** Saatmine ei õnnestunud - palun ava jututoa täisvaade"; -"notification_invitation_action_reject" = "Lükka tagasi"; "notification_invite_body" = "Kutse osalema vestluses"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ saatus sulle vestluskutse"; "notification_mentioned_you_body" = "Mainis sind: %1$@"; "notification_new_messages" = "Uued sõnumid"; "notification_reaction_body" = "Reageeris nii: %1$@"; "notification_room_invite_body" = "Saatis sulle kutse jututuppa"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ saatis sulle kutse jututoaga liitumiseks"; "notification_sender_me" = "Mina"; "notification_sender_mention_reply" = "%1$@ mainis või vastas"; "notification_test_push_notification_content" = "See ongi teavitus! Klõpsi mind!"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Eemalda taandrida"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Lisa manus"; +"rich_text_editor_composer_caption_placeholder" = "Pealkiri, kui soovid lisada…"; "screen_advanced_settings_element_call_base_url" = "Element Calli kohandatud teenuseaadress"; "screen_advanced_settings_element_call_base_url_description" = "Seadista kohandatud teenuseaadress Element Calli jaoks."; "screen_advanced_settings_element_call_base_url_validation_error" = "Vigane url. Palun vaata, et url algaks protokolliga (http/https) ning aadress ise oleks ka õige."; +"screen_create_room_room_address_section_footer" = "Selleks, et see jututuba oleks nähtav jututubade avalikus kataloogis, sa vajad jututoa aadressi."; +"screen_create_room_room_address_section_title" = "Jututoa aadress"; +"screen_create_room_room_visibility_section_title" = "Jututoa nähtavus"; +"screen_create_room_access_section_anyone_option_description" = "Kõik võivad selle jututoaga liituda"; +"screen_create_room_access_section_anyone_option_title" = "Kõik"; +"screen_create_room_access_section_header" = "Ligipääs jututoale"; +"screen_create_room_access_section_knocking_option_description" = "Kõik võivad paluda selle jututoaga liitumist, kuid peakasutaja või moderaator peavad selle kinnitama"; +"screen_create_room_access_section_knocking_option_title" = "Küsi võimalust liitumiseks"; +"screen_join_room_cancel_knock_action" = "Tühista liitumispalve"; +"screen_join_room_cancel_knock_alert_confirmation" = "Jah, tühista"; +"screen_join_room_cancel_knock_alert_description" = "Kas sa oled kindel, et soovid tühistada oma palve jututoaga liitumiseks?"; +"screen_join_room_cancel_knock_alert_title" = "Tühista liitumispalve"; +"screen_join_room_knock_message_description" = "Selgitus (kui soovid lisada)"; +"screen_join_room_knock_sent_description" = "Kui sinu liitumispalvega ollakse nõus, siis saad kutse jututoaga liitumiseks."; +"screen_join_room_knock_sent_title" = "Liitumispalve on saadetud"; "screen_pinned_timeline_empty_state_description" = "Siia lisamiseks vajuta sõnumil ja vali „%1$@“."; "screen_pinned_timeline_empty_state_headline" = "Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile"; -"screen_pinned_timeline_screen_title_empty" = "Esiletõstetud sõnumid"; "screen_reset_encryption_password_error" = "Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Unusta verifitseerimine ja saada ikkagi"; "screen_resolve_send_failure_changed_identity_subtitle" = "Sa võid jätta verifitseerimisvea tähelepanuta ja sõnumi ikkagi saata või katkestad saatmise ja peale kasutaja %1$@ verifitseerimist proovid seda uuesti."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Laadime sõnumit…"; "screen_room_pinned_banner_view_all_button_title" = "Näita kõiki"; "screen_room_details_pinned_events_row_title" = "Esiletõstetud sõnumid"; +"screen_roomlist_knock_event_sent_description" = "Liitumispäring on saadetud"; "screen_timeline_item_menu_send_failure_changed_identity" = "Sõnum on saatmata, kuna kasutaja %1$@ verifitseeritud identiteet on muutunud."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Sõnum on saatmata, kuna %1$@ pole verifitseerinud kõiki oma seadmeid."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata."; -"screen_account_provider_change" = "Muuda teenusepakkujat"; "screen_account_provider_form_hint" = "Koduserveri aadress"; "screen_account_provider_form_notice" = "Sisesta otsingusõna või domeeni nimi."; "screen_account_provider_form_subtitle" = "Otsi äriühingut, kogukonda või võrgus leiduvat Matrixi serverit."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Sa oled loomas kasutajakontot %@ teenuses"; "screen_advanced_settings_developer_mode" = "Arendaja valikud"; "screen_advanced_settings_developer_mode_description" = "Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud valikud."; +"screen_advanced_settings_media_compression_description" = "Sellega laadid fotosid ja videoid kiiremini üles ning vähendad andmemahtu"; +"screen_advanced_settings_media_compression_title" = "Optimeeri meedia kvaliteeti"; "screen_advanced_settings_rich_text_editor_description" = "Kui soovid Markdown-vormingut käsitsi lisada, siis lülita vormindatud teksti toimeti välja."; "screen_advanced_settings_send_read_receipts" = "Lugemisteatised"; "screen_advanced_settings_send_read_receipts_description" = "Kui lülitad selle valiku välja, siis mitte keegi enam ei saa sinult lugemisteatisi. Küll aga saad sina teiste kasutajate lugemisteatisi."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Vali oma server"; "screen_chat_backup_key_backup_action_disable" = "Lülita võtmete varundamine välja"; "screen_chat_backup_key_backup_action_enable" = "Lülita võtmete varundamine sisse"; -"screen_chat_backup_key_backup_description" = "Varundamine tagab, et sinu sõnumite ajalugu on alati loetav. %1$@."; -"screen_chat_backup_key_backup_title" = "Varundus"; +"screen_chat_backup_key_backup_description" = "Salvesta oma krüptoidentiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes. %1$@."; +"screen_chat_backup_key_backup_title" = "Krüptovõtmete varundus"; +"screen_chat_backup_key_storage_disabled_error" = "Taastamise seadistamiseks peab võtmehoidla olema sisselülitatud."; +"screen_chat_backup_key_storage_toggle_description" = "Laadi siin seadmes leiduvad võtmed üles"; +"screen_chat_backup_key_storage_toggle_title" = "Luba krüptovõtmete salvestamine"; "screen_chat_backup_recovery_action_change" = "Muuda taastevõtit"; -"screen_chat_backup_recovery_action_confirm" = "Sisesta oma taastevõti"; -"screen_chat_backup_recovery_action_confirm_description" = "Sinu vestluste krüptograafia varukoopia pole hetkel enam sünkroonis."; -"screen_chat_backup_recovery_action_setup" = "Seadista krüptovõtmete varundus"; +"screen_chat_backup_recovery_action_change_description" = "Kui sa oled kaotanud ligipääsu kõikidele oma olemasolevatele seadmetele, siis sa saad taastevõtme abil taastada ligipääsu oma krüptoidentiteedile ja sõnumite ajaloole."; +"screen_chat_backup_recovery_action_confirm_description" = "Sinu krüptovõtmete varundus pole hetkel enam sünkroonis."; "screen_chat_backup_recovery_action_setup_description" = "Säilita ligipääs oma krüptitud sõnumitele ka siis, kui sa kaotad kõik oma seadmed ja/või logid kõikjal välja rakendusest %1$@."; "screen_create_account_title" = "Loo kasutajakonto"; "screen_create_new_recovery_key_list_item_1" = "Ava %1$@ töölauaga seadmes"; @@ -447,28 +481,27 @@ "screen_create_poll_anonymous_desc" = "Näita tulemusi alles pärast küsitluse lõppu"; "screen_create_poll_anonymous_headline" = "Peida hääled"; "screen_create_poll_answer_hint" = "Valik %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Sinu muudatused jäävad salvestamata"; "screen_create_poll_cancel_confirmation_title_ios" = "Tühista küsitlus"; "screen_create_poll_question_desc" = "Küsimus või teema"; "screen_create_poll_question_hint" = "Mis on küsitluse teema?"; "screen_create_poll_title" = "Loo küsitlus"; "screen_create_room_action_create_room" = "Uus jututuba"; "screen_create_room_error_creating_room" = "Jututoa loomisel tekkis viga"; -"screen_create_room_private_option_description" = "Sõnumid siin jututoas on krüptitud ja seda ei saa hiljem välja lülitada."; -"screen_create_room_private_option_title" = "Privaatne jututuba (liitumine vaid kutsega)"; -"screen_create_room_public_option_description" = "Sõnumid pole krüptitud ja neid saavad kõik lugeda. Soovi korral saad hiljem krüptimise sisse lülitada."; -"screen_create_room_public_option_title" = "Avalik jututuba (avatud kõigile)"; +"screen_create_room_private_option_description" = "Ligipääs siia jututuppa on vaid kutse alusel. Kõik sõnumid siin jututoas on läbivalt krüptitud."; +"screen_create_room_private_option_title" = "Privaatne jututuba"; +"screen_create_room_public_option_description" = "Kõik saavad seda jututuba leida.\nSa võid seda jututoa seadistustest alati muuta."; +"screen_create_room_public_option_title" = "Avalik jututuba"; "screen_create_room_topic_label" = "Teema (kui soovid lisada)"; "screen_deactivate_account_confirmation_dialog_content" = "Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest"; "screen_deactivate_account_delete_all_messages" = "Kustuta kõik minu sõnumid"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; +"screen_deactivate_account_delete_all_messages_notice" = "Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi."; +"screen_deactivate_account_description" = "Sinu konto kasutusest eemaldamine on %1$@ ja sellega:"; +"screen_deactivate_account_description_bold_part" = "pöördumatu"; +"screen_deactivate_account_list_item_1" = "Sinu kasutajakonto %1$@ (sa ei saa enam sellega võrku logida ning kasutajatunnust ei saa enam pruukida)."; +"screen_deactivate_account_list_item_1_bold_part" = "jäädavalt eemaldatakse kasutusest"; +"screen_deactivate_account_list_item_2" = "Sind logitakse välja kõikidest jututubadest."; +"screen_deactivate_account_list_item_3" = "Kustutatakse sinu andmed meie isikutuvastusserverist."; +"screen_deactivate_account_list_item_4" = "Sinu sõnumid on jätkuvalt nähtavad registreeritud kasutajatele, kuid kui otsustad sõnumid kustutada, siis nad nad pole nähtavad uutele ja registreerimata kasutajatele."; "screen_deactivate_account_title" = "Eemalda konto kasutusest"; "screen_edit_poll_delete_confirmation" = "Kas sa oled kindel, et soovid selle küsitluse kustutada?"; "screen_edit_profile_display_name" = "Kuvatav nimi"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Rühmavestlused"; "screen_notification_settings_invite_for_me_label" = "Kutsed"; "screen_notification_settings_mentions_only_disclaimer" = "Sinu koduserver ei toeta seda funktsionaalsust krüptitud jututubades ja seega ei pruugi kõik teavitused sinuni jõuda."; -"screen_notification_settings_mentions_section_title" = "Mainimised"; "screen_notification_settings_mode_all" = "Kõik"; "screen_notification_settings_mode_mentions" = "Mainimiste alusel"; "screen_notification_settings_notification_section_title" = "Teavita mind"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Vali %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "„Seo uus seade“"; "screen_qr_code_login_initial_state_item_4" = "Skaneeri QR-koodi selle seadmega"; +"screen_qr_code_login_initial_state_subtitle" = "See funktsionaalsus on sadaval vaid siis, kui sinu teenusepakkuja seda toetab."; "screen_qr_code_login_initial_state_title" = "QR-koodi saamiseks ava %1$@ oma teises seadmes"; "screen_qr_code_login_invalid_scan_state_description" = "Kasuta teises seadmes näidatavat QR-koodi"; "screen_qr_code_login_invalid_scan_state_subtitle" = "Vale QR-kood"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "Sinu verifitseerimiskood"; "screen_recovery_key_change_description" = "Kui oled vana taastevõtme kaotanud, siis loo uus. Peale seda muudatust vana taastevõti enam ei tööta."; "screen_recovery_key_change_generate_key" = "Loo uus taastevõti"; -"screen_recovery_key_change_generate_key_description" = "Palun hoia taastevõtit turvaliselt, näiteks vana kooli seifis või digitaalses salasõnalaekas"; "screen_recovery_key_change_success" = "Taastevõti on muudetud"; "screen_recovery_key_change_title" = "Kas muudame taastevõtme?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Loo uus taastevõti"; "screen_recovery_key_confirm_description" = "Palun vaata, et keegi teine ei näeks seda ekraanivaadet!"; -"screen_recovery_key_confirm_error_content" = "Kinnitamaks ligipääsu sinu vestluse varukoopiale, palun proovi uuesti"; +"screen_recovery_key_confirm_error_content" = "Kinnitamaks ligipääsu sinu krüptovõtmete varundusele, palun proovi uuesti"; "screen_recovery_key_confirm_error_title" = "Vigane taastevõti"; "screen_recovery_key_confirm_key_description" = "Kui sul on turvavõti või turvafraas, siis need toimivad ka."; "screen_recovery_key_confirm_key_placeholder" = "Sisesta..."; "screen_recovery_key_confirm_lost_recovery_key" = "Kas sa oled taastevõtme kaotanud?"; "screen_recovery_key_confirm_success" = "Taastevõti on kinnitatud"; -"screen_recovery_key_confirm_title" = "Sisesta oma taastevõti"; "screen_recovery_key_copied_to_clipboard" = "Taastevõti on kopeeritud lõikelauale"; "screen_recovery_key_generating_key" = "Loome..."; "screen_recovery_key_save_action" = "Salvesta taastevõti"; -"screen_recovery_key_save_description" = "Palun märgi taastevõti üles ja hoia seda turvaliselt, näiteks vana kooli seifis või digitaalses salasõnalaekas"; +"screen_recovery_key_save_description" = "Palun märgi taastevõti üles ja hoia seda turvaliselt, näiteks digitaalses salasõnalaekas, krüptitud märkmetes või vana kooli seifis."; "screen_recovery_key_save_key_description" = "Taastevõtme kopeerimiseks puuduta"; "screen_recovery_key_save_title" = "Salvesta oma taastevõti"; "screen_recovery_key_setup_confirmation_description" = "Peale seda sammu sul pole enam ligipääsu oma taastevõtmele."; "screen_recovery_key_setup_confirmation_title" = "Kas sa oled oma taastevõtme talletanud?"; "screen_recovery_key_setup_description" = "Sinu vestluste varundus on krüptitud taastevõtmega. Kui peale muutusi peaks vaja olema uut taastevõtit, siis palun kasuta valikut „Loo taastevõti“"; "screen_recovery_key_setup_generate_key" = "Loo oma taastevõti"; -"screen_recovery_key_setup_generate_key_description" = "Palun hoia taastevõtit turvaliselt, näiteks vana kooli seifis või digitaalses salasõnalaekas"; +"screen_recovery_key_setup_generate_key_description" = "Ära jaga seda kellegagi"; "screen_recovery_key_setup_success" = "Andmete taastamise seadistamine õnnestus"; "screen_recovery_key_setup_title" = "Seadista andmete taastamine"; "screen_report_content_block_user_hint" = "Vali see eelistus, kui sa soovid peita selle kasutaja kõik senised ja tulevased sõnumid"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Jah, lähtesta nüüd"; "screen_reset_encryption_confirmation_alert_subtitle" = "See tegevus on tagasipöördumatu."; "screen_reset_encryption_confirmation_alert_title" = "Kas sa oled kindel, et soovid oma andmete krüptimist lähtestada?"; -"screen_reset_encryption_password_placeholder" = "Sisesta..."; "screen_reset_encryption_password_subtitle" = "Palun kinnita, et soovid oma andmete krüptimist lähtestada."; "screen_reset_encryption_password_title" = "Jätkamaks sisesta oma kasutajakonto salasõna"; "screen_reset_identity_confirmation_subtitle" = "Oma võrguidentiteedi lähtestamiseks suuname sind %1$@ kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Peakasutajatel on automaatselt ka moderaatori õigused"; "screen_room_change_role_moderators_title" = "Muuda moderaatoreid"; "screen_room_change_role_unsaved_changes_description" = "Sul on salvestamata muudatusi"; -"screen_room_change_role_unsaved_changes_title" = "Kas salvestame muudatused?"; "screen_room_details_add_topic_title" = "Lisa teema"; "screen_room_details_already_a_member" = "Sa juba oled jututoa liige"; "screen_room_details_already_invited" = "Sa juba oled kutse saanud"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Selle jututoa summutamise eemaldamine ei õnnestunud. Palun proovi uuesti."; "screen_room_details_notification_mode_custom" = "Kohandatud"; "screen_room_details_notification_mode_default" = "Vaikimisi"; -"screen_room_details_notification_title" = "Teavitused"; "screen_room_details_share_room_title" = "Jaga jututuba"; "screen_room_details_title" = "Jututoa teave"; "screen_room_details_updating_room" = "Uuendame jututuba…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Eemalda blokeering"; "screen_room_member_details_unblock_alert_description" = "Nüüd näed sa jälle kõiki tema sõnumeid"; "screen_room_member_details_unblock_user" = "Eemalda kasutajalt blokeering"; +"screen_room_member_details_verify_button_subtitle" = "Kasutaja verifitseerimiseks kasuta veebirakendust."; +"screen_room_member_details_verify_button_title" = "Verifitseeri kasutaja %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Sea suhtluskeeld"; "screen_room_member_list_ban_member_confirmation_description" = "Ta ei saa selle jututoaga liituda isegi kutse olemasolul."; "screen_room_member_list_ban_member_confirmation_title" = "Kas sa oled kindel, et soovid sellele kasutajale seada suhtluskeelu?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Seame kasutajale %1$@ suhtluskeelu"; "screen_room_member_list_manage_member_ban" = "Eemalda ja sea suhtluskeeld"; "screen_room_member_list_manage_member_remove" = "Eemalda kasutaja jututoast"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eemalda kasutaja jututoast ja sea talle suhtluskeeld"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Ainult eemalda kasutaja"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Kas eemaldama kasutaja ja seame talle tulevikuks suhtluskeelu?"; "screen_room_member_list_manage_member_unban_action" = "Eemalda suhtluskeeld"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Näita vähem"; "screen_room_timeline_message_copied" = "Sõnum on kopeeritud"; "screen_room_timeline_no_permission_to_post" = "Sul pole õigusi siia jututuppa kirjutada"; -"screen_room_timeline_reactions_show_less" = "Näita vähem"; "screen_room_timeline_reactions_show_more" = "Näita rohkem"; "screen_room_timeline_read_marker_title" = "Uus"; "screen_room_title" = "Vestlus"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Märgi loetuks"; "screen_roomlist_mark_as_unread" = "Märgi mitteloetuks"; "screen_roomlist_room_directory_button_title" = "Sirvi kõiki jututube"; -"screen_server_confirmation_change_server" = "Muuda teenusepakujat"; "screen_server_confirmation_message_login_element_dot_io" = "Privaatne server Elemendi töötajate jaoks."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."; "screen_server_confirmation_message_register" = "See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Võrdle numbreid"; "screen_session_verification_complete_subtitle" = "Sinu uus sessioon on nüüd verifitseeritud. Sellel sessioonil on nüüd ligipääs sinu krüptitud sõnumitele ja teised osapooled näevad teda usaldusväärsena."; "screen_session_verification_enter_recovery_key" = "Sisesta taastevõti"; +"screen_session_verification_failed_subtitle" = "Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus."; "screen_session_verification_open_existing_session_subtitle" = "Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga."; "screen_session_verification_open_existing_session_title" = "Ava olemasolev sessioon"; "screen_session_verification_positive_button_canceled" = "Proovi verifitseerimist uuesti"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Ootame kinnitust sobivusele"; "screen_session_verification_ready_subtitle" = "Võrdle unikaalset emojide kombinatsiooni"; "screen_session_verification_request_accepted_subtitle" = "Võrdle unikaalset emojide kombinatsiooni ning kontrolli, et nad on täpselt samas järjekorras."; +"screen_session_verification_request_details_timestamp" = "Sisselogitud"; +"screen_session_verification_request_failure_title" = "Verifitseerimine ei õnnestunud"; +"screen_session_verification_request_footer" = "Jätka vaid siis, kui sina algatasid verifitseerimise."; +"screen_session_verification_request_subtitle" = "Hoidmaks oma sõnumiajalugu turvatuna verifitseeri teine seade."; +"screen_session_verification_request_success_subtitle" = "Võid nüüd sõnumeid oma teises seadmes turvaliselt saata ja vastu võtta."; +"screen_session_verification_request_success_title" = "Seade on verifitseeritud"; +"screen_session_verification_request_title" = "Verifitseerimispäring"; "screen_session_verification_they_dont_match" = "Nad ei klapi omavahel"; "screen_session_verification_they_match" = "Nad klapivad omavahel"; "screen_session_verification_waiting_to_accept_subtitle" = "Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Oled oma viimasest seansist välja logimas. Kui logid nüüd välja, kaotad ligipääsu oma krüptitud sõnumitele."; "screen_signout_key_backup_disabled_title" = "Sa oled varukoopiate tegemise välja lülitanud"; "screen_signout_key_backup_offline_subtitle" = "Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis logi rakendusest välja."; -"screen_signout_key_backup_offline_title" = "Sinu krüptovõtmed on veel varundamisel"; "screen_signout_key_backup_ongoing_subtitle" = "Enne väljalogimist palun oota, et pooleliolev toiming lõppeb."; "screen_signout_key_backup_ongoing_title" = "Sinu krüptovõtmed on veel varundamisel"; "screen_signout_recovery_disabled_subtitle" = "Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis kaotad ligipääsu oma krüptitud sõnumitele."; "screen_signout_recovery_disabled_title" = "Andmete taastamine on seadistamata"; "screen_signout_save_recovery_key_subtitle" = "Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis ilmselt kaotad ligipääsu oma krüptitud sõnumitele."; -"screen_signout_save_recovery_key_title" = "Kas sa oled oma taastevõtme salvestanud?"; "screen_start_chat_error_starting_chat" = "Vestluse alustamisel tekkis viga"; "screen_view_location_title" = "Asukoht"; "screen_welcome_bullet_1" = "Kõned, küsitlused, otsing ja palju muud lisanduvad hiljem selle aasta jooksul."; @@ -919,7 +952,6 @@ "test_language_identifier" = "et"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Veaotsing"; -"troubleshoot_notifications_entry_point_title" = "Teavituste veaotsing"; "troubleshoot_notifications_screen_action" = "Käivita testid"; "troubleshoot_notifications_screen_action_again" = "Käivita testid uuesti"; "troubleshoot_notifications_screen_failure" = "Mõned testid tuvastasid vigu. Palun vaata üksikasjalikku teavet."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Palun veendu, et UnifiedPushi levitajad on saadaval."; "troubleshoot_notifications_test_unified_push_failure" = "Tõuketeenuse levitajaid ei leidu."; "troubleshoot_notifications_test_unified_push_title" = "Kontrolli UnifiedPushi"; +"a11y_poll" = "Küsitlus"; +"banner_set_up_recovery_submit" = "Seadista andmete taastamine"; "dialog_title_error" = "Viga"; "dialog_title_success" = "Õnnestus"; "notification_fallback_content" = "Teavitus"; "notification_invitation_action_join" = "Liitu"; +"notification_invitation_action_reject" = "Keeldu"; "notification_room_action_mark_as_read" = "Märgi loetuks"; "notification_room_action_quick_reply" = "Kiirvastus"; +"screen_pinned_timeline_screen_title_empty" = "Esiletõstetud sõnumid"; "screen_room_mentions_at_room_title" = "Kõik"; +"screen_account_provider_change" = "Muuda teenusepakkujat"; "screen_account_provider_signin_subtitle" = "See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."; "screen_account_provider_signup_subtitle" = "See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."; "screen_analytics_settings_help_us_improve" = "Võimalike rakenduse vigade leidmiseks palun jaga anonüümset kasutusteavet."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Nüüd näed sa jälle kõiki tema sõnumeid"; "screen_blocked_users_unblock_alert_title" = "Eemalda kasutajalt blokeering"; "screen_bug_report_rash_logs_alert_title" = "%1$@ jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"; +"screen_chat_backup_recovery_action_confirm" = "Sisesta taastevõti"; +"screen_chat_backup_recovery_action_setup" = "Seadista andmete taastamine"; +"screen_create_poll_cancel_confirmation_content_ios" = "Sinu tehtud muudatused jäävad salvestamata"; "screen_create_room_add_people_title" = "Kutsu osalejaid"; "screen_create_room_room_name_label" = "Jututoa nimi"; "screen_create_room_title" = "Loo jututuba"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Muuda küsitlust"; "screen_identity_use_another_device" = "Kasuta teist seadet"; "screen_login_subtitle" = "Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."; +"screen_notification_settings_mentions_section_title" = "Mainimiste alusel"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Proovi uuesti"; +"screen_recovery_key_change_generate_key_description" = "Ära jaga seda kellegagi"; +"screen_recovery_key_confirm_title" = "Sisesta oma taastevõti"; "screen_report_content_block_user" = "Blokeeri kasutaja"; +"screen_reset_encryption_password_placeholder" = "Sisesta..."; "screen_room_attachment_source_camera_photo" = "Tee pilt"; "screen_room_change_permissions_everyone" = "Kõik"; "screen_room_change_permissions_member_moderation" = "Jututoas osalejate modereerimine"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Peakasutajad"; "screen_room_change_role_section_moderators" = "Moderaatorid"; "screen_room_change_role_section_users" = "Liikmed"; +"screen_room_change_role_unsaved_changes_title" = "Kas salvestame muudatused?"; "screen_room_details_invite_people_title" = "Kutsu osalejaid"; "screen_room_details_leave_conversation_title" = "Lahku vestlusest"; "screen_room_details_leave_room_title" = "Lahku jututoast"; +"screen_room_details_notification_title" = "Teavitused"; "screen_room_details_roles_and_permissions" = "Rollid ja õigused"; "screen_room_details_room_name_label" = "Jututoa nimi"; "screen_room_details_security_title" = "Turvalisus"; "screen_room_details_topic_title" = "Teema"; "screen_room_error_failed_processing_media" = "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eemalda ja sea suhtluskeeld"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mainimiste ja võtmesõnade alusel"; +"screen_room_timeline_reactions_show_less" = "Näita vähem"; "screen_roomlist_filter_people" = "Inimesed"; +"screen_server_confirmation_change_server" = "Muuda teenusepakkujat"; +"screen_session_verification_request_failure_subtitle" = "Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus."; "screen_signout_confirmation_dialog_submit" = "Logi välja"; "screen_signout_confirmation_dialog_title" = "Logi välja"; +"screen_signout_key_backup_offline_title" = "Sinu krüptovõtmed on veel varundamisel"; "screen_signout_preference_item" = "Logi välja"; +"screen_signout_save_recovery_key_title" = "Kas sa oled oma taastevõtme talletanud?"; +"troubleshoot_notifications_entry_point_title" = "Teavituste veaotsing"; diff --git a/ElementX/Resources/Localizations/fa.lproj/Localizable.strings b/ElementX/Resources/Localizations/fa.lproj/Localizable.strings new file mode 100644 index 0000000000..e8cce2aae0 --- /dev/null +++ b/ElementX/Resources/Localizations/fa.lproj/Localizable.strings @@ -0,0 +1,1068 @@ +"Notification" = "آگاهی"; +"a11y_delete" = "حذف"; +"a11y_hide_password" = "نهفتن گذرواژه"; +"a11y_jump_to_bottom" = "به پایین بروید"; +"a11y_notifications_mentions_only" = "فقط اشاره‌ها"; +"a11y_notifications_muted" = "خموش"; +"a11y_page_n" = "صفحهٔ %1$d"; +"a11y_pause" = "مکث"; +"a11y_pin_field" = "زمینهٔ پین"; +"a11y_play" = "پخش"; +"a11y_poll_end" = "نظرسنجی پایان یافته"; +"a11y_react_with" = "واکنش با %1$@"; +"a11y_react_with_other_emojis" = "واکنش با شکلک‌های دیگر"; +"a11y_read_receipts_multiple" = "خوانده به دست %1$@ و %2$@"; +"a11y_read_receipts_single" = "خوانده به دست %1$@"; +"a11y_read_receipts_tap_to_show_all" = "زدن برای نمایش همه"; +"a11y_remove_reaction_with" = "برداشتن واکنش با %1$@"; +"a11y_send_files" = "ارسال پرونده‌ها"; +"a11y_show_password" = "نمایش گذرواژه"; +"a11y_start_call" = "آغاز یک تماس"; +"a11y_user_menu" = "فهرست کاربر"; +"a11y_voice_message_record" = "ضبط پیام صوتی."; +"a11y_voice_message_stop_recording" = "توقّف ضبط"; +"action_accept" = "پذیرش"; +"action_add_to_timeline" = "افزودن به خط زمانی"; +"action_back" = "بازگشت"; +"action_call" = "تماس"; +"action_cancel" = "لغو"; +"action_cancel_for_now" = "لغو برای امنون"; +"action_choose_photo" = "گزینش عکس"; +"action_clear" = "پاک سازی"; +"action_close" = "بستن"; +"action_complete_verification" = "تکمیل تأیید"; +"action_confirm" = "تأیید"; +"action_confirm_password" = "تأیید گذرواژه"; +"action_continue" = "ادامه"; +"action_copy" = "رونوشت"; +"action_copy_link" = "رونوشت از پیوند"; +"action_copy_link_to_message" = "رونوشت از پیوند پیام"; +"action_create" = "ایجاد"; +"action_create_a_room" = "ایجاد اتاق"; +"action_deactivate" = "غیرفعّال"; +"action_deactivate_account" = "غیرفعّال‌سازی حساب"; +"action_decline" = "رد"; +"action_delete_poll" = "حذف نظرسنجی"; +"action_disable" = "از کار انداختن"; +"action_discard" = "دور ریختن"; +"action_done" = "انجام شد"; +"action_edit" = "ویرایش"; +"action_edit_poll" = "ویرایش نظرسنجی"; +"action_enable" = "به کار انداختن"; +"action_end_poll" = "پایان نظرسنجی"; +"action_enter_pin" = "ورود پین"; +"action_forgot_password" = "گذرواژه را فراموش کردید؟"; +"action_forward" = "پیشروی"; +"action_go_back" = "پس‌روی"; +"action_ignore" = "Ignore"; +"action_invite" = "دعوت"; +"action_invite_friends" = "دعوت افراد"; +"action_invite_friends_to_app" = "دعوت به %1$@"; +"action_invite_people_to_app" = "دعوت افراد به %1$@"; +"action_invites_list" = "دعوت‌ها"; +"action_join" = "پیوستن"; +"action_learn_more" = "بیش‌تر دانستن"; +"action_leave" = "ترک"; +"action_leave_conversation" = "ترک گفت‌وگو"; +"action_leave_room" = "ترک اتاق"; +"action_load_more" = "بار کردن بیش‌تر"; +"action_manage_account" = "مدیریت حساب"; +"action_manage_devices" = "مدیریت افزاره‌ها"; +"action_message" = "پیام"; +"action_next" = "بعدی"; +"action_no" = "نه"; +"action_not_now" = "الآن نه"; +"action_ok" = "قبول"; +"action_open_settings" = "تنظیمات"; +"action_open_with" = "گشودن با"; +"action_pin" = "سنجاق"; +"action_quick_reply" = "پاسخ سریع"; +"action_quote" = "نقل قول"; +"action_react" = "واکنش"; +"action_reject" = "رد کردن"; +"action_remove" = "برداشتن"; +"action_reply" = "پاسخ"; +"action_reply_in_thread" = "پاسخ در رشته"; +"action_report_bug" = "گزارش اشکال"; +"action_report_content" = "گزارش محتوا"; +"action_reset" = "بازنشانی"; +"action_reset_identity" = "بازنشانی هویت"; +"action_retry" = "تلاش دوباره"; +"action_retry_decryption" = "تلاش دوباره برای رمزگشایی"; +"action_save" = "ذخیره"; +"action_search" = "جست‌وجو"; +"action_send" = "فرستادن"; +"action_send_message" = "فرستادن پیام"; +"action_share" = "هم‌رسانی"; +"action_share_link" = "هم‌رسانی پیوند"; +"action_show" = "نمایش"; +"action_sign_in_again" = "ورود دوباره"; +"action_signout" = "خروج"; +"action_signout_anyway" = "خروج به هر صورت"; +"action_skip" = "پرش"; +"action_start" = "آغاز"; +"action_start_chat" = "آغاز گپ"; +"action_start_verification" = "آغاز تأیید"; +"action_static_map_load" = "زدن برای بار کردن نقشه"; +"action_take_photo" = "عکس گرفتن"; +"action_tap_for_options" = "زدن برای گزینه‌ها"; +"action_try_again" = "تلاش دوباره"; +"action_unpin" = "سنجاق نکردن"; +"action_view_in_timeline" = "دیدن در خط زمانی"; +"action_view_source" = "دیدن منبع"; +"action_yes" = "بله"; +"banner_migrate_to_native_sliding_sync_action" = "خروج و ارتقا"; +"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; +"banner_migrate_to_native_sliding_sync_title" = "ارتقا موجود است"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "برپایی بازیابی"; +"common_about" = "درباره"; +"common_acceptable_use_policy" = "سیاست استفادهٔ پذیرفتنی"; +"common_advanced_settings" = "تنظیمات پیش‌رفته"; +"common_analytics" = "تجزیه و تحلیل"; +"common_appearance" = "ظاهر"; +"common_audio" = "صدا"; +"common_blocked_users" = "کاربران مسدود"; +"common_bubbles" = "حباب‌ها"; +"common_call_invite" = "تماس در جریان (پشتیبانی نشده)"; +"common_call_started" = "تماس آغاز شد"; +"common_chat_backup" = "پشتیبان گپ"; +"common_copyright" = "حق رونوشت"; +"common_creating_room" = "ایجاد کردن اتاق…"; +"common_current_user_left_room" = "اتاق را ترک کرد"; +"common_dark" = "تیره"; +"common_decryption_error" = "خطای رمزگشایی"; +"common_developer_options" = "گزینه‌های توسعه دهنده"; +"common_device_id" = "Device ID"; +"common_direct_chat" = "گپ مستقیم"; +"common_edited_suffix" = "(ویراسته)"; +"common_editing" = "ویرایش"; +"common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; +"common_encryption_enabled" = "رمزنگاری به کار افتاده"; +"common_enter_your_pin" = "پینتان را وارد کنید"; +"common_error" = "خطا"; +"common_everyone" = "هرکسی"; +"common_face_id_ios" = "شناسهٔ صورت"; +"common_failed" = "شکست خورد"; +"common_favourite" = "برگزیده"; +"common_favourited" = "برگزیده"; +"common_file" = "پرونده"; +"common_forward_message" = "هدایت پیام"; +"common_frequently_used" = "Frequently used"; +"common_gif" = "جیف"; +"common_image" = "تصویر"; +"common_in_reply_to" = "در پاسخ به %1$@"; +"common_invite_unknown_profile" = "شناسهٔ ماتریکس نتوانست پیدا شود. ممکن است دعوت نرسیده باشد."; +"common_leaving_room" = "ترک کردن اتاق"; +"common_light" = "روشن"; +"common_link_copied_to_clipboard" = "پیوند در تخته‌گیره رونوشت شد"; +"common_loading" = "بار کردن…"; +"common_message" = "پیام"; +"common_message_actions" = "کنش‌های پیام"; +"common_message_layout" = "جینش پیام"; +"common_message_removed" = "پیام برداشته شد"; +"common_modern" = "نوین"; +"common_mute" = "بی‌صدا"; +"common_no_results" = "بدون نتیجه"; +"common_no_room_name" = "بدون نام اتاق"; +"common_offline" = "برون‌خط"; +"common_optic_id_ios" = "شناسهٔ نوری"; +"common_or" = "یا"; +"common_password" = "گذرواژه"; +"common_people" = "افراد"; +"common_permalink" = "پایاپیوند"; +"common_permission" = "اجازه"; +"common_please_wait" = "لطفا صبر کنید…"; +"common_poll_end_confirmation" = "مطئنید که می‌خواهید این نظرسنجی را پایان دهید؟"; +"common_poll_summary" = "نظرسنجی: %1$@"; +"common_poll_total_votes" = "مجموع آرا: %1$@"; +"common_poll_undisclosed_text" = "نتیجه‌ها پس از پایان نظرسنجی نشان داده خواهند شد"; +"common_privacy_policy" = "سیاست محرمانگی"; +"common_reaction" = "واکنش"; +"common_reactions" = "واکنش‌ها"; +"common_recovery_key" = "کلید بازیابی"; +"common_refreshing" = "تازه سازی…"; +"common_replying_to" = "پاسخ دادن به %1$@"; +"common_report_a_bug" = "گزارش یک اشکال"; +"common_report_a_problem" = "گزارش مشکل"; +"common_report_submitted" = "گزارش ثبت شد"; +"common_rich_text_editor" = "ویرایشگر متن غنی"; +"common_room" = "اتاق"; +"common_room_name" = "نام اتاق"; +"common_room_name_placeholder" = "برای نمونه نام پروژه‌تان"; +"common_saved_changes" = "تغییرات ذخیره شده"; +"common_saving" = "ذخیره کردن"; +"common_screen_lock" = "قفل صفحه"; +"common_search_for_someone" = "جست‌وجوی افراد"; +"common_search_results" = "نتایج جست‌وجو"; +"common_security" = "امنیت"; +"common_seen_by" = "دیده شده به دست"; +"common_sending" = "فرستادن…"; +"common_sending_failed" = "فرستادن شکست خورد"; +"common_sent" = "فرستاده"; +"common_server_not_supported" = "کارساز پشتیبانی نمی‌شود"; +"common_server_url" = "نشانی کارساز"; +"common_settings" = "تنظیمات"; +"common_shared_location" = "مکان هم‌رسانده"; +"common_signing_out" = "خارج شدن"; +"common_something_went_wrong" = "چیزی اشتباه پیش رفت"; +"common_starting_chat" = "آغازیدن گپ…"; +"common_sticker" = "عکس برگردان"; +"common_success" = "موفّقیت"; +"common_suggestions" = "پیشنهادها"; +"common_syncing" = "هم‌گام ساختن"; +"common_system" = "سامانه"; +"common_text" = "متن"; +"common_third_party_notices" = "تذکّرهای سوم‌شخص"; +"common_thread" = "رشته"; +"common_topic" = "موضوع"; +"common_topic_placeholder" = "این اتاق دربارهٔ چیست؟"; +"common_touch_id_ios" = "شناسهٔ لمس"; +"common_unable_to_decrypt" = "ناتوان در رمزگشایی"; +"common_unable_to_decrypt_no_access" = "به این پیام دسترسی ندارید"; +"common_unable_to_invite_message" = "دعوت‌ها نتوانستند به کاربرانی برسند."; +"common_unable_to_invite_title" = "ناتوان در فرستادن دعوت(ها)"; +"common_unlock" = "قفل‌گشایی"; +"common_unmute" = "باصدا"; +"common_unsupported_event" = "رویداد پشتیبانی نشده"; +"common_username" = "نام کاربری"; +"common_verification_cancelled" = "تأیید لغو شد"; +"common_verification_complete" = "تأیید کامل شد"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "تأیید افزاره"; +"common_verify_identity" = "Verify identity"; +"common_video" = "ویدیو"; +"common_voice_message" = "پیام صوتی"; +"common_waiting" = "در انتظار…"; +"common_waiting_for_decryption_key" = "در انتظار این پیام"; +"common.copied_to_clipboard" = "در تخته‌گیره رونوشت شد"; +"common.do_not_show_this_again" = "این مورد را دوباره نشان نده"; +"common.open_source_licenses" = "پروانه‌های نرم‌افزاری آزاد"; +"common.pinned" = "سنجاق شده"; +"common.send_to" = "فرستادن به"; +"common.you" = "شما"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; +"confirm_recovery_key_banner_title" = "ورود کلید بازیابیتان"; +"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"crypto_identity_change_pin_violation" = "به نظر می‌رسد هویت %1$@ تغییر کرده. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; +"dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; +"dialog_permission_generic" = "لطفاً در تنظیمات سامانه اجازه بدهید."; +"dialog_permission_location_description_ios" = "اعطای دسترسی در تنظیمات -> مکان."; +"dialog_permission_location_title_ios" = "%1$@ به مکانتان دسترسی ندارد."; +"dialog_permission_microphone" = "In order to let the application use the microphone, please grant the permission in the system settings."; +"dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio."; +"dialog_permission_microphone_title_ios" = "%1$@ needs permission to access your microphone."; +"dialog_permission_notification" = "In order to let the application display notifications, please grant the permission in the system settings."; +"dialog_title_confirmation" = "تایید"; +"dialog_title_warning" = "هشدار"; +"dialog_unsaved_changes_description_ios" = "تغییراتتان ذخیره نمی‌شوند"; +"dialog_unsaved_changes_title" = "ذخیرهٔ تغییرات؟"; +"emoji_picker_category_activity" = "فعّالیت‌ها"; +"emoji_picker_category_flags" = "پرچم‌ها"; +"emoji_picker_category_foods" = "غذا و نوشیدنی"; +"emoji_picker_category_nature" = "حیوانات و طبعیت"; +"emoji_picker_category_objects" = "اشیا"; +"emoji_picker_category_people" = "شکلک‌ها و افراد"; +"emoji_picker_category_places" = "سفر و مکان‌ها"; +"emoji_picker_category_symbols" = "نمادها"; +"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_failed_creating_the_permalink" = "شکست در ایجاد پایاپیوند"; +"error_failed_loading_map" = "%1$@ could not load the map. Please try again later."; +"error_failed_loading_messages" = "شکست در بار کردن پیام‌ها"; +"error_failed_locating_user" = "%1$@ could not access your location. Please try again later."; +"error_failed_uploading_voice_message" = "شکست در بارگذاری پیام صوتیتان."; +"error_message_not_found" = "پیام پیدا نشد"; +"error_no_compatible_app_found" = "No compatible app was found to handle this action."; +"error_some_messages_have_not_been_sent" = "برخی پیام‌ها ارسال نشده‌اند"; +"error_unknown" = "متأسفیم ، خطایی رخ داد"; +"event_shield_reason_authenticity_not_guaranteed" = "اعتبار این پیام رمز شده نمی‌تواند روی این افزاره تأیید شود."; +"event_shield_reason_previously_verified" = "رمز شده به دست کاربری از پیش تأیید شده."; +"event_shield_reason_sent_in_clear" = "رمز نشده."; +"event_shield_reason_unknown_device" = "رمز شده به دست افزاره‌ای ناشناخته یا حذف شده."; +"event_shield_reason_unsigned_device" = "رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده."; +"event_shield_reason_unverified_identity" = "رمز شده به دست کاربری تأیید نشده."; +"full_screen_intent_banner_message" = "To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."; +"full_screen_intent_banner_title" = "بهبود تجریهٔ تماستان"; +"invite_friends_rich_title" = "🔐️ پییوستن به من روی %1$@"; +"invite_friends_text" = "درود. با من روی %1$@ صحبت کن: %2$@"; +"leave_conversation_alert_subtitle" = "Are you sure that you want to leave this conversation? This conversation is not public and you won't be able to rejoin without an invite."; +"leave_room_alert_empty_subtitle" = "Are you sure that you want to leave this room? You're the only person here. If you leave, no one will be able to join in the future, including you."; +"leave_room_alert_private_subtitle" = "Are you sure that you want to leave this room? This room is not public and you won't be able to rejoin without an invite."; +"leave_room_alert_subtitle" = "Are you sure that you want to leave the room?"; +"login_initial_device_name_ios" = "%1$@ آی‌اواس"; +"notification_channel_call" = "تماس"; +"notification_channel_listening_for_events" = "در حال گوش دادن به رویدادها"; +"notification_channel_noisy" = "اعلان‌های پرصدا"; +"notification_channel_ringing_calls" = "زنگ خوردن تماس"; +"notification_channel_silent" = "اعلان‌های صامت"; +"notification_incoming_call" = "تماس ورودی"; +"notification_inline_reply_failed" = "**‌شکست در فرستادن - لطفاً اتاق را بگشایید"; +"notification_invite_body" = "به گپ دعوتتان کرد"; +"notification_invite_body_with_sender" = "%1$@ به گپ دعوتتان کرد"; +"notification_mentioned_you_body" = "به شما اشاره کرد: %1$@"; +"notification_new_messages" = "پیام جدید"; +"notification_reaction_body" = "با %1$@ واکنش داد"; +"notification_room_invite_body" = "دعوت کرد به اتاق بپیوندید"; +"notification_room_invite_body_with_sender" = "%1$@ دعوت کرد به اتاق بپیوندید"; +"notification_sender_me" = "خودم"; +"notification_sender_mention_reply" = "%1$@ اشاره کرد یا پاسخ داد"; +"notification_test_push_notification_content" = "دارید آگاهی را مشاهده می‌کنید! کلیک کنید!"; +"notification_ticker_text_dm" = "%1$@: %2$@"; +"notification_ticker_text_group" = "%1$@: %2$@ %3$@"; +"notification_unread_notified_messages_and_invitation" = "%1$@ و %2$@"; +"notification_unread_notified_messages_in_room" = "%1$@ در %2$@"; +"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ در %2$@ و %3$@"; +"preference_rageshake" = "Rageshake to report bug"; +"rageshake_detection_dialog_content" = "به نظر می‌رسد دارید گوشی خود را به دلیل کار نکردن تکان می‌دهید! آیا می‌خواهید یک اشکال در برنامه گزارش نمایید؟"; +"rich_text_editor_bullet_list" = "تغییر وضعیت سیاههٔ گلوله‌ای"; +"rich_text_editor_close_formatting_options" = "بستن گزینه‌های قالب‌بندی"; +"rich_text_editor_code_block" = "تغییر حالت بلوک کد"; +"rich_text_editor_composer_placeholder" = "پیام…"; +"rich_text_editor_create_link" = "ایجاد پیوند"; +"rich_text_editor_edit_link" = "ویرایش پیوند"; +"rich_text_editor_format_bold" = "اعمال قالب توپر"; +"rich_text_editor_format_italic" = "اعمال قالب کج"; +"rich_text_editor_format_strikethrough" = "اعمال قالب خط‌خورده"; +"rich_text_editor_format_underline" = "اعمال قالب زیرخط‌دار"; +"rich_text_editor_full_screen_toggle" = "تغییر حالت تمام‌صفحه"; +"rich_text_editor_indent" = "تورفتگی"; +"rich_text_editor_inline_code" = "اعمال قالب کد درون‌خط"; +"rich_text_editor_link" = "تنظیم پیوند"; +"rich_text_editor_numbered_list" = "تغییر وضعیت سیاههٔ شماره‌دار"; +"rich_text_editor_open_compose_options" = "گشودن گزینه‌های نوشتن"; +"rich_text_editor_quote" = "تغییر حالت نقل قول"; +"rich_text_editor_remove_link" = "برداشتن پیوند"; +"rich_text_editor_unindent" = "تونرفتگی"; +"rich_text_editor_url_placeholder" = "پیوند"; +"rich_text_editor_a11y_add_attachment" = "افزودن پیوست"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; +"screen_advanced_settings_element_call_base_url" = "نشانی پایهٔ تماس المنتی سفارشی"; +"screen_advanced_settings_element_call_base_url_description" = "تنظمی نشانی پایه‌‌ای سفارشی برای تماس المنتی."; +"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; +"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; +"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; +"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; +"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "فرستادن پیام به هر روی"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; +"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_room_mentions_at_room_subtitle" = "آگاهی به تمام اتاق"; +"screen_room_pinned_banner_indicator" = "%1$@ از %2$@"; +"screen_room_pinned_banner_indicator_description" = "%1$@ پیام‌های سنجاق شده"; +"screen_room_pinned_banner_loading_description" = "بار کردن پشام‌ها…"; +"screen_room_pinned_banner_view_all_button_title" = "نمایش همه"; +"screen_room_details_pinned_events_row_title" = "پیام‌های سنجاق شده"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; +"screen_account_provider_form_hint" = "نشانی کارساز خانگی"; +"screen_account_provider_form_notice" = "ورود عبارت جست‌وجو یا نشانی دامنه."; +"screen_account_provider_form_subtitle" = "جست‌وجو برای شرکت، اجتماع یا کارسازی خصوصی."; +"screen_account_provider_form_title" = "یافتن فراهم کنندهٔ حساب"; +"screen_account_provider_signin_title" = "دارید وارد %@ می‌شوید"; +"screen_account_provider_signup_title" = "دارید حسابی روی %@ می‌سازید"; +"screen_advanced_settings_developer_mode" = "حالت توسعه‌دهنده"; +"screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; +"screen_advanced_settings_rich_text_editor_description" = "از کار انداختن ویرایشگر متن غنی یا نوشتن دستی مارک‌دون."; +"screen_advanced_settings_send_read_receipts" = "رسید‌های خواندن"; +"screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; +"screen_advanced_settings_share_presence" = "هم‌رسانی حضور"; +"screen_advanced_settings_share_presence_description" = "If turned off, you won’t be able to send or receive read receipts or typing notifications."; +"screen_advanced_settings_view_source_description" = "Enable option to view message source in the timeline."; +"screen_analytics_prompt_data_usage" = "We won't record or profile any personal data"; +"screen_analytics_prompt_help_us_improve" = "Share anonymous usage data to help us identify issues."; +"screen_analytics_prompt_read_terms" = "You can read all our terms %1$@."; +"screen_analytics_prompt_read_terms_content_link" = "این‌جا"; +"screen_analytics_prompt_settings" = "می‌توانید در هر زمان خاموشش کنید"; +"screen_analytics_prompt_third_party_sharing" = "داده‌هایتان را با سوم‌شخص‌ها هم‌نمی‌رسانیم"; +"screen_analytics_prompt_title" = "کمک به بهبود %1$@"; +"screen_analytics_settings_share_data" = "هم رسانی داده‌های تحلیلی"; +"screen_app_lock_biometric_authentication" = "هویت‌سنجی زیستی"; +"screen_app_lock_biometric_unlock" = "قفل‌گشایی زیست‌سنجی"; +"screen_app_lock_biometric_unlock_reason_ios" = "برای دسترسی به کاره‌تان نیاز به هویت‌سنجی است"; +"screen_app_lock_forgot_pin" = "فراموشی پین؟"; +"screen_app_lock_settings_change_pin" = "تغییر کد پین"; +"screen_app_lock_settings_enable_biometric_unlock" = "احازه به قفل گشایی زیست‌سنجی"; +"screen_app_lock_settings_enable_face_id_ios" = "اجازه به شناسهٔ صورت"; +"screen_app_lock_settings_enable_optic_id_ios" = "اجازه به شناسهٔ نوری"; +"screen_app_lock_settings_enable_touch_id_ios" = "اجازه به شناسهٔ لمسی"; +"screen_app_lock_settings_remove_pin" = "برداشتن پین"; +"screen_app_lock_settings_remove_pin_alert_message" = "مطمئنید که می‌خواهید پین را بردارید؟"; +"screen_app_lock_settings_remove_pin_alert_title" = "برداشتن پین؟"; +"screen_app_lock_setup_biometric_unlock_allow_title" = "اجازه به %1$@"; +"screen_app_lock_setup_biometric_unlock_skip" = "ترجیح می‌دهم از پین استفاده کنم"; +"screen_app_lock_setup_biometric_unlock_subtitle" = "زمیانتان را ذخیره کرده و از %1$@ برای قفل‌گشایی هربارهٔ کاره استفاده کنید"; +"screen_app_lock_setup_choose_pin" = "گزینش پین"; +"screen_app_lock_setup_confirm_pin" = "تأیید پین"; +"screen_app_lock_setup_pin_context" = "Lock %1$@ to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app."; +"screen_app_lock_setup_pin_forbidden_dialog_content" = "به دلیل امنیتی نمی‌توانید این پین را برگزینید"; +"screen_app_lock_setup_pin_forbidden_dialog_title" = "گزینشی پینی متفاوت"; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "لطفاً یک پین را دو بار وارد کنید"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "پین‌ها مطابق نیستند"; +"screen_app_lock_signout_alert_message" = "برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید"; +"screen_app_lock_signout_alert_title" = "دارید خارج می‌شوید"; +"screen_blocked_users_empty" = "هیچ کاربر مسدودی ندارید"; +"screen_blocked_users_unblocking" = "رفع کردن انسداد…"; +"screen_bug_report_attach_screenshot" = "پیوست نماگرفت"; +"screen_bug_report_contact_me" = "اگر پرسش دیگری دارید، می‌توانید با من در تماس باشید."; +"screen_bug_report_contact_me_title" = "تماس با من"; +"screen_bug_report_edit_screenshot" = "ویرایش نماگرفت"; +"screen_bug_report_editor_description" = "Please describe the problem. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can."; +"screen_bug_report_editor_placeholder" = "شرح مشکل…"; +"screen_bug_report_editor_supporting" = "ترجیحاً توضیحات را به زبان انگلیسی بنویسید."; +"screen_bug_report_error_description_too_short" = "The description is too short, please provide more details about what happened. Thanks!"; +"screen_bug_report_include_crash_logs" = "ارسال رخدادنگارهای خطا"; +"screen_bug_report_include_logs" = "اجازه به گزارش‌ها"; +"screen_bug_report_include_screenshot" = "ارسال تصویر صفحه"; +"screen_bug_report_logs_description" = "Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."; +"screen_bug_report_view_logs" = "دیدن گزارش‌ها"; +"screen_change_account_provider_matrix_org_subtitle" = "ماتریکس‌دات‌اورگ کارسازی بزرگ و آزاد روی شبکهٔ ماتریکس عمومی برای ارتباطات نامتمرکز و امن است که به دست بنیاد ماتریکس‌دات‌اورگ اداره می‌شود."; +"screen_change_account_provider_other" = "دیگر"; +"screen_change_account_provider_subtitle" = "استفاده از فراهم کنندهٔ حسابی دیگر چون کارساز خصوصی خوتان یا حسابی کاری."; +"screen_change_account_provider_title" = "تغییر فراهم کنندهٔ حساب"; +"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help."; +"screen_change_server_error_invalid_well_known" = "Sliding sync isn't available due to an issue in the well-known file:\n%1$@"; +"screen_change_server_error_no_sliding_sync_message" = "این کارساز در حال حاضر از هم‌گام سازی اسلایدی پشتیبانی نمی‌کند."; +"screen_change_server_form_header" = "نشانی کارساز خانگی"; +"screen_change_server_form_notice" = "تنها می‌توانید به کارسازهای موجودی که از هم‌گام سازی اسلاید پشتیبانی می‌کنند وصل شود. مدیر کارساز خانگیتان باید پیکربندیش کند. %1$@"; +"screen_change_server_subtitle" = "نشانی کارسازتان چیست؟"; +"screen_change_server_title" = "کارسازتان را برگزینید"; +"screen_chat_backup_key_backup_action_disable" = "خاموش کردن پشتیبان"; +"screen_chat_backup_key_backup_action_enable" = "روشن کردن پشتیبان"; +"screen_chat_backup_key_backup_description" = "پشتیبان‌ها اطمینان می‌دهند که تاریخچهٔ پیام‌هایتان را از دست نمی‌دهید. %1$@."; +"screen_chat_backup_key_backup_title" = "پشتیبان گیری"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; +"screen_chat_backup_recovery_action_change" = "تغییر کلید بازیابی"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; +"screen_chat_backup_recovery_action_confirm_description" = "پشتیبان گپتان از هم‌گام بودن در آمده."; +"screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; +"screen_create_account_title" = "ایجاد حساب"; +"screen_create_new_recovery_key_list_item_1" = "گشودن %1$@ در افزارهٔ میزکار"; +"screen_create_new_recovery_key_list_item_2" = "ورود دوباره به حسابتان"; +"screen_create_new_recovery_key_list_item_3" = "گزینش %1$@ هنگام درخواست تأیید افزاره‌تان"; +"screen_create_new_recovery_key_list_item_3_reset_all" = "«بازنشانی همه»"; +"screen_create_new_recovery_key_list_item_4" = "پیروی از دستورالعمل‌ها برای ایجاد کلید بازیابی جدید"; +"screen_create_new_recovery_key_list_item_5" = "ذخیرهٔ کلید بازیابی جدیدتان در مدیر گذرواژه یا یادداشت رمز شده"; +"screen_create_new_recovery_key_title" = "بازنشانی رمزنگاری برای حسابتان با استفاده از افزاره‌ای دیگر"; +"screen_create_poll_add_option_btn" = "افزودن گزینه"; +"screen_create_poll_anonymous_desc" = "نمایش نتیجه‌ها تنها پس از پایان نظرسنجی"; +"screen_create_poll_anonymous_headline" = "نهفتن رأی‌ها"; +"screen_create_poll_answer_hint" = "گزینهٔ %1$d"; +"screen_create_poll_cancel_confirmation_title_ios" = "لغو نظرسنجی"; +"screen_create_poll_question_desc" = "پرسش یا موضوع"; +"screen_create_poll_question_hint" = "این نظرسنجی دربارهٔ چیست؟"; +"screen_create_poll_title" = "ایجاد نظرسنجی"; +"screen_create_room_action_create_room" = "اتاق جدید"; +"screen_create_room_error_creating_room" = "هنگام ایجاد اتاق خطایی رخ داد"; +"screen_create_room_private_option_description" = "پیام‌های این اتاق رمز شده‌اند. رمزنگاری نمی‌تواند از این پس تغییر کند."; +"screen_create_room_private_option_title" = "اتاق خصوصی (فقط دعوت)"; +"screen_create_room_public_option_description" = "پیام‌ها رمزنگاری نشده و هرکسی می‌تواند بخواندشان. می‌توانید بعداً رمزنگاری را به کار بیندازید."; +"screen_create_room_public_option_title" = "اتاق عمومی (هرکسی)"; +"screen_create_room_topic_label" = "موضوع (اختیاری)"; +"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; +"screen_deactivate_account_delete_all_messages" = "حذف همهٔ پیام‌هایم"; +"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; +"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; +"screen_deactivate_account_description_bold_part" = "بازگشت‌ناپذیر"; +"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; +"screen_deactivate_account_list_item_1_bold_part" = "از کار انداختن دایمی"; +"screen_deactivate_account_list_item_2" = "برداشتنتان از همهٔ اتاق‌های گپ."; +"screen_deactivate_account_list_item_3" = "حذف اطّلاعات حسابتان از کارساز هویت."; +"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; +"screen_deactivate_account_title" = "غیرفعّال‌سازی حساب"; +"screen_edit_poll_delete_confirmation" = "مطمئنید که می‌خواهید این نظرسنجی را حذف کنید؟"; +"screen_edit_profile_display_name" = "نام نمایشی"; +"screen_edit_profile_display_name_placeholder" = "نام نمایشیتان"; +"screen_edit_profile_error" = "خطایی ناشناخته رخ داد و اطّلاعات نتوانستند تغییر کنند."; +"screen_edit_profile_error_title" = "ناتوان در به‌روز کردن نمایه"; +"screen_edit_profile_title" = "ویرایش نمایه"; +"screen_edit_profile_updating_details" = "به‌روز کردن نمایه…"; +"screen_encryption_reset_action_continue_reset" = "ادامهٔ بازنشانی"; +"screen_encryption_reset_bullet_1" = "جزییات حساب، آشنایان، ترجیحات و سیاههٔ گپ‌هایتان حفظ خواهند شد"; +"screen_encryption_reset_bullet_2" = "تاریخچهٔ گپ‌هایتان را از دست خواهید داد"; +"screen_encryption_reset_bullet_3" = "لازم است دوباره همهٔ آشنایان و افزاره‌های موجودتان را تأیید کنید"; +"screen_encryption_reset_footer" = "فقط اگر به افزاره‌ای وارد شده از پیش دسترسی ندارید و کلید بازیابیتان را گم کرده‌اید بازنشانی کنید."; +"screen_encryption_reset_title" = "نمی‌توانید تأیید کنید؟ لازم است هویتتان را بازنشانی کنید."; +"screen_identity_confirmation_cannot_confirm" = "نمی‌توانید تأیید کنید؟"; +"screen_identity_confirmation_create_new_recovery_key" = "ایجاد کلید بازیابی جدید"; +"screen_identity_confirmation_subtitle" = "تأیید این افزاره برای برپایی پیام‌رسانی امن."; +"screen_identity_confirmation_title" = "تأیید هویتتان"; +"screen_identity_confirmation_use_another_device" = "استفاده از افزاره‌ای دیگر"; +"screen_identity_confirmation_use_recovery_key" = "استفاده از کلید بازیابی"; +"screen_identity_confirmed_subtitle" = "اکنون می‌توانید پیام‌ها را به صورت امن فرستاده و بگیرید و هرکسی که با او گپ می‌زنید نیز می‌تواند به این افزاره اعتماد کند."; +"screen_identity_confirmed_title" = "افزاره تأیید شده"; +"screen_identity_waiting_on_other_device" = "منتظر افزارهٔ دیگر…"; +"screen_invites_decline_chat_message" = "مطمئنید که می‌خواهید دعوت پیوستن به %1$@ را رد کنید؟"; +"screen_invites_decline_chat_title" = "رد دعوت"; +"screen_invites_decline_direct_chat_message" = "مطمئنید که می‌خواهید این گپ خصوصی با %1$@ را رد کنید؟"; +"screen_invites_decline_direct_chat_title" = "رد گپ"; +"screen_invites_empty_list" = "بدون دعوت"; +"screen_invites_invited_you" = "%1$@ (%2$@) دعوتتان کرد"; +"screen_join_room_join_action" = "پیوستن به اتاق"; +"screen_join_room_knock_action" = "در زدن برای پیوستن"; +"screen_join_room_space_not_supported_description" = "%1$@ هنوز از فضاها پشتیبانی نمی‌کند. می‌توانید روی وب به فضاها دسترسی داشته باشید."; +"screen_join_room_space_not_supported_title" = "فضاها هنوز پشتیبانی نمی‌شوند"; +"screen_join_room_subtitle_knock" = "زدن روی این دکمه برای آگاه شدن مدیر اتاق. پس از تأیید می‌توانید به گفت‌وگو بپیوندید."; +"screen_join_room_subtitle_no_preview" = "برای دیدن تاریخچهٔ پیام باید عضو این اتاق باشید."; +"screen_join_room_title_knock" = "می‌خواهید به اتاق بپیوندید؟"; +"screen_join_room_title_no_preview" = "پیش‌نمایش موجود نیست"; +"screen_key_backup_disable_confirmation_action_turn_off" = "خاموش کردن"; +"screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; +"screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"; +"screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?"; +"screen_login_error_deactivated_account" = "این حساب از کار افتاده است."; +"screen_login_error_invalid_credentials" = "نام کاربری یا گذرواژه نامعتبر است"; +"screen_login_error_invalid_user_id" = "این یک شناسه کاربری معتبر نیست. قالب صحیح: ‪«@user:homeserver.or"; +"screen_login_error_refresh_tokens" = "This server is configured to use refresh tokens. These aren't supported when using password based login."; +"screen_login_error_unsupported_authentication" = "The selected homeserver doesn't support password or OIDC login. Please contact your admin or choose another homeserver."; +"screen_login_form_header" = "جزییاتتان را وارد کنید"; +"screen_login_title" = "خوش برگشتید!"; +"screen_login_title_with_homeserver" = "ورود به %1$@"; +"screen_media_picker_error_failed_selection" = "گزینش رسانه شکست خورد. لطفاً دوباره تلاش کنید."; +"screen_media_upload_preview_error_failed_processing" = "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."; +"screen_media_upload_preview_error_failed_sending" = "بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."; +"screen_migration_message" = "فرایندی یک باره است. ممنون از شکیباییتان."; +"screen_migration_title" = "برپایی حسابتان."; +"screen_notification_optin_subtitle" = "می‌توانید بعداً تنظیماتتان را تغییر دهید."; +"screen_notification_optin_title" = "اجازه به آگاهی‌ها و از دست ندادن پیام‌ها"; +"screen_notification_settings_additional_settings_section_title" = "تنظیمات اضافی"; +"screen_notification_settings_calls_label" = "تماس‌های صوتی و تصویری"; +"screen_notification_settings_configuration_mismatch" = "نامتطابقت در پیکربندی"; +"screen_notification_settings_configuration_mismatch_description" = "تنظمیات آگاهی را ساده کرده‌ایم تا یافتن انتخاب‌ها را ساده‌تر کنیم. برهی تنظمیات سفارسی که در گذشته گزیده‌اید این‌جا نشان داده نمی‌شوند؛ ولی همچنن فعّالند.\n\nبا ادامه داد ممکن است برخی تنظیماتتان تغییر کنند."; +"screen_notification_settings_direct_chats" = "گپ‌های مستقیم"; +"screen_notification_settings_edit_custom_settings_section_title" = "تنظیمات سفارشی برای هر گپ"; +"screen_notification_settings_edit_failed_updating_default_mode" = "هنگام به‌روز کردن تنظیمات آگاهی خطایی رخ داد."; +"screen_notification_settings_edit_mode_all_messages" = "همهٔ پیام‌ها"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "فقط اشاره‌ها و کلیدواژگان"; +"screen_notification_settings_edit_screen_direct_section_header" = "آگاهی در گپ‌های مستقیم برای"; +"screen_notification_settings_edit_screen_group_section_header" = "آگاهی در گپ‌های گروهی برای"; +"screen_notification_settings_enable_notifications" = "به کار انداختن آگاهی‌ها روی این افزاره"; +"screen_notification_settings_failed_fixing_configuration" = "پیکربندی درست نشد. لطفاً دوباره تلاش کنید."; +"screen_notification_settings_group_chats" = "گپ‌های گروهی"; +"screen_notification_settings_invite_for_me_label" = "دعوت‌ها"; +"screen_notification_settings_mentions_only_disclaimer" = "کارساز خانگیتان از این گزینه در اتاق‌های رمز شده پشتیبانی نمی‌کند. ممکن است در برخی اتاق‌ها آگاه نشوید."; +"screen_notification_settings_mode_all" = "همه"; +"screen_notification_settings_mode_mentions" = "اشاره‌ها"; +"screen_notification_settings_notification_section_title" = "آگاه کردنم برای"; +"screen_notification_settings_room_mention_label" = "آگاه کردنم برای ‪@room"; +"screen_notification_settings_system_notifications_action_required" = "برای گرفتن آگاهی‌ها لطفاً%1$@تان را تغییر دهید."; +"screen_notification_settings_system_notifications_action_required_content_link" = "تنظیمات سامانه"; +"screen_notification_settings_system_notifications_turned_off" = "آگاهی‌های سامانه‌ای خاموش شدند"; +"screen_notification_settings_title" = "آگاهی‌ها"; +"screen_onboarding_sign_in_manually" = "ورود دستی"; +"screen_onboarding_sign_in_with_qr_code" = "ورود با کد QR"; +"screen_onboarding_sign_up" = "ایجاد حساب"; +"screen_onboarding_welcome_message" = "به سریع‌ترین %1$@ خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."; +"screen_onboarding_welcome_subtitle" = "به %1$@ خوش آمدید. بازطرّاحی شده برای سرعت و سادگی."; +"screen_onboarding_welcome_title" = "در المنتتان باشید"; +"screen_polls_history_empty_ongoing" = "نتوانست هیچ نظرسنجی در جریانی بیابد."; +"screen_polls_history_empty_past" = "نتوانست هیچ نظرسنجی گذشته‌ای بیابد."; +"screen_polls_history_filter_ongoing" = "در جریان"; +"screen_polls_history_filter_past" = "گذشته"; +"screen_polls_history_title" = "نظرسنجی‌ها"; +"screen_qr_code_login_connecting_subtitle" = "برقرار کدن اتّصالی امن"; +"screen_qr_code_login_connection_note_secure_state_description" = "نتوانست اتّصالی امن به افزارهٔ جدید بسازد. افزاره‌های موجودتان هنوز امنند و نیازی نیست نگرانشان باشید."; +"screen_qr_code_login_connection_note_secure_state_list_header" = "اکنون چه؟"; +"screen_qr_code_login_connection_note_secure_state_list_item_1" = "Try signing in again with a QR code in case this was a network problem"; +"screen_qr_code_login_connection_note_secure_state_list_item_2" = "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"; +"screen_qr_code_login_connection_note_secure_state_list_item_3" = "ورود دستی در صورت کار نکردنش"; +"screen_qr_code_login_connection_note_secure_state_title" = "اتّصال ناامن"; +"screen_qr_code_login_device_code_subtitle" = "از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید."; +"screen_qr_code_login_device_code_title" = "شمارهٔ زیر را روی افزارهٔ دیگرتان وارد کنید"; +"screen_qr_code_login_device_not_signed_in_scan_state_description" = "به افزارهٔ دیگرتان وارد شده و دوباره تلاش کنید یا از افزارهٔ دیگری که از پیش وارد شده استفاده کنید."; +"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "افزارهٔ دیگر وارد نشده"; +"screen_qr_code_login_error_cancelled_subtitle" = "ورود روی افزارهٔ دیگر لغو شد."; +"screen_qr_code_login_error_cancelled_title" = "درخواست ورد لغو شد"; +"screen_qr_code_login_error_declined_subtitle" = "ورود به دست افزارهٔ دیگر رد شد."; +"screen_qr_code_login_error_declined_title" = "ورود رد شد"; +"screen_qr_code_login_error_expired_subtitle" = "ورود منقضی شد. لطفاً دوباره تلاش کنید."; +"screen_qr_code_login_error_expired_title" = "ورود در زمان معیّن کامل نشد"; +"screen_qr_code_login_error_linking_not_suported_subtitle" = "افزارهٔ دیگرتان از ورود به %@ با کد پاس پشتیبانی نمی‌کند.\n\nآزمودن ورود دستی یا پویش کد پاس با افزاره‌ای دیگر."; +"screen_qr_code_login_error_linking_not_suported_title" = "کد پاس پشتیبانی نمی‌شود"; +"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "فراهم کنندهٔ حسابتان از %1$@ پشتیبانی نمی‌کند."; +"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ پشتیبانی نمی‌شود"; +"screen_qr_code_login_initial_state_button_title" = "آمادهٔ پویش"; +"screen_qr_code_login_initial_state_item_1" = "گشودن %1$@ در افزارهٔ میزکار"; +"screen_qr_code_login_initial_state_item_2" = "زدن روی چهرکتان"; +"screen_qr_code_login_initial_state_item_3" = "گزینش %1$@"; +"screen_qr_code_login_initial_state_item_3_action" = "«پیوند افزارهٔ جدید»"; +"screen_qr_code_login_initial_state_item_4" = "پویش کد پاس با این افزاره"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; +"screen_qr_code_login_initial_state_title" = "گشودن %1$@ روی افزاره‌ای دیگر برای گرفتن کد پاس"; +"screen_qr_code_login_invalid_scan_state_description" = "استفاده از کد پاس نشان داده روی افزارهٔ دیگر."; +"screen_qr_code_login_invalid_scan_state_subtitle" = "کد پاس اشتباه"; +"screen_qr_code_login_no_camera_permission_button" = "رفتن به تنظیمات دوربین"; +"screen_qr_code_login_no_camera_permission_state_description" = "برای ادامه باید اجازهٔ استفادهٔ %1$@ از دوربین افزاره‌تان را بدهید."; +"screen_qr_code_login_no_camera_permission_state_title" = "اجازهٔ دسترسی دوربین برای پویش کد پاس"; +"screen_qr_code_login_scanning_state_title" = "پویش کد پاس"; +"screen_qr_code_login_start_over_button" = "آغاز از نو"; +"screen_qr_code_login_unknown_error_description" = "خطایی غیرمنتظره رخ داد. لطفاً دوباره تلاش کنید."; +"screen_qr_code_login_verify_code_loading" = "منتظر افزارهٔ دیگرتان"; +"screen_qr_code_login_verify_code_subtitle" = "ممکن است فراهم کنندهٔ حسابتان کد زیر را برای تأیید ورود بخواهد."; +"screen_qr_code_login_verify_code_title" = "کد تأییدتان"; +"screen_recovery_key_change_description" = "گرفتن کلید بازیابی جدید در صورت فراموشی کلید کنونی. پس از تغییر دادن کلید بازیابیتان، کلید پیشین دیگر کار نخواهد کرد."; +"screen_recovery_key_change_generate_key" = "تولید کلید بازیابی جدید"; +"screen_recovery_key_change_success" = "کلید بازیابی تغییر کرد"; +"screen_recovery_key_change_title" = "تغییر کلید بازیابی؟"; +"screen_recovery_key_confirm_create_new_recovery_key" = "ایجاد کلید بازیابی جدید"; +"screen_recovery_key_confirm_description" = "اطمینان از این که کسی نمی‌تواند این صفحه را ببیند!"; +"screen_recovery_key_confirm_error_content" = "لطفاً برای تأیید دسترسی به پشتیبان گپتان دوباره تلاش کنید."; +"screen_recovery_key_confirm_error_title" = "کلید بازیابی اشتباه"; +"screen_recovery_key_confirm_key_description" = "کلید امنیتی یا عبارت امنیتی نیز باید کار کنند."; +"screen_recovery_key_confirm_key_placeholder" = "ورود…"; +"screen_recovery_key_confirm_lost_recovery_key" = "گم کردن کلید بازیابیتان؟"; +"screen_recovery_key_confirm_success" = "کلید بازیابی تأیید شد"; +"screen_recovery_key_copied_to_clipboard" = "کلید بازیابی رونوشت شد"; +"screen_recovery_key_generating_key" = "تولید کردن…"; +"screen_recovery_key_save_action" = "ذخیرهٔ کلید بازیابی"; +"screen_recovery_key_save_description" = "نوشتن کلید بازیابیتان در جایی امن یا ذخیره‌اش در مدیر گذرواژه."; +"screen_recovery_key_save_key_description" = "زدن برای رونوشت از کلید بازیابی"; +"screen_recovery_key_save_title" = "ذخیرهٔ کلید بازیابیتان"; +"screen_recovery_key_setup_confirmation_description" = "پس از این برپایی قادر به دسترسی به کلید بازیابی جدیدتان نخواهید بود."; +"screen_recovery_key_setup_confirmation_title" = "کلید بازیابیتان را ذخیره کرده‌اید؟"; +"screen_recovery_key_setup_description" = "پشتیبان گپتان با کلید بازیابی محافظت می‌شود. اگر پس از برپایی نیاز به کلید بازیابی جدیدی داشتید می‌توانید با گزینش «دگرگونی کلید بازیابی» دوباره ایجادش کنید."; +"screen_recovery_key_setup_generate_key" = "تولید کلید بازیابیتان"; +"screen_recovery_key_setup_generate_key_description" = "اطمینان از امکان نگه داری کلید بازیابیتان در جایی امن"; +"screen_recovery_key_setup_success" = "برپایی بازیابی موفّق بود"; +"screen_recovery_key_setup_title" = "برپایی بازیابی"; +"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; +"screen_report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."; +"screen_report_content_hint" = "دلیل گزارش این محتوا"; +"screen_reset_encryption_confirmation_alert_action" = "بله. اکنون بازنشانی شود"; +"screen_reset_encryption_confirmation_alert_subtitle" = "این فرایند بازگشت‌ناپذیر است."; +"screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; +"screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; +"screen_reset_encryption_password_title" = "Enter your account password to continue"; +"screen_reset_identity_confirmation_subtitle" = "داردید برای بازنشانی هویتتان به حساب %1$@ می‌روید. پس از آن به کاره برگردانده خواهید شد."; +"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_room_alias_resolver_resolve_alias_failure" = "Failed to resolve room alias."; +"screen_room_attachment_source_camera" = "دوربین"; +"screen_room_attachment_source_camera_video" = "ضبط ویدیو"; +"screen_room_attachment_source_files" = "پیوست"; +"screen_room_attachment_source_gallery" = "کتابخانهٔ عکس و ویدیو"; +"screen_room_attachment_source_location" = "مکان"; +"screen_room_attachment_source_poll" = "نظرسنجی"; +"screen_room_attachment_text_formatting" = "قالب‌بندی متن"; +"screen_room_change_permissions_administrators" = "فقط مدیران"; +"screen_room_change_permissions_ban_people" = "تحریم افراد"; +"screen_room_change_permissions_delete_messages" = "برداشتن پیام‌ها"; +"screen_room_change_permissions_invite_people" = "دعوت افراد"; +"screen_room_change_permissions_moderators" = "مدیرن و ناظران"; +"screen_room_change_permissions_remove_people" = "برداشتن افراد"; +"screen_room_change_permissions_room_avatar" = "تغییر چهرک اتاق"; +"screen_room_change_permissions_room_name" = "تغییر نام اتاق"; +"screen_room_change_permissions_room_topic" = "دگرگونی موضوع اتاق"; +"screen_room_change_permissions_send_messages" = "فرستادن پیام‌ها"; +"screen_room_change_role_administrators_title" = "ویرایش مدیران"; +"screen_room_change_role_confirm_add_admin_description" = "قادر نخواهید بود این کنش را بازکردانید. داردید کاربر را به سطح قدرت خودتان ارتقا می‌دهید."; +"screen_room_change_role_confirm_add_admin_title" = "افزودن مدیر؟"; +"screen_room_change_role_confirm_demote_self_action" = "تنزل بده"; +"screen_room_change_role_confirm_demote_self_description" = "شما نمی‌توانید این تغییر را بازگردانید زیرا در حال تنزل نقش خود در اتاق هستید، اگر آخرین کاربر ممتاز در اتاق باشید، امکان دستیابی مجدد به دسترسی‌های سطح بالای اتاق غیرممکن است."; +"screen_room_change_role_confirm_demote_self_title" = "تنزل نقش شما در اتاق؟"; +"screen_room_change_role_invited_member_name" = "%1$@ (منتظر)"; +"screen_room_change_role_moderators_admin_section_footer" = "مدیران به صورت خودکار اجازه‌های نظارتی را دارند"; +"screen_room_change_role_moderators_title" = "ویرایش ناظران"; +"screen_room_change_role_unsaved_changes_description" = "تغییراتی ذخیره نشده دارید."; +"screen_room_details_add_topic_title" = "افزودن موضوع"; +"screen_room_details_already_a_member" = "از پیش عضو است"; +"screen_room_details_already_invited" = "از پیش دعوت شده"; +"screen_room_details_badge_encrypted" = "رمز شده"; +"screen_room_details_badge_not_encrypted" = "رمزنگارش نشده"; +"screen_room_details_badge_public" = "اتاق عمومی"; +"screen_room_details_edit_room_title" = "ویرایش اتاق"; +"screen_room_details_edition_error" = "خطایی ناشناخته رخ داد و اطّلاعات قابل تغییر نبودند."; +"screen_room_details_edition_error_title" = "ناتوان در به‌روز رسانی اتاق"; +"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; +"screen_room_details_encryption_enabled_title" = "رمزنگاری پیام به کار افتاد"; +"screen_room_details_error_loading_notification_settings" = "An error occurred when loading notification settings."; +"screen_room_details_error_muting" = "Failed muting this room, please try again."; +"screen_room_details_error_unmuting" = "Failed unmuting this room, please try again."; +"screen_room_details_notification_mode_custom" = "سفارشی"; +"screen_room_details_notification_mode_default" = "پیش‌گزیده"; +"screen_room_details_share_room_title" = "هم‌رسانی اتاق"; +"screen_room_details_title" = "اطّلاعات اتاق"; +"screen_room_details_updating_room" = "به‌روز کردن اتاق…"; +"screen_room_directory_search_loading_error" = "شکست در بار کردن"; +"screen_room_directory_search_title" = "فهرست اتاق‌ها"; +"screen_room_encrypted_history_banner" = "Message history is currently unavailable."; +"screen_room_encrypted_history_banner_unverified" = "Message history is unavailable in this room. Verify this device to see your message history."; +"screen_room_error_failed_retrieving_user_details" = "Could not retrieve user details"; +"screen_room_invite_again_alert_message" = "می‌خواهید دوباره دعوتش کنید؟"; +"screen_room_invite_again_alert_title" = "در این گپ تنهایید"; +"screen_room_member_details_block_alert_action" = "بلوک"; +"screen_room_member_details_block_alert_description" = "Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime."; +"screen_room_member_details_block_user" = "انسداد کاربر"; +"screen_room_member_details_title" = "نمایه"; +"screen_room_member_details_unblock_alert_action" = "رفع انسداد"; +"screen_room_member_details_unblock_alert_description" = "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."; +"screen_room_member_details_unblock_user" = "رفع انسداد کاربر"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; +"screen_room_member_list_ban_member_confirmation_action" = "تحریم"; +"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; +"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; +"screen_room_member_list_banned_empty" = "There are no banned users in this room."; +"screen_room_member_list_banning_user" = "تحریم کردن %1$@"; +"screen_room_member_list_manage_member_ban" = "برداشت و تحریم عضو"; +"screen_room_member_list_manage_member_remove" = "برداشتن از اتاق"; +"screen_room_member_list_manage_member_remove_confirmation_kick" = "تنها برداشتن عضو"; +"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; +"screen_room_member_list_manage_member_unban_action" = "رفع انسداد"; +"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited."; +"screen_room_member_list_manage_member_unban_title" = "تحریم نکردن کاربر"; +"screen_room_member_list_manage_member_user_info" = "دیدن نمایه"; +"screen_room_member_list_mode_banned" = "محروم"; +"screen_room_member_list_mode_members" = "اعضا"; +"screen_room_member_list_pending_header_title" = "منتظر"; +"screen_room_member_list_removing_user" = "برداشتن %1$@…"; +"screen_room_member_list_role_administrator" = "مدیر"; +"screen_room_member_list_role_moderator" = "ناظمر"; +"screen_room_member_list_room_members_header_title" = "اعضای اتاق"; +"screen_room_member_list_unbanning_user" = "رفع تحریم %1$@"; +"screen_room_notification_settings_allow_custom" = "اجازه به تنظیمت شخصی"; +"screen_room_notification_settings_allow_custom_footnote" = "Turning this on will override your default setting"; +"screen_room_notification_settings_custom_settings_title" = "آگاهی من در این گپ برای"; +"screen_room_notification_settings_default_setting_footnote" = "می‌توانید در %1$@تان تغییرش دهید."; +"screen_room_notification_settings_default_setting_footnote_content_link" = "تنظیمات جهانی"; +"screen_room_notification_settings_default_setting_title" = "تنظیمات پیش‌گزیده"; +"screen_room_notification_settings_edit_remove_setting" = "برداشتن تنظیمات سفارشی"; +"screen_room_notification_settings_error_loading_settings" = "An error occurred while loading notification settings."; +"screen_room_notification_settings_error_restoring_default" = "Failed restoring the default mode, please try again."; +"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again."; +"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room."; +"screen_room_notification_settings_mode_all_messages" = "همهٔ پیام‌ها"; +"screen_room_notification_settings_room_custom_settings_title" = "آگاهی من در این اتاق برای"; +"screen_room_retry_send_menu_send_again_action" = "فرستادن دوباره"; +"screen_room_retry_send_menu_title" = "فرستادن پیامتان شکست خورد"; +"screen_room_roles_and_permissions_admins" = "مدیران"; +"screen_room_roles_and_permissions_change_my_role" = "تغییر نقشم"; +"screen_room_roles_and_permissions_change_role_demote_to_member" = "تنزّل به عضو"; +"screen_room_roles_and_permissions_change_role_demote_to_moderator" = "تنزّل به ناظم"; +"screen_room_roles_and_permissions_member_moderation" = "نظارت اعضا"; +"screen_room_roles_and_permissions_messages_and_content" = "پیام‌ها و محتوا"; +"screen_room_roles_and_permissions_moderators" = "ناظم‌ها"; +"screen_room_roles_and_permissions_permissions_header" = "اجازه‌ها"; +"screen_room_roles_and_permissions_reset" = "بازنشانی اجازه‌ها"; +"screen_room_roles_and_permissions_reset_confirm_description" = "Once you reset permissions, you will lose the current settings."; +"screen_room_roles_and_permissions_reset_confirm_title" = "بازنشانی اجازه‌ها؟"; +"screen_room_roles_and_permissions_roles_header" = "نقش‌ها"; +"screen_room_roles_and_permissions_room_details" = "جزییات اتاق"; +"screen_room_roles_and_permissions_title" = "نقش‌ها و اجازه‌ها"; +"screen_room_timeline_add_reaction" = "افزودن شکلک"; +"screen_room_timeline_beginning_of_room" = "آغاز %1$@ است."; +"screen_room_timeline_beginning_of_room_no_name" = "این، آغاز گفت‌وگوست."; +"screen_room_timeline_less_reactions" = "نمایش کم‌تر"; +"screen_room_timeline_message_copied" = "پیام رونوشت شد"; +"screen_room_timeline_no_permission_to_post" = "اجازهٔ فرستادن به این اتاق را ندارید"; +"screen_room_timeline_reactions_show_more" = "نمایش بیش‌تر"; +"screen_room_timeline_read_marker_title" = "جدید"; +"screen_room_title" = "گپ"; +"screen_room_typing_many_members_first_component_ios" = "%1$@، %2$@ و "; +"screen_room_typing_notification_plural_ios" = " دارند می‌نویسند…"; +"screen_room_typing_notification_singular_ios" = " دارد می‌نویسد…"; +"screen_room_typing_two_members" = "%1$@ و %2$@"; +"screen_room_voice_message_tooltip" = "نگه داشتن برای ضبط"; +"screen_roomlist_a11y_create_message" = "ایجاد اتاق یا گفت‌وگویی جدید"; +"screen_roomlist_empty_message" = "آغاز با پیام دادن به کسی."; +"screen_roomlist_empty_title" = "هنوز گپی وجود ندارد."; +"screen_roomlist_filter_favourites" = "علاقه‌مندی‌ها"; +"screen_roomlist_filter_favourites_empty_state_subtitle" = "You can add a chat to your favourites in the chat settings.\nFor now, you can deselect filters in order to see your other chats"; +"screen_roomlist_filter_favourites_empty_state_title" = "هنوز هیچ گپ مورد علاقه‌ای ندارید"; +"screen_roomlist_filter_invites" = "دعوت‌ها"; +"screen_roomlist_filter_invites_empty_state_title" = "هیچ دعوت منتظری ندارید."; +"screen_roomlist_filter_low_priority" = "اولویت کم"; +"screen_roomlist_filter_mixed_empty_state_subtitle" = "می توانید پالایه‌ها را برای دیدن دیگر گپ‌هایتان بردارید"; +"screen_roomlist_filter_mixed_empty_state_title" = "هیچ گپی برای این گزینش ندارید"; +"screen_roomlist_filter_people_empty_state_title" = "هنوز هیچ پیام مستقیمی ندارید"; +"screen_roomlist_filter_rooms" = "اتاق‌ها"; +"screen_roomlist_filter_rooms_empty_state_title" = "هنوز در هیچ اتاقی نیستید"; +"screen_roomlist_filter_unreads" = "نخوانده‌ها"; +"screen_roomlist_filter_unreads_empty_state_title" = "تبریک!\nهیچ پیام نخوانده‌ای ندارید!"; +"screen_roomlist_main_space_title" = "گپ‌ها"; +"screen_roomlist_mark_as_read" = "علامت‌گذاری به عنوان خوانده شده"; +"screen_roomlist_mark_as_unread" = "نشان به ناخوانده"; +"screen_roomlist_room_directory_button_title" = "مرور همهٔ اتاق‌ها"; +"screen_server_confirmation_message_login_element_dot_io" = "A private server for Element employees."; +"screen_server_confirmation_message_login_matrix_dot_org" = "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."; +"screen_server_confirmation_message_register" = "جایی که گفت‌وگوهایتان خواهند زیست — درست مثل استفاده‌تان از فراهم کنندهٔ رایانامه‌ای برای نگه داشتن رایانامه‌هایتان."; +"screen_server_confirmation_title_login" = "دارید به %1$@ وارد می‌شوید"; +"screen_server_confirmation_title_register" = "دارید روی %1$@ حساب می‌سازید"; +"screen_session_verification_cancelled_subtitle" = "Something doesn’t seem right. Either the request timed out or the request was denied."; +"screen_session_verification_compare_emojis_subtitle" = "Confirm that the emojis below match those shown on your other session."; +"screen_session_verification_compare_emojis_title" = "مقایسهٔ شکلک‌ها"; +"screen_session_verification_compare_numbers_subtitle" = "Confirm that the numbers below match those shown on your other session."; +"screen_session_verification_compare_numbers_title" = "مقایسهٔ اعداد"; +"screen_session_verification_complete_subtitle" = "نشست جدید شما اکنون تأیید شده‌است. این نشست به پیام‌های رمزگذاری شده‌ی شما دسترسی داشته و سایر کاربران نیز این نشست را قابل اعتماد می دانند."; +"screen_session_verification_enter_recovery_key" = "ورود کلید بازیابی"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; +"screen_session_verification_open_existing_session_subtitle" = "Prove it’s you in order to access your encrypted message history."; +"screen_session_verification_open_existing_session_title" = "گشودن نشستی موجود"; +"screen_session_verification_positive_button_canceled" = "تلاش برای تأیید دوباره"; +"screen_session_verification_positive_button_initial" = "آماده‌ام"; +"screen_session_verification_positive_button_verifying_ongoing" = "منتظر تطابق"; +"screen_session_verification_ready_subtitle" = "مقایسهٔ مجموعه‌ای یکتا از شکلک‌ها."; +"screen_session_verification_request_accepted_subtitle" = "شکلک‌ها را مقایسه کنید، از ترتیب نمایش آنان نیز مطمئن شوید."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; +"screen_session_verification_they_dont_match" = "مطابق نیستند"; +"screen_session_verification_they_match" = "مطابقند"; +"screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue."; +"screen_session_verification_waiting_to_accept_title" = "منظر پذیرش درخواست"; +"screen_share_location_title" = "هم‌رسانی موقعیت"; +"screen_share_my_location_action" = "هم‌رسانی مکانم"; +"screen_share_open_apple_maps" = "گشودن در نقشه‌های اپل"; +"screen_share_open_google_maps" = "گشودن در نقشه‌های گوگل"; +"screen_share_open_osm_maps" = "گشودن در اوپن‌استریت‌مپ"; +"screen_share_this_location_action" = "هم‌رسانی این مکان"; +"screen_signed_out_reason_1" = "گذرواژه‌تان را در نشستی دیگر تغییر داده‌اید"; +"screen_signed_out_reason_2" = "این نشست را از نشستی دیگر حذف کرده‌اید"; +"screen_signed_out_reason_3" = "Your server’s administrator has invalidated your access"; +"screen_signed_out_subtitle" = "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %@."; +"screen_signed_out_title" = "خارج شده‌اید"; +"screen_signout_confirmation_dialog_content" = "مطمئنید که می‌خواهید از حسابتان خارج شوید؟"; +"screen_signout_in_progress_dialog_content" = "خارج شدن…"; +"screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; +"screen_signout_key_backup_disabled_title" = "پشتیبان را خاموش کرده‌اید"; +"screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; +"screen_signout_key_backup_ongoing_subtitle" = "لطفاً پیش از خروج منتظر پایانش شوید."; +"screen_signout_key_backup_ongoing_title" = "کلیدهایتان هنوز در حال پشتیبان گیریند"; +"screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; +"screen_signout_recovery_disabled_title" = "بازگردانی برپا نشده"; +"screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; +"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; +"screen_view_location_title" = "مکان"; +"screen_welcome_bullet_1" = "Calls, polls, search and more will be added later this year."; +"screen_welcome_bullet_2" = "Message history for encrypted rooms isn’t available yet."; +"screen_welcome_bullet_3" = "We’d love to hear from you, let us know what you think via the settings page."; +"screen_welcome_button" = "بزن بریم!"; +"screen_welcome_subtitle" = "چیزهایی که باید بدانید:"; +"screen_welcome_title" = "به %1$@ خوش آمدید!"; +"session_verification_banner_message" = "Looks like you’re using a new device. Verify with another device to access your encrypted messages."; +"session_verification_banner_title" = "تأیید کنید که خودتانید"; +"settings_rageshake" = "تکان دادن"; +"settings_rageshake_detection_threshold" = "آستانهٔ تشخیص"; +"settings_version_number" = "نگارش : %1$@ (%2$@)"; +"state_event_avatar_changed_too" = "(چهرک نیز تغییر کرد)"; +"state_event_avatar_url_changed" = "%1$@ چهرکش را تغییر داد"; +"state_event_avatar_url_changed_by_you" = "چهرکتان را تغییر دادید"; +"state_event_demoted_to_member" = "%1$@ به عضو تنزّل یافت"; +"state_event_demoted_to_moderator" = "%1$@ به ناظم تنزّل یافت"; +"state_event_display_name_changed_from" = "%1$@ نام نمایشیش را از %2$@ به%3$@ تغییر داد"; +"state_event_display_name_changed_from_by_you" = "نام نمایشیتان را از %1$@ به %2$@ تغییر دادید"; +"state_event_display_name_removed" = "%1$@ نام نمایشیش را برداشت (%2$@ بود)"; +"state_event_display_name_removed_by_you" = "نام نمایشیتان را برداشتید (%1$@ بود)"; +"state_event_display_name_set" = "%1$@ نام نمایشیش را به %2$@ تغییر داد"; +"state_event_display_name_set_by_you" = "نام نمایشیتان را به %1$@ تغییر دادید"; +"state_event_promoted_to_administrator" = "%1$@ به مدیر ارتقا یافت"; +"state_event_promoted_to_moderator" = "%1$@ به ناظم ارتقا یافت"; +"state_event_room_avatar_changed" = "%1$@ چهرک اتاق را تغییر داد"; +"state_event_room_avatar_changed_by_you" = "چهرک اتاق را تغییر دادید"; +"state_event_room_avatar_removed" = "%1$@ چهرک اتاق را برداشت"; +"state_event_room_avatar_removed_by_you" = "چهرک اتاق را برداشتید"; +"state_event_room_ban" = "%2$@ به دست %1$@ مسدود کرد"; +"state_event_room_ban_by_you" = "%1$@ را مسدود کردید"; +"state_event_room_created" = "%1$@ اتاق را ایجاد کرد"; +"state_event_room_created_by_you" = "اتاق را ساختید"; +"state_event_room_invite" = "%1$@ از %2$@ دعوت کرد"; +"state_event_room_invite_accepted" = "%1$@ دعوت را پذیرفت"; +"state_event_room_invite_accepted_by_you" = "دعوت را پذیرفتید"; +"state_event_room_invite_by_you" = "از %1$@ دعوت کردید"; +"state_event_room_invite_you" = "%1$@ دعوتتان کرد"; +"state_event_room_join" = "%1$@ به اتاق پیوست"; +"state_event_room_join_by_you" = "به اتاق پیوستید"; +"state_event_room_knock" = "%1$@ درخواست پیوستن کرد"; +"state_event_room_knock_accepted" = "%1$@ گذاشت %2$@ بپیوندد"; +"state_event_room_knock_accepted_by_you" = "گذاشتید %1$@ بپیوندد"; +"state_event_room_knock_by_you" = "درخواست پیوستن کردید"; +"state_event_room_knock_denied" = "درخواست پیوستن %2$@ به دست %1$@ لغو شد"; +"state_event_room_knock_denied_by_you" = "درخوسات پیوستن %1$@ را رد کردید"; +"state_event_room_knock_denied_you" = "درخواست پیوستنتان به دست %1$@ رد شد"; +"state_event_room_knock_retracted" = "%1$@ دیگر علاقه‌ای به پیوستن ندارد"; +"state_event_room_knock_retracted_by_you" = "درخواست پیوستنتان را لغو کردید"; +"state_event_room_leave" = "%1$@ اتاق را ترک کرد"; +"state_event_room_leave_by_you" = "اتاق را ترک کردید"; +"state_event_room_name_changed" = "%1$@ نام اتاق را تغییر داد: %2$@"; +"state_event_room_name_changed_by_you" = "نام اتاق را تغییر دادید: %1$@"; +"state_event_room_name_removed" = "%1$@نام اتاق را برداشت"; +"state_event_room_name_removed_by_you" = "نام اتاق را برداشتید"; +"state_event_room_none" = "%1$@ تغییری ایجاد نکرد"; +"state_event_room_none_by_you" = "تغییری ایجاد نکردید"; +"state_event_room_pinned_events_changed" = "%1$@ پیام‌های سنجاق شده را تغییر داد"; +"state_event_room_pinned_events_changed_by_you" = "پیام‌های سنجاق شده را تغییر دادید"; +"state_event_room_pinned_events_pinned" = "%1$@ پیامی را سنجاق کرد"; +"state_event_room_pinned_events_pinned_by_you" = "پیامی را سنجاق کردید"; +"state_event_room_pinned_events_unpinned" = "%1$@ سنجاق پیامی را برداشت"; +"state_event_room_pinned_events_unpinned_by_you" = "سنجاق پیامی را برداشتید"; +"state_event_room_reject" = "%1$@ دعوت را رد کرد"; +"state_event_room_reject_by_you" = "دعوت را رد کردید"; +"state_event_room_remove" = "%2$@ به دست %1$@ برداشته شد"; +"state_event_room_remove_by_you" = "%1$@ را برداشتید"; +"state_event_room_third_party_invite" = "%1$@ دعوتی برای پیوستن %2$@ به اتاق فرستاد"; +"state_event_room_third_party_invite_by_you" = "برای %1$@ دعوت پیوستن به اتاق فرستادید"; +"state_event_room_third_party_revoked_invite" = "%1$@ دعوت پیوستن به اتاق %2$@ را باطل کرد"; +"state_event_room_third_party_revoked_invite_by_you" = "دعوت پیوستن %1$@ به اتاق را پس گرفتید"; +"state_event_room_topic_changed" = "%1$@ موضوع را تغییر داد: %2$@"; +"state_event_room_topic_changed_by_you" = "موضوع را تغییر دادید: %1$@"; +"state_event_room_topic_removed" = "%1$@موضوع اتاق را برداشت"; +"state_event_room_topic_removed_by_you" = "موضوع اتاق را برداشتید"; +"state_event_room_unban" = "%1$@ انسداد %2$@ را لغو کرد"; +"state_event_room_unban_by_you" = "انسداد%1$@ را لغو کردید"; +"state_event_room_unknown_membership_change" = "%1$@ تغییری نامعلوم در عضویتش داد"; +"test_language_identifier" = "fa"; +"test_untranslated_default_language_identifier" = "en"; +"troubleshoot_notifications_entry_point_section" = "رفع‌اشکال"; +"troubleshoot_notifications_screen_action" = "اجرای آزمون‌ها"; +"troubleshoot_notifications_screen_action_again" = "اجرای دوبارهٔ آزمون‌ها"; +"troubleshoot_notifications_screen_failure" = "برخی آزمون‌ها شکست خوردند. بررسی جزییات."; +"troubleshoot_notifications_screen_notice" = "Run the tests to detect any issue in your configuration that may make notifications not behave as expected."; +"troubleshoot_notifications_screen_quick_fix_action" = "تلاش برای تعمیر"; +"troubleshoot_notifications_screen_success" = "همهٔ آزمون‌ها با موفّقیت گذرانده شدند."; +"troubleshoot_notifications_screen_title" = "رفع‌اشکال آگاهی‌ها"; +"troubleshoot_notifications_screen_waiting" = "Some tests require your attention. Please check the details."; +"troubleshoot_notifications_test_check_permission_description" = "Check that the application can show notifications."; +"troubleshoot_notifications_test_check_permission_title" = "بررسی اجازه‌ها"; +"troubleshoot_notifications_test_current_push_provider_description" = "Get the name of the current provider."; +"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected."; +"troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@."; +"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider"; +"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider."; +"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found."; +"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers"; +"troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification."; +"troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked."; +"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification."; +"troubleshoot_notifications_test_display_notification_success" = "روی آگاهی کلیک شد!"; +"troubleshoot_notifications_test_display_notification_title" = "Display notification"; +"troubleshoot_notifications_test_display_notification_waiting" = "Please click on the notification to continue the test."; +"troubleshoot_notifications_test_firebase_availability_description" = "Ensure that Firebase is available."; +"troubleshoot_notifications_test_firebase_availability_failure" = "Firebase is not available."; +"troubleshoot_notifications_test_firebase_availability_success" = "Firebase is available."; +"troubleshoot_notifications_test_firebase_availability_title" = "Check Firebase"; +"troubleshoot_notifications_test_firebase_token_description" = "Ensure that Firebase token is available."; +"troubleshoot_notifications_test_firebase_token_failure" = "Firebase token is not known."; +"troubleshoot_notifications_test_firebase_token_success" = "Firebase token: %1$@."; +"troubleshoot_notifications_test_firebase_token_title" = "Check Firebase token"; +"troubleshoot_notifications_test_push_loop_back_description" = "Ensure that the application is receiving push."; +"troubleshoot_notifications_test_push_loop_back_failure_1" = "خطا: فرستنده درخواست را رد کرد."; +"troubleshoot_notifications_test_push_loop_back_failure_2" = "خطا: %1$@."; +"troubleshoot_notifications_test_push_loop_back_failure_3" = "خطا. نتوانست فرستادن را بیازماید."; +"troubleshoot_notifications_test_push_loop_back_failure_4" = "خطا. مهلت زمانی انتظار برای فرستادن سر رسید."; +"troubleshoot_notifications_test_push_loop_back_success" = "Push loop back took %1$d ms."; +"troubleshoot_notifications_test_push_loop_back_title" = "Test Push loop back"; +"troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; +"troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; +"troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "نظرسنجی"; +"banner_set_up_recovery_submit" = "برپایی بازیابی"; +"dialog_title_error" = "خطا"; +"dialog_title_success" = "موفّقیت"; +"notification_fallback_content" = "آگاهی"; +"notification_invitation_action_join" = "پیوستن"; +"notification_invitation_action_reject" = "رد کردن"; +"notification_room_action_mark_as_read" = "علامت‌گذاری به عنوان خوانده شده"; +"notification_room_action_quick_reply" = "پاسخ سریع"; +"screen_pinned_timeline_screen_title_empty" = "پیام‌های سنجاق شده"; +"screen_room_mentions_at_room_title" = "هرکسی"; +"screen_account_provider_change" = "تغییر فراهم کنندهٔ حساب"; +"screen_account_provider_signin_subtitle" = "جایی که گفت‌وگوهایتان خواهند زیست — درست مثل استفاده‌تان از فراهم کنندهٔ رایانامه‌ای برای نگه داشتن رایانامه‌هایتان."; +"screen_account_provider_signup_subtitle" = "جایی که گفت‌وگوهایتان خواهند زیست — درست مثل استفاده‌تان از فراهم کنندهٔ رایانامه‌ای برای نگه داشتن رایانامه‌هایتان."; +"screen_analytics_settings_help_us_improve" = "Share anonymous usage data to help us identify issues."; +"screen_analytics_settings_read_terms" = "You can read all our terms %1$@."; +"screen_analytics_settings_read_terms_content_link" = "این‌جا"; +"screen_blocked_users_unblock_alert_action" = "رفع انسداد"; +"screen_blocked_users_unblock_alert_description" = "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."; +"screen_blocked_users_unblock_alert_title" = "رفع انسداد کاربر"; +"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"screen_chat_backup_recovery_action_confirm" = "ورود کلید بازیابی"; +"screen_chat_backup_recovery_action_setup" = "برپایی بازیابی"; +"screen_create_poll_cancel_confirmation_content_ios" = "تغییراتتان ذخیره نمی‌شوند"; +"screen_create_room_add_people_title" = "دعوت افراد"; +"screen_create_room_room_name_label" = "نام اتاق"; +"screen_create_room_title" = "ایجاد اتاق"; +"screen_dm_details_block_alert_action" = "بلوک"; +"screen_dm_details_block_alert_description" = "Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime."; +"screen_dm_details_block_user" = "انسداد کاربر"; +"screen_dm_details_unblock_alert_action" = "رفع انسداد"; +"screen_dm_details_unblock_alert_description" = "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."; +"screen_dm_details_unblock_user" = "رفع انسداد کاربر"; +"screen_edit_poll_delete_confirmation_title" = "حذف نظرسنجی"; +"screen_edit_poll_title" = "ویرایش نظرسنجی"; +"screen_identity_use_another_device" = "استفاده از افزاره‌ای دیگر"; +"screen_login_subtitle" = "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است."; +"screen_notification_settings_mentions_section_title" = "اشاره‌ها"; +"screen_qr_code_login_invalid_scan_state_retry_button" = "تلاش دوباره"; +"screen_recovery_key_change_generate_key_description" = "اطمینان از امکان نگه داری کلید بازیابیتان در جایی امن"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; +"screen_report_content_block_user" = "انسداد کاربر"; +"screen_reset_encryption_password_placeholder" = "ورود…"; +"screen_room_attachment_source_camera_photo" = "عکس گرفتن"; +"screen_room_change_permissions_everyone" = "هرکسی"; +"screen_room_change_permissions_member_moderation" = "نظارت اعضا"; +"screen_room_change_permissions_messages_and_content" = "پیام‌ها و محتوا"; +"screen_room_change_permissions_room_details" = "جزییات اتاق"; +"screen_room_change_role_section_administrators" = "مدیران"; +"screen_room_change_role_section_moderators" = "ناظم‌ها"; +"screen_room_change_role_section_users" = "اعضا"; +"screen_room_change_role_unsaved_changes_title" = "ذخیرهٔ تغییرات؟"; +"screen_room_details_invite_people_title" = "دعوت افراد"; +"screen_room_details_leave_conversation_title" = "ترک گفت‌وگو"; +"screen_room_details_leave_room_title" = "ترک اتاق"; +"screen_room_details_notification_title" = "آگاهی‌ها"; +"screen_room_details_roles_and_permissions" = "نقش‌ها و اجازه‌ها"; +"screen_room_details_room_name_label" = "نام اتاق"; +"screen_room_details_security_title" = "امنیت"; +"screen_room_details_topic_title" = "موضوع"; +"screen_room_error_failed_processing_media" = "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "برداشت و تحریم عضو"; +"screen_room_notification_settings_mode_mentions_and_keywords" = "فقط اشاره‌ها و کلیدواژگان"; +"screen_room_timeline_reactions_show_less" = "نمایش کم‌تر"; +"screen_roomlist_filter_people" = "افراد"; +"screen_server_confirmation_change_server" = "تغییر فراهم کنندهٔ حساب"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; +"screen_signout_confirmation_dialog_submit" = "خروج"; +"screen_signout_confirmation_dialog_title" = "خروج"; +"screen_signout_key_backup_offline_title" = "کلیدهایتان هنوز در حال پشتیبان گیریند"; +"screen_signout_preference_item" = "خروج"; +"screen_signout_save_recovery_key_title" = "کلید بازیابیتان را ذخیره کرده‌اید؟"; +"troubleshoot_notifications_entry_point_title" = "رفع‌اشکال آگاهی‌ها"; diff --git a/ElementX/Resources/Localizations/fa.lproj/SAS.strings b/ElementX/Resources/Localizations/fa.lproj/SAS.strings new file mode 100644 index 0000000000..42b46e16d6 --- /dev/null +++ b/ElementX/Resources/Localizations/fa.lproj/SAS.strings @@ -0,0 +1,64 @@ +"aeroplane" = "هواپیما"; +"anchor" = "لنگر"; +"apple" = "سیب"; +"ball" = "توپ"; +"banana" = "موز"; +"bell" = "زنگ"; +"bicycle" = "دوچرخه"; +"book" = "کتاب"; +"butterfly" = "پروانه"; +"cactus" = "کاکتوس"; +"cake" = "کیک"; +"cat" = "گربه"; +"clock" = "ساعت"; +"cloud" = "ابر"; +"corn" = "ذرت"; +"dog" = "سگ"; +"elephant" = "فیل"; +"fire" = "آتش"; +"fish" = "ماهی"; +"flag" = "پرچم"; +"flower" = "گل"; +"folder" = "پوشه"; +"gift" = "هدیه"; +"glasses" = "عینک"; +"globe" = "زمین"; +"guitar" = "گیتار"; +"hammer" = "چکش"; +"hat" = "کلاه"; +"headphones" = "هدفون"; +"heart" = "قلب"; +"horse" = "اسب"; +"hourglass" = "ساعت شنی"; +"key" = "کلید"; +"light_bulb" = "لامپ"; +"lion" = "شیر"; +"lock" = "قفل"; +"moon" = "ماه"; +"mushroom" = "قارچ"; +"octopus" = "اختاپوس"; +"panda" = "پاندا"; +"paperclip" = "گیره کاغذ"; +"pencil" = "مداد"; +"penguin" = "پنگوئن"; +"pig" = "خوک"; +"pin" = "سنجاق"; +"pizza" = "پیتزا"; +"rabbit" = "خرگوش"; +"robot" = "ربات"; +"rocket" = "موشک"; +"rooster" = "خروس"; +"santa" = "بابا نوئل"; +"scissors" = "قیچی"; +"smiley" = "خنده"; +"spanner" = "آچار"; +"strawberry" = "توت فرنگی"; +"telephone" = "تلفن"; +"thumbs_up" = "لایک"; +"train" = "قطار"; +"tree" = "درخت"; +"trophy" = "جام"; +"trumpet" = "شیپور"; +"turtle" = "لاک‌پشت"; +"umbrella" = "چتر"; +"unicorn" = "تک شاخ"; \ No newline at end of file diff --git a/ElementX/Resources/Localizations/fr.lproj/Localizable.strings b/ElementX/Resources/Localizations/fr.lproj/Localizable.strings index c2ee5e78fb..5103afcbd0 100644 --- a/ElementX/Resources/Localizations/fr.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/fr.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pause"; "a11y_pin_field" = "Code PIN"; "a11y_play" = "Lecture"; -"a11y_poll" = "Sondage"; "a11y_poll_end" = "Sondage terminé"; "a11y_react_with" = "Réagir avec %1$@"; "a11y_react_with_other_emojis" = "Réagir avec d’autres emojis"; @@ -33,14 +32,15 @@ "action_close" = "Fermer"; "action_complete_verification" = "Terminer la vérification"; "action_confirm" = "Confirmer"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Confirmez le mot de passe"; "action_continue" = "Continuer"; "action_copy" = "Copier"; "action_copy_link" = "Copier le lien"; "action_copy_link_to_message" = "Copier le lien vers le message"; "action_create" = "Créer"; "action_create_a_room" = "Créer un salon"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Désactiver"; +"action_deactivate_account" = "Désactiver le compte"; "action_decline" = "Refuser"; "action_delete_poll" = "Supprimer le sondage"; "action_disable" = "Désactiver"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Mot de passe oublié ?"; "action_forward" = "Transférer"; "action_go_back" = "Retour"; +"action_ignore" = "Ignorer"; "action_invite" = "Inviter"; "action_invite_friends" = "Inviter des amis"; "action_invite_friends_to_app" = "Inviter des amis à %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Quitter"; "action_leave_conversation" = "Quitter la discussion"; "action_leave_room" = "Quitter le salon"; +"action_load_more" = "Voir plus"; "action_manage_account" = "Gérer le compte"; "action_manage_devices" = "Gérez les sessions"; "action_message" = "Message"; @@ -84,7 +86,7 @@ "action_report_bug" = "Signaler un problème"; "action_report_content" = "Signaler le contenu"; "action_reset" = "Réinitialiser"; -"action_reset_identity" = "Reset identity"; +"action_reset_identity" = "Réinitialiser l'identité"; "action_retry" = "Réessayer"; "action_retry_decryption" = "Réessayer le déchiffrement"; "action_save" = "Enregistrer"; @@ -93,6 +95,7 @@ "action_send_message" = "Envoyer un message"; "action_share" = "Partager"; "action_share_link" = "Partager le lien"; +"action_show" = "Afficher"; "action_sign_in_again" = "Se connecter à nouveau"; "action_signout" = "Se déconnecter"; "action_signout_anyway" = "Se déconnecter quand même"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Voir dans la discussion"; "action_view_source" = "Afficher la source"; "action_yes" = "Oui"; -"action.load_more" = "Voir plus"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Déconnecter et mettre à niveau"; "banner_migrate_to_native_sliding_sync_description" = "Votre serveur prend désormais en charge un nouveau protocole plus rapide. Déconnectez-vous, puis reconnectez-vous pour effectuer la mise à niveau dès maintenant. En le faisant tout de suite, vous éviterez une déconnexion forcée lorsque l'ancien protocole sera supprimé."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Votre serveur d’accueil ne prend plus en charge l'ancien protocole. Veuillez vous déconnecter puis vous reconnecter pour continuer à utiliser l'application."; "banner_migrate_to_native_sliding_sync_title" = "Mise à niveau disponible"; -"banner.set_up_recovery.content" = "Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l'historique de vos messages chiffrés au cas où vous perdriez l'accès à vos appareils."; -"banner.set_up_recovery.title" = "Configurer la récupération"; +"banner_set_up_recovery_content" = "Générez une nouvelle clé de récupération qui peut être utilisée pour restaurer l'historique de vos messages chiffrés au cas où vous perdriez l'accès à vos appareils."; +"banner_set_up_recovery_title" = "Configurer la récupération"; "common_about" = "À propos"; "common_acceptable_use_policy" = "Politique d’utilisation acceptable"; "common_advanced_settings" = "Paramètres avancés"; @@ -133,10 +134,12 @@ "common_dark" = "Sombre"; "common_decryption_error" = "Erreur de déchiffrement"; "common_developer_options" = "Options pour les développeurs"; +"common_device_id" = "Identifiant de session"; "common_direct_chat" = "Discussion à deux"; "common_edited_suffix" = "(modifié)"; "common_editing" = "Édition"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Chiffrement"; "common_encryption_enabled" = "Chiffrement activé"; "common_enter_your_pin" = "Saisissez votre code PIN"; "common_error" = "Erreur"; @@ -147,6 +150,7 @@ "common_favourited" = "Favorisé"; "common_file" = "Fichier"; "common_forward_message" = "Transférer le message"; +"common_frequently_used" = "Fréquemment utilisé"; "common_gif" = "GIF"; "common_image" = "Image"; "common_in_reply_to" = "En réponse à %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderne"; "common_mute" = "Mettre en sourdine"; "common_no_results" = "Aucun résultat"; +"common_no_room_name" = "Salon sans nom"; "common_offline" = "Hors ligne"; "common_optic_id_ios" = "Optic ID"; "common_or" = "ou"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalien"; "common_permission" = "Autorisation"; "common_please_wait" = "Veuillez patienter…"; +"common_poll_end_confirmation" = "Êtes-vous sûr de vouloir mettre fin à ce sondage ?"; +"common_poll_summary" = "Sondage : %1$@"; "common_poll_total_votes" = "Nombre total de votes : %1$@"; "common_poll_undisclosed_text" = "Les résultats s’afficheront une fois le sondage terminé"; "common_privacy_policy" = "Politique de confidentialité"; @@ -200,6 +207,7 @@ "common_settings" = "Paramètres"; "common_shared_location" = "Position partagée"; "common_signing_out" = "Déconnexion"; +"common_something_went_wrong" = "Une erreur s'est produite"; "common_starting_chat" = "Création de la discussion..."; "common_sticker" = "Autocollant"; "common_success" = "Succès"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "De quoi s’agit-il dans ce salon ?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Échec de déchiffrement"; +"common_unable_to_decrypt_no_access" = "Vous ne pouvez pas voir ce message"; "common_unable_to_invite_message" = "Les invitations n’ont pas pu être envoyées à un ou plusieurs utilisateurs."; "common_unable_to_invite_title" = "Impossible d’envoyer une ou plusieurs invitations"; "common_unlock" = "Déverrouillage"; @@ -221,23 +230,30 @@ "common_username" = "Nom d’utilisateur"; "common_verification_cancelled" = "Vérification annulée"; "common_verification_complete" = "Vérification terminée"; +"common_verification_failed" = "Échec de la vérification"; +"common_verified" = "Vérifié(e)"; +"common_verify_device" = "Vérifier la session"; +"common_verify_identity" = "Verify identity"; "common_video" = "Vidéo"; "common_voice_message" = "Message vocal"; "common_waiting" = "En attente..."; "common_waiting_for_decryption_key" = "En attente de la clé de déchiffrement"; +"common.copied_to_clipboard" = "Copié dans le presse-papiers"; "common.do_not_show_this_again" = "Ne plus afficher"; "common.open_source_licenses" = "Licences open source"; "common.pinned" = "Épinglé"; "common.send_to" = "Envoyer vers"; -"common_no_room_name" = "Salon sans nom"; -"common_poll_end_confirmation" = "Êtes-vous sûr de vouloir mettre fin à ce sondage ?"; -"common_poll_summary" = "Sondage : %1$@"; -"common_something_went_wrong" = "Une erreur s'est produite"; -"common_unable_to_decrypt_no_access" = "Vous ne pouvez pas voir ce message"; -"common_verify_device" = "Vérifier la session"; +"common.you" = "Vous"; +"common_unable_to_decrypt_insecure_device" = "Envoyé depuis un appareil non sécurisé"; +"common_unable_to_decrypt_verification_violation" = "L'identité vérifiée de l'expéditeur a changé"; "confirm_recovery_key_banner_message" = "La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Confirmer votre clé de récupération"; "crash_detection_dialog_content" = "%1$@ s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?"; +"crypto_identity_change_pin_violation" = "L'identité de %1$@ semble avoir changé. %2$@"; +"crypto_identity_change_pin_violation_new" = "L'identité de %1$@ %2$@ semble avoir changé. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Pour permettre à l’application d’utiliser l’appareil photo, veuillez accorder l’autorisation dans les paramètres du système."; "dialog_permission_generic" = "Veuillez accorder l’autorisation dans les paramètres du système."; "dialog_permission_location_description_ios" = "Autorisez l’accès dans Paramètres / Localisation."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Notifications silencieuses"; "notification_incoming_call" = "Appel entrant"; "notification_inline_reply_failed" = "** Échec de l’envoi - veuillez ouvrir le salon"; -"notification_invitation_action_reject" = "Rejeter"; "notification_invite_body" = "Vous a invité(e) à discuter"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ vous a invité à discuter"; "notification_mentioned_you_body" = "Mentionné(e): %1$@"; "notification_new_messages" = "Nouveaux messages"; "notification_reaction_body" = "A réagi avec %1$@"; "notification_room_invite_body" = "Vous a invité(e) à rejoindre le salon"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ vous a invité à rejoindre le salon"; "notification_sender_me" = "Moi"; "notification_sender_mention_reply" = "%1$@ mentionné ou en réponse"; "notification_test_push_notification_content" = "Vous êtes en train de voir la notification ! Cliquez-moi !"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Décaler vers la gauche"; "rich_text_editor_url_placeholder" = "Lien"; "rich_text_editor_a11y_add_attachment" = "Ajouter une pièce jointe"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "URL de base pour Element Call personnalisée"; "screen_advanced_settings_element_call_base_url_description" = "Configurer une URL de base pour Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte."; +"screen_create_room_room_address_section_footer" = "Pour que ce salon soit visible dans le répertoire des salons publics, vous aurez besoin d'une adresse de salon."; +"screen_create_room_room_address_section_title" = "Adresse du salon"; +"screen_create_room_room_visibility_section_title" = "Visibilité du salon"; +"screen_create_room_access_section_anyone_option_description" = "Tout le monde peut rejoindre ce salon"; +"screen_create_room_access_section_anyone_option_title" = "Tout le monde"; +"screen_create_room_access_section_header" = "Accès au salon"; +"screen_create_room_access_section_knocking_option_description" = "Tout le monde peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande"; +"screen_create_room_access_section_knocking_option_title" = "Demander à rejoindre"; +"screen_join_room_cancel_knock_action" = "Annuler la demande"; +"screen_join_room_cancel_knock_alert_confirmation" = "Oui, annuler"; +"screen_join_room_cancel_knock_alert_description" = "Êtes-vous sûr de vouloir annuler votre demande d'accès à ce salon?"; +"screen_join_room_cancel_knock_alert_title" = "Annuler la demande d'adhésion"; +"screen_join_room_knock_message_description" = "Message (facultatif)"; +"screen_join_room_knock_sent_description" = "Vous recevrez une invitation à rejoindre le salon si votre demande est acceptée."; +"screen_join_room_knock_sent_title" = "Demande de rejoindre le salon envoyée"; "screen_pinned_timeline_empty_state_description" = "Cliquez (clic long) sur un message et choisissez « %1$@ » pour qu‘il apparaisse ici."; "screen_pinned_timeline_empty_state_headline" = "Épinglez les messages importants pour leur donner plus de visibilité"; -"screen_pinned_timeline_screen_title_empty" = "Messages épinglés"; "screen_reset_encryption_password_error" = "Une erreur s'est produite. Vérifiez que le mot de passe de votre compte est correct et réessayez."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Révoquer la verification et envoyer"; "screen_resolve_send_failure_changed_identity_subtitle" = "Vous pouvez révoquer la verification et envoyer ce message, ou vous pouvez annuler pour l'instant et réessayer plus tard après avoir vérifié à nouveau %1$@."; @@ -342,18 +372,18 @@ "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Envoyer le message quand même"; "screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ utilise un ou plusieurs appareils non vérifiés. Vous pouvez quand même envoyer le message, ou vous pouvez annuler pour l'instant et réessayer plus tard après que %2$@ vérifie tous ses appareils."; "screen_resolve_send_failure_unsigned_device_title" = "Votre message n'a pas été envoyé car %1$@ n'a pas vérifié tous ses appareils"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Un ou plusieurs de vos appareils ne sont pas vérifiés. Vous pouvez quand même envoyer le message, ou vous pouvez annuler et réessayer plus tard après avoir vérifié tous vos appareils."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Votre message n'a pas été envoyé car vous n'avez pas vérifié tous vos appareils"; "screen_room_mentions_at_room_subtitle" = "Notifier tout le salon"; "screen_room_pinned_banner_indicator" = "%1$@ sur %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Messages épinglés"; "screen_room_pinned_banner_loading_description" = "Chargement du message..."; "screen_room_pinned_banner_view_all_button_title" = "Voir tout"; "screen_room_details_pinned_events_row_title" = "Messages épinglés"; +"screen_roomlist_knock_event_sent_description" = "Demande d'adhésion envoyée"; "screen_timeline_item_menu_send_failure_changed_identity" = "Le message n'a pas été envoyé car l'identité vérifiée de %1$@ a changé."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Le message n'a pas été envoyé car %1$@ n'a pas vérifié tous ses appareils."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Changer de fournisseur de compte"; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message non envoyé car vous n'avez pas vérifié tous vos appareils."; "screen_account_provider_form_hint" = "Adresse du serveur d’accueil"; "screen_account_provider_form_notice" = "Entrez un terme de recherche ou une adresse de domaine."; "screen_account_provider_form_subtitle" = "Recherchez une entreprise, une communauté ou un serveur privé."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Vous êtes sur le point de créer un compte sur %@"; "screen_advanced_settings_developer_mode" = "Mode développeur"; "screen_advanced_settings_developer_mode_description" = "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."; +"screen_advanced_settings_media_compression_description" = "Optimisé pour le téléchargement"; +"screen_advanced_settings_media_compression_title" = "Media"; "screen_advanced_settings_rich_text_editor_description" = "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."; "screen_advanced_settings_send_read_receipts" = "Accusés de lecture"; "screen_advanced_settings_send_read_receipts_description" = "En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Choisissez votre serveur"; "screen_chat_backup_key_backup_action_disable" = "Désactiver la sauvegarde"; "screen_chat_backup_key_backup_action_enable" = "Activer la sauvegarde"; -"screen_chat_backup_key_backup_description" = "La sauvegarde assure que vous ne perdiez pas l’historique des discussions. %1$@."; -"screen_chat_backup_key_backup_title" = "Sauvegarde"; +"screen_chat_backup_key_backup_description" = "Stockez votre identité cryptographique et vos clés de message en toute sécurité sur le serveur. Cela vous permettra de consulter l'historique de vos messages sur tous les nouveaux appareils. %1$@."; +"screen_chat_backup_key_backup_title" = "Stockage des clés"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Télécharger les clés depuis cet appareil"; +"screen_chat_backup_key_storage_toggle_title" = "Autoriser le stockage des clés"; "screen_chat_backup_recovery_action_change" = "Changer la clé de récupération"; -"screen_chat_backup_recovery_action_confirm" = "Confirmer la clé de récupération"; +"screen_chat_backup_recovery_action_change_description" = "Récupérez votre identité cryptographique et l'historique de vos messages à l'aide d'une clé de récupération si vous avez perdu tous vos appareils existants."; "screen_chat_backup_recovery_action_confirm_description" = "La sauvegarde des discussions est désynchronisée."; -"screen_chat_backup_recovery_action_setup" = "Configurer la récupération"; "screen_chat_backup_recovery_action_setup_description" = "Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$@ partout."; "screen_create_account_title" = "Créer un compte"; "screen_create_new_recovery_key_list_item_1" = "Ouvrez %1$@ sur un ordinateur"; @@ -447,29 +481,28 @@ "screen_create_poll_anonymous_desc" = "Afficher les résultats uniquement après la fin du sondage"; "screen_create_poll_anonymous_headline" = "Masquer les votes"; "screen_create_poll_answer_hint" = "Option %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Vos modifications ne seront pas enregistrées"; "screen_create_poll_cancel_confirmation_title_ios" = "Annuler le sondage"; "screen_create_poll_question_desc" = "Question ou sujet"; "screen_create_poll_question_hint" = "Quel est le sujet du sondage ?"; "screen_create_poll_title" = "Créer un sondage"; "screen_create_room_action_create_room" = "Nouveau salon"; "screen_create_room_error_creating_room" = "Une erreur s’est produite lors de la création du salon"; -"screen_create_room_private_option_description" = "Les messages dans ce salon sont chiffrés. Le chiffrement ne pourra pas être désactivé par la suite."; -"screen_create_room_private_option_title" = "Salon privé (sur invitation seulement)"; -"screen_create_room_public_option_description" = "Les messages ne sont pas chiffrés et n’importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."; -"screen_create_room_public_option_title" = "Salon public (tout le monde)"; +"screen_create_room_private_option_description" = "Seules les personnes invitées peuvent accéder à ce salon. Tous les messages sont chiffrés de bout en bout."; +"screen_create_room_private_option_title" = "Salon privé"; +"screen_create_room_public_option_description" = "N'importe qui peut trouver ce salon.\nVous pouvez modifier cela à tout moment dans les paramètres du salon."; +"screen_create_room_public_option_title" = "Salon public"; "screen_create_room_topic_label" = "Sujet (facultatif)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Veuillez confirmer que vous souhaitez désactiver votre compte. Cette action ne peut pas être annulée."; +"screen_deactivate_account_delete_all_messages" = "Supprimer tous mes messages"; +"screen_deactivate_account_delete_all_messages_notice" = "Attention : les futurs utilisateurs pourraient voir des conversations incomplètes."; +"screen_deactivate_account_description" = "La désactivation de votre compte est %1$@, cela va:"; +"screen_deactivate_account_description_bold_part" = "irréversible"; +"screen_deactivate_account_list_item_1" = "%1$@ votre compte (vous ne pourrez plus vous reconnecter et votre identifiant ne pourra pas être réutilisé)."; +"screen_deactivate_account_list_item_1_bold_part" = "Désactiver définitivement"; +"screen_deactivate_account_list_item_2" = "Vous retirer de tous les salons et toutes les discussions."; +"screen_deactivate_account_list_item_3" = "Supprimer les informations de votre compte du serveur d'identité."; +"screen_deactivate_account_list_item_4" = "Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés."; +"screen_deactivate_account_title" = "Désactiver votre compte"; "screen_edit_poll_delete_confirmation" = "Êtes-vous certain de vouloir supprimer ce sondage?"; "screen_edit_profile_display_name" = "Pseudonyme"; "screen_edit_profile_display_name_placeholder" = "Votre pseudonyme"; @@ -482,7 +515,7 @@ "screen_encryption_reset_bullet_2" = "Vous perdrez l’historique de vos messages"; "screen_encryption_reset_bullet_3" = "Vous devrez vérifier à nouveau tous vos appareils et tous vos contacts"; "screen_encryption_reset_footer" = "Ne réinitialisez votre identité que si vous n'avez plus accès à aucune autre session et que vous avez perdu votre clé de récupération."; -"screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; +"screen_encryption_reset_title" = "Vous ne pouvez pas confirmer ? Vous devez réinitialiser votre identité."; "screen_identity_confirmation_cannot_confirm" = "Confirmation impossible ?"; "screen_identity_confirmation_create_new_recovery_key" = "Créer une nouvelle clé de récupération"; "screen_identity_confirmation_subtitle" = "Vérifier cette session pour configurer votre messagerie sécurisée."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Discussions de groupe"; "screen_notification_settings_invite_for_me_label" = "Invitations"; "screen_notification_settings_mentions_only_disclaimer" = "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."; -"screen_notification_settings_mentions_section_title" = "Mentions"; "screen_notification_settings_mode_all" = "Tous"; "screen_notification_settings_mode_mentions" = "Mentions"; "screen_notification_settings_notification_section_title" = "Prévenez-moi pour"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Choisissez %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Associer une nouvelle session”"; "screen_qr_code_login_initial_state_item_4" = "Scanner le code QR avec cet appareil"; +"screen_qr_code_login_initial_state_subtitle" = "Disponible uniquement si votre fournisseur de compte le supporte."; "screen_qr_code_login_initial_state_title" = "Ouvrez %1$@ sur un autre appareil pour obtenir le QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Scannez le QR code affiché sur l'autre appareil."; "screen_qr_code_login_invalid_scan_state_subtitle" = "QR code erroné"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Votre code de vérification"; "screen_recovery_key_change_description" = "Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."; "screen_recovery_key_change_generate_key" = "Générer une nouvelle clé"; -"screen_recovery_key_change_generate_key_description" = "Assurez-vous de conserver la clé dans un endroit sûr"; "screen_recovery_key_change_success" = "Clé de récupération modifée"; "screen_recovery_key_change_title" = "Changer la clé de récupération?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Créer une nouvelle clé de récupération"; @@ -616,18 +648,17 @@ "screen_recovery_key_confirm_key_placeholder" = "Saisissez la clé ici…"; "screen_recovery_key_confirm_lost_recovery_key" = "Clé de récupération perdue?"; "screen_recovery_key_confirm_success" = "Clé de récupération confirmée"; -"screen_recovery_key_confirm_title" = "Saisissez votre clé de récupération"; "screen_recovery_key_copied_to_clipboard" = "Clé de récupération copiée"; "screen_recovery_key_generating_key" = "Génération…"; "screen_recovery_key_save_action" = "Enregistrer la clé"; -"screen_recovery_key_save_description" = "Recopier votre clé de récupération dans un endroit sécurisé ou enregistrer la dans un manager de mot de passe."; +"screen_recovery_key_save_description" = "Recopier cette clé de récupération dans un endroit sûr, comme un gestionnaire de mots de passe, une note chiffrée ou un coffre-fort physique."; "screen_recovery_key_save_key_description" = "Taper pour copier la clé"; "screen_recovery_key_save_title" = "Sauvegarder la clé"; "screen_recovery_key_setup_confirmation_description" = "La clé ne pourra plus être affichée après cette étape."; "screen_recovery_key_setup_confirmation_title" = "Avez-vous sauvegardé votre clé de récupération?"; "screen_recovery_key_setup_description" = "Votre sauvegarde est protégée par votre clé de récupération. Si vous avez besoin d’une nouvelle clé après la configuration, vous pourrez en créer une nouvelle en cliquant sur \"Changer la clé de récupération\"."; "screen_recovery_key_setup_generate_key" = "Générer la clé de récupération"; -"screen_recovery_key_setup_generate_key_description" = "Assurez-vous de conserver la clé dans un endroit sûr"; +"screen_recovery_key_setup_generate_key_description" = "Ne partagez cela avec personne !"; "screen_recovery_key_setup_success" = "Sauvegarde mise en place avec succès"; "screen_recovery_key_setup_title" = "Configurer la sauvegarde"; "screen_report_content_block_user_hint" = "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur."; @@ -635,8 +666,7 @@ "screen_report_content_hint" = "Raison du signalement de ce contenu"; "screen_reset_encryption_confirmation_alert_action" = "Oui, réinitialisez maintenant"; "screen_reset_encryption_confirmation_alert_subtitle" = "Cette opération ne peut pas être annulée."; -"screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Saisissez..."; +"screen_reset_encryption_confirmation_alert_title" = "Êtes-vous sûr de vouloir réinitialiser votre identité ?"; "screen_reset_encryption_password_subtitle" = "Veuillez confirmer que vous souhaitez réinitialiser votre identité."; "screen_reset_encryption_password_title" = "Saisissez le mot de passe de votre compte pour continuer"; "screen_reset_identity_confirmation_subtitle" = "Vous êtes sur le point d'accéder à votre compte %1$@ pour réinitialiser votre identité. Vous serez ensuite redirigé vers l'application."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Les administrateurs ont automatiquement les privilèges des modérateurs"; "screen_room_change_role_moderators_title" = "Modifier les modérateurs"; "screen_room_change_role_unsaved_changes_description" = "Vous avez des modifications non-enregistrées."; -"screen_room_change_role_unsaved_changes_title" = "Enregistrer les modifications?"; "screen_room_details_add_topic_title" = "Ajouter un sujet"; "screen_room_details_already_a_member" = "Déjà membre"; "screen_room_details_already_invited" = "Déjà invité(e)"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer."; "screen_room_details_notification_mode_custom" = "Personnalisé"; "screen_room_details_notification_mode_default" = "Défaut"; -"screen_room_details_notification_title" = "Notifications"; "screen_room_details_share_room_title" = "Partager le salon"; "screen_room_details_title" = "Informations du salon"; "screen_room_details_updating_room" = "Mise à jour du salon…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Débloquer"; "screen_room_member_details_unblock_alert_description" = "Vous pourrez à nouveau voir tous ses messages."; "screen_room_member_details_unblock_user" = "Débloquer l’utilisateur"; +"screen_room_member_details_verify_button_subtitle" = "Utilisez l'application Web pour vérifier cet utilisateur."; +"screen_room_member_details_verify_button_title" = "Vérifier %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Bannir"; "screen_room_member_list_ban_member_confirmation_description" = "L‘utilisateur ne pourra pas rejoindre le salon à nouveau, même si il est invité."; "screen_room_member_list_ban_member_confirmation_title" = "Êtes-vous certain de vouloir bannir ce membre?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Bannissement de %1$@"; "screen_room_member_list_manage_member_ban" = "Retirer et bannir ce membre"; "screen_room_member_list_manage_member_remove" = "Retirer le membre du salon"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Retirer et bannir le membre"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Retirer le membre uniquement"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Retirer le membre et interdire l'adhésion à l'avenir ?"; "screen_room_member_list_manage_member_unban_action" = "Débannir"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Afficher moins"; "screen_room_timeline_message_copied" = "Message copié"; "screen_room_timeline_no_permission_to_post" = "Vous n’êtes pas autorisé à publier dans ce salon"; -"screen_room_timeline_reactions_show_less" = "Afficher moins"; "screen_room_timeline_reactions_show_more" = "Afficher plus"; "screen_room_timeline_read_marker_title" = "Nouveau"; "screen_room_title" = "Discussion"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Marquer comme lu"; "screen_roomlist_mark_as_unread" = "Marquer comme non lu"; "screen_roomlist_room_directory_button_title" = "Parcourir tous les salons"; -"screen_server_confirmation_change_server" = "Changer de fournisseur de compte"; "screen_server_confirmation_message_login_element_dot_io" = "Un serveur privé pour les employés d’Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."; "screen_server_confirmation_message_register" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Comparez les nombres"; "screen_session_verification_complete_subtitle" = "Votre nouvelle session est désormais vérifiée. Elle a accès à vos messages chiffrés et les autres utilisateurs la verront identifiée comme fiable."; "screen_session_verification_enter_recovery_key" = "Utiliser la clé de récupération"; +"screen_session_verification_failed_subtitle" = "Soit la demande a expiré, soit elle a été refusée, soit il y a eu une non-concordance de vérification."; "screen_session_verification_open_existing_session_subtitle" = "Prouvez qu’il s’agit bien de vous pour accéder à l’historique de vos messages chiffrés."; "screen_session_verification_open_existing_session_title" = "Ouvrir une session existante"; "screen_session_verification_positive_button_canceled" = "Réessayer la vérification"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "En attente de correspondance"; "screen_session_verification_ready_subtitle" = "Comparer un groupe unique d’Emojis."; "screen_session_verification_request_accepted_subtitle" = "Comparez les emoji uniques en veillant à ce qu’ils apparaissent dans le même ordre."; +"screen_session_verification_request_details_timestamp" = "Connecté"; +"screen_session_verification_request_failure_title" = "Échec de la vérification"; +"screen_session_verification_request_footer" = "Continuez uniquement si c'est vous qui avez commencé cette vérification."; +"screen_session_verification_request_subtitle" = "Vérifiez l'autre appareil pour sécuriser l'historique de vos messages."; +"screen_session_verification_request_success_subtitle" = "Vous pouvez désormais lire ou envoyer des messages en toute sécurité sur votre autre appareil."; +"screen_session_verification_request_success_title" = "Appareil vérifié"; +"screen_session_verification_request_title" = "Vérification demandée"; "screen_session_verification_they_dont_match" = "Ils ne correspondent pas"; "screen_session_verification_they_match" = "Ils correspondent"; "screen_session_verification_waiting_to_accept_subtitle" = "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."; "screen_signout_key_backup_disabled_title" = "Vous avez désactivé la sauvegarde"; "screen_signout_key_backup_offline_subtitle" = "Vos clés étaient en cours de sauvegarde lorsque vous avez perdu la connexion au réseau. Il faudrait rétablir cette connexion afin de pouvoir terminer la sauvegarde avant de vous déconnecter."; -"screen_signout_key_backup_offline_title" = "Vos clés sont en cours de sauvegarde"; "screen_signout_key_backup_ongoing_subtitle" = "Veuillez attendre que cela se termine avant de vous déconnecter."; "screen_signout_key_backup_ongoing_title" = "Vos clés sont en cours de sauvegarde"; "screen_signout_recovery_disabled_subtitle" = "Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos messages."; "screen_signout_recovery_disabled_title" = "La récupération n’est pas configurée."; "screen_signout_save_recovery_key_subtitle" = "Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."; -"screen_signout_save_recovery_key_title" = "Avez-vous sauvegardé votre clé de récupération?"; "screen_start_chat_error_starting_chat" = "Une erreur s’est produite lors de la tentative de création de la discussion"; "screen_view_location_title" = "Position"; "screen_welcome_bullet_1" = "Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."; @@ -919,7 +952,6 @@ "test_language_identifier" = "fr"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Dépannage"; -"troubleshoot_notifications_entry_point_title" = "Résoudre les problèmes liés aux notifications"; "troubleshoot_notifications_screen_action" = "Exécuter les tests"; "troubleshoot_notifications_screen_action_again" = "Relancer les tests"; "troubleshoot_notifications_screen_failure" = "Certains tests ont échoué. Veuillez vérifier les détails."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Vérifier qu’au moins un distributeur UnifiedPush est disponible."; "troubleshoot_notifications_test_unified_push_failure" = "Aucun distributeur UnifiedPush n'a été trouvé."; "troubleshoot_notifications_test_unified_push_title" = "Vérifier UnifiedPush"; +"a11y_poll" = "Sondage"; +"banner_set_up_recovery_submit" = "Configurer la sauvegarde"; "dialog_title_error" = "Erreur"; "dialog_title_success" = "Succès"; "notification_fallback_content" = "Notification"; "notification_invitation_action_join" = "Rejoindre"; +"notification_invitation_action_reject" = "Rejeter"; "notification_room_action_mark_as_read" = "Marquer comme lu"; "notification_room_action_quick_reply" = "Réponse rapide"; +"screen_pinned_timeline_screen_title_empty" = "Messages épinglés"; "screen_room_mentions_at_room_title" = "Tout le monde"; +"screen_account_provider_change" = "Changer de fournisseur de compte"; "screen_account_provider_signin_subtitle" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; "screen_account_provider_signup_subtitle" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; "screen_analytics_settings_help_us_improve" = "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Vous pourrez à nouveau voir tous ses messages."; "screen_blocked_users_unblock_alert_title" = "Débloquer l’utilisateur"; "screen_bug_report_rash_logs_alert_title" = "%1$@ s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?"; +"screen_chat_backup_recovery_action_confirm" = "Utiliser la clé de récupération"; +"screen_chat_backup_recovery_action_setup" = "Configurer la sauvegarde"; +"screen_create_poll_cancel_confirmation_content_ios" = "Vos modifications ne seront pas enregistrées"; "screen_create_room_add_people_title" = "Inviter des amis"; "screen_create_room_room_name_label" = "Nom du salon"; "screen_create_room_title" = "Créer un salon"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Modifier le sondage"; "screen_identity_use_another_device" = "Utiliser une autre session"; "screen_login_subtitle" = "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."; +"screen_notification_settings_mentions_section_title" = "Mentions"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Essayer à nouveau"; +"screen_recovery_key_change_generate_key_description" = "Ne partagez cela avec personne !"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Bloquer l’utilisateur"; +"screen_reset_encryption_password_placeholder" = "Saisissez la clé ici…"; "screen_room_attachment_source_camera_photo" = "Prendre une photo"; "screen_room_change_permissions_everyone" = "Tout le monde"; "screen_room_change_permissions_member_moderation" = "Administration des membres"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administrateurs"; "screen_room_change_role_section_moderators" = "Modérateurs"; "screen_room_change_role_section_users" = "Membres"; +"screen_room_change_role_unsaved_changes_title" = "Enregistrer les changements?"; "screen_room_details_invite_people_title" = "Inviter des amis"; "screen_room_details_leave_conversation_title" = "Quitter la discussion"; "screen_room_details_leave_room_title" = "Quitter le salon"; +"screen_room_details_notification_title" = "Notifications"; "screen_room_details_roles_and_permissions" = "Rôles et autorisations"; "screen_room_details_room_name_label" = "Nom du salon"; "screen_room_details_security_title" = "Sécurité"; "screen_room_details_topic_title" = "Sujet"; "screen_room_error_failed_processing_media" = "Échec du traitement des médias à télécharger, veuillez réessayer."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Retirer et bannir ce membre"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions et mots clés uniquement"; +"screen_room_timeline_reactions_show_less" = "Afficher moins"; "screen_roomlist_filter_people" = "Personnes"; +"screen_server_confirmation_change_server" = "Changer de fournisseur de compte"; +"screen_session_verification_request_failure_subtitle" = "Soit la demande a expiré, soit elle a été refusée, soit il y a eu une non-concordance de vérification."; "screen_signout_confirmation_dialog_submit" = "Se déconnecter"; "screen_signout_confirmation_dialog_title" = "Se déconnecter"; +"screen_signout_key_backup_offline_title" = "Vos clés sont en cours de sauvegarde"; "screen_signout_preference_item" = "Se déconnecter"; +"screen_signout_save_recovery_key_title" = "Avez-vous sauvegardé votre clé de récupération?"; +"troubleshoot_notifications_entry_point_title" = "Dépanner les notifications"; diff --git a/ElementX/Resources/Localizations/hu.lproj/Localizable.strings b/ElementX/Resources/Localizations/hu.lproj/Localizable.strings index e1cef07ed6..829d85237a 100644 --- a/ElementX/Resources/Localizations/hu.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/hu.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Szüneteltetés"; "a11y_pin_field" = "PIN-mező"; "a11y_play" = "Lejátszás"; -"a11y_poll" = "Szavazás"; "a11y_poll_end" = "Befejezett szavazás"; "a11y_react_with" = "Reagálás a következővel: %1$@"; "a11y_react_with_other_emojis" = "Reagálás más emodzsikkal"; @@ -41,6 +40,7 @@ "action_create" = "Létrehozás"; "action_create_a_room" = "Szoba létrehozása"; "action_deactivate" = "Deaktiválás"; +"action_deactivate_account" = "Fiók deaktiválása"; "action_decline" = "Elutasítás"; "action_delete_poll" = "Szavazás törlése"; "action_disable" = "Letiltás"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Elfelejtette a jelszót?"; "action_forward" = "Tovább"; "action_go_back" = "Visszalépés"; +"action_ignore" = "Mellőzés"; "action_invite" = "Meghívás"; "action_invite_friends" = "Ismerősök meghívása"; "action_invite_friends_to_app" = "Ismerősök meghívása ide: %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Elhagyás"; "action_leave_conversation" = "Beszélgetés elhagyása"; "action_leave_room" = "Szoba elhagyása"; +"action_load_more" = "Továbbiak betöltése"; "action_manage_account" = "Fiók kezelése"; "action_manage_devices" = "Eszközök kezelése"; "action_message" = "Üzenet"; @@ -93,6 +95,7 @@ "action_send_message" = "Üzenet küldése"; "action_share" = "Megosztás"; "action_share_link" = "Hivatkozás megosztása"; +"action_show" = "Megjelenítés"; "action_sign_in_again" = "Jelentkezzen be újra"; "action_signout" = "Kijelentkezés"; "action_signout_anyway" = "Kijelentkezés mindenképp"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Megtekintés az idővonalon"; "action_view_source" = "Forrás megtekintése"; "action_yes" = "Igen"; -"action.load_more" = "Továbbiak betöltése"; -"action_deactivate_account" = "Fiók deaktiválása"; "banner_migrate_to_native_sliding_sync_action" = "Kijelentkezés és frissítés"; "banner_migrate_to_native_sliding_sync_description" = "A kiszolgálója mostantól egy új, gyorsabb protokollt támogat. A frissítéshez jelentkezzen ki, majd jelentkezzen be újra. Ha ezt most megteszi, elkerülheti a kényszerített kijelentkeztetést a régi protokollt eltávolításakor."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "A Matrix-kiszolgáló már nem támogatja a régi protokollt. Az alkalmazás további használatához jelentkezzen ki és be."; "banner_migrate_to_native_sliding_sync_title" = "Frissítés érhető el"; -"banner.set_up_recovery.content" = "Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."; -"banner.set_up_recovery.title" = "Helyreállítás beállítása"; +"banner_set_up_recovery_content" = "Hozzon létre egy új helyreállítási kulcsot, amellyel visszaállíthatja a titkosított üzenetek előzményeit, ha elveszíti az eszközökhöz való hozzáférést."; +"banner_set_up_recovery_title" = "Helyreállítás beállítása"; "common_about" = "Névjegy"; "common_acceptable_use_policy" = "Elfogadható használatra vonatkozó szabályzat"; "common_advanced_settings" = "Speciális beállítások"; @@ -133,10 +134,12 @@ "common_dark" = "Sötét"; "common_decryption_error" = "Visszafejtési hiba"; "common_developer_options" = "Fejlesztői beállítások"; +"common_device_id" = "Eszközazonosító"; "common_direct_chat" = "Közvetlen csevegés"; "common_edited_suffix" = "(szerkesztve)"; "common_editing" = "Szerkesztés"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Titkosítás"; "common_encryption_enabled" = "Titkosítás engedélyezve"; "common_enter_your_pin" = "Adja meg a PIN-kódját"; "common_error" = "Hiba"; @@ -147,6 +150,7 @@ "common_favourited" = "Kedvencnek jelölve"; "common_file" = "Fájl"; "common_forward_message" = "Üzenet továbbítása"; +"common_frequently_used" = "Gyakran használt"; "common_gif" = "GIF"; "common_image" = "Kép"; "common_in_reply_to" = "Válasz erre: %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Némítás"; "common_no_results" = "Nincs találat"; +"common_no_room_name" = "Nincs szobanév"; "common_offline" = "Kapcsolat nélkül"; "common_optic_id_ios" = "Optic ID"; "common_or" = "vagy"; @@ -170,6 +175,8 @@ "common_permalink" = "Állandó hivatkozás"; "common_permission" = "Engedély"; "common_please_wait" = "Kis türelmet…"; +"common_poll_end_confirmation" = "Biztos, hogy befejezi ezt a szavazást?"; +"common_poll_summary" = "Szavazás: %1$@"; "common_poll_total_votes" = "Összes szavazat: %1$@"; "common_poll_undisclosed_text" = "Az eredmények a szavazás befejezése után jelennek meg"; "common_privacy_policy" = "Adatvédelmi nyilatkozat"; @@ -200,6 +207,7 @@ "common_settings" = "Beállítások"; "common_shared_location" = "Megosztott tartózkodási hely"; "common_signing_out" = "Kijelentkezés"; +"common_something_went_wrong" = "Valamilyen hiba történt"; "common_starting_chat" = "Csevegés megkezdése…"; "common_sticker" = "Matrica"; "common_success" = "Sikeres"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Miről szól ez a szoba?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Nem lehet visszafejteni"; +"common_unable_to_decrypt_no_access" = "Nincs hozzáférése ehhez az üzenethez"; "common_unable_to_invite_message" = "Nem sikerült meghívót küldeni egy vagy több felhasználónak."; "common_unable_to_invite_title" = "Nem sikerült elküldeni a meghívót (meghívókat)"; "common_unlock" = "Feloldás"; @@ -221,23 +230,30 @@ "common_username" = "Felhasználónév"; "common_verification_cancelled" = "Az ellenőrzés megszakítva"; "common_verification_complete" = "Az ellenőrzés befejeződött"; +"common_verification_failed" = "Az ellenőrzés sikertelen"; +"common_verified" = "Ellenőrizve"; +"common_verify_device" = "Eszköz ellenőrzése"; +"common_verify_identity" = "Személyazonosság ellenőrzése"; "common_video" = "Videó"; "common_voice_message" = "Hangüzenet"; "common_waiting" = "Várakozás…"; "common_waiting_for_decryption_key" = "Várakozás a visszafejtési kulcsra"; +"common.copied_to_clipboard" = "A vágólapra másolva"; "common.do_not_show_this_again" = "Ne jelenjen meg többé"; "common.open_source_licenses" = "Nyílt forráskódú licencek"; "common.pinned" = "Kitűzve"; "common.send_to" = "Címzett"; -"common_no_room_name" = "Nincs szobanév"; -"common_poll_end_confirmation" = "Biztos, hogy befejezi ezt a szavazást?"; -"common_poll_summary" = "Szavazás: %1$@"; -"common_something_went_wrong" = "Valamilyen hiba történt"; -"common_unable_to_decrypt_no_access" = "Nincs hozzáférése ehhez az üzenethez"; -"common_verify_device" = "Eszköz ellenőrzése"; -"confirm_recovery_key_banner_message" = "A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítenie a helyreállítási kulcsát, hogy továbbra is hozzáférjen a csevegés biztonsági mentéséhez."; -"confirm_recovery_key_banner_title" = "Helyreállítási kulcs megerősítése"; +"common.you" = "Ön"; +"common_unable_to_decrypt_insecure_device" = "Nem biztonságos eszközről küldve"; +"common_unable_to_decrypt_verification_violation" = "A feladó ellenőrzött személyazonossága megváltozott"; +"confirm_recovery_key_banner_message" = "Erősítse meg a helyreállítási kulcsát, hogy továbbra is hozzáférjen a kulcstárolójához és az üzenetelőzményekhez."; +"confirm_recovery_key_banner_primary_button_title" = "Adja meg a helyreállítási kulcsot"; +"confirm_recovery_key_banner_secondary_button_title" = "Elfelejtette a helyreállítási kulcsot?"; +"confirm_recovery_key_banner_title" = "A kulcstároló nincs szinkronizálva"; "crash_detection_dialog_content" = "Az %1$@ összeomlott a legutóbbi használata óta. Megosztja velünk az összeomlás-jelentést?"; +"crypto_identity_change_pin_violation" = "Úgy tűnik, hogy %1$@ személyazonossága megváltozott. %2$@"; +"crypto_identity_change_pin_violation_new" = "Úgy tűnik, hogy %1$@ %2$@ személyazonossága megváltozott. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Hogy az alkalmazás használhassa a kamerát, adja meg az engedélyt a rendszerbeállításokban."; "dialog_permission_generic" = "Adja meg az engedélyt a rendszerbeállításokban."; "dialog_permission_location_description_ios" = "Adjon hozzáférést a Beállítások -> Hely menüpontban."; @@ -274,8 +290,8 @@ "event_shield_reason_unknown_device" = "Ismeretlen vagy törölt eszköz által titkosítva."; "event_shield_reason_unsigned_device" = "A tulajdonos által nem ellenőrzött eszköz által titkosítva."; "event_shield_reason_unverified_identity" = "Nem ellenőrzött felhasználó által titkosítva."; -"full_screen_intent_banner_message" = "Annak érdekében, hogy soha ne maradjon le egyetlen fontos hívásról sem, módosítsa a beállításokat, hogy engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van."; -"full_screen_intent_banner_title" = "Növelje a hívásélményét"; +"full_screen_intent_banner_message" = "Hogy sose maradjon le egyetlen fontos hívásról sem, a beállításokban engedélyezze a teljes képernyős értesítéseket, amikor a telefon zárolva van."; +"full_screen_intent_banner_title" = "Fokozza a hívásélményét"; "invite_friends_rich_title" = "🔐️ Csatlakozz hozzám itt: %1$@"; "invite_friends_text" = "Beszélgessünk itt: %1$@, %2$@"; "leave_conversation_alert_subtitle" = "Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Csendes értesítések"; "notification_incoming_call" = "Bejövő hívás"; "notification_inline_reply_failed" = "** Nem sikerült elküldeni – nyissa meg a szobát"; -"notification_invitation_action_reject" = "Elutasítás"; "notification_invite_body" = "Meghívta, hogy csevegjen"; "notification_invite_body_with_sender" = "%1$@ meghívta egy csevegésre"; "notification_mentioned_you_body" = "Megemlítette Önt: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Behúzás nélkül"; "rich_text_editor_url_placeholder" = "Hivatkozás"; "rich_text_editor_a11y_add_attachment" = "Melléklet hozzáadása"; +"rich_text_editor_composer_caption_placeholder" = "Nem kötelező felirat…"; "screen_advanced_settings_element_call_base_url" = "Egyéni Element Call alapwebcím"; "screen_advanced_settings_element_call_base_url_description" = "Egyéni alapwebcím beállítása az Element Callhoz."; "screen_advanced_settings_element_call_base_url_validation_error" = "Érvénytelen webcím, győződjön meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím."; +"screen_create_room_room_address_section_footer" = "Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."; +"screen_create_room_room_address_section_title" = "Szoba címe"; +"screen_create_room_room_visibility_section_title" = "Szoba láthatósága"; +"screen_create_room_access_section_anyone_option_description" = "Bárki csatlakozhat ehhez a szobához"; +"screen_create_room_access_section_anyone_option_title" = "Bárki"; +"screen_create_room_access_section_header" = "Szobahozzáférés"; +"screen_create_room_access_section_knocking_option_description" = "Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"; +"screen_create_room_access_section_knocking_option_title" = "Csatlakozás kérése"; +"screen_join_room_cancel_knock_action" = "Kérés visszavonása"; +"screen_join_room_cancel_knock_alert_confirmation" = "Igen, visszavonás"; +"screen_join_room_cancel_knock_alert_description" = "Biztos, hogy visszavonja a szobához való csatlakozási kérését?"; +"screen_join_room_cancel_knock_alert_title" = "Csatlakozási kérés visszavonása"; +"screen_join_room_knock_message_description" = "Üzenet (nem kötelező)"; +"screen_join_room_knock_sent_description" = "Ha a kérését elfogadják, meghívót kap a szobához való csatlakozáshoz."; +"screen_join_room_knock_sent_title" = "Csatlakozási kérés elküldve"; "screen_pinned_timeline_empty_state_description" = "Nyomjon hosszan az üzenetre, és válassza a „%1$@” lehetőséget, hogy itt szerepeljen."; "screen_pinned_timeline_empty_state_headline" = "Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek"; -"screen_pinned_timeline_screen_title_empty" = "Kitűzött üzenetek"; "screen_reset_encryption_password_error" = "Ismeretlen hiba történt. Ellenőrizze, hogy a fiókja jelszava helyes-e, és próbálja meg újra."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Ellenőrzés visszavonása és elküldés"; "screen_resolve_send_failure_changed_identity_subtitle" = "Visszavonhatja az ellenőrzést, és ennek ellenére elküldheti ezt az üzenetet, vagy egyelőre törölheti, és %1$@ újbóli ellenőrzése után újra megpróbálhatja."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Üzenet betöltése…"; "screen_room_pinned_banner_view_all_button_title" = "Összes megtekintése"; "screen_room_details_pinned_events_row_title" = "Kitűzött üzenetek"; +"screen_roomlist_knock_event_sent_description" = "Csatlakozási kérés elküldve"; "screen_timeline_item_menu_send_failure_changed_identity" = "Az üzenet nem lett elküldve, mert %1$@ ellenőrzött személyazonossága megváltozott."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Az üzenet nem lett elküldve, mert %1$@ nem ellenőrizte az összes eszközét."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte."; -"screen_account_provider_change" = "Fiókszolgáltató módosítása"; "screen_account_provider_form_hint" = "Matrix-kiszolgáló webcíme"; "screen_account_provider_form_notice" = "Adjon meg egy keresési kifejezést vagy egy tartománycímet."; "screen_account_provider_form_subtitle" = "Keresés egy cégre, közösségre vagy privát kiszolgálóra."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Hamarosan létrehoz egy fiókot itt: %@"; "screen_advanced_settings_developer_mode" = "Fejlesztői mód"; "screen_advanced_settings_developer_mode_description" = "Engedélyezze, hogy elérje a fejlesztőknek szánt funkciókat."; +"screen_advanced_settings_media_compression_description" = "Töltse fel gyorsabban a fényképeket és videókat, valamint csökkentse az adatforgalmat"; +"screen_advanced_settings_media_compression_title" = "Média minőségének optimalizálása"; "screen_advanced_settings_rich_text_editor_description" = "A formázott szöveges szerkesztő letiltása, hogy kézzel írhasson Markdownt."; "screen_advanced_settings_send_read_receipts" = "Olvasási visszaigazolások"; "screen_advanced_settings_send_read_receipts_description" = "Ha ki van kapcsolva, az olvasási visszaigazolások nem lesznek elküldve senkinek. A többi felhasználó olvasási visszaigazolását továbbra is meg fogja kapni."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Válassza ki a kiszolgálóját"; "screen_chat_backup_key_backup_action_disable" = "Biztonsági mentés kikapcsolása"; "screen_chat_backup_key_backup_action_enable" = "Biztonsági mentés bekapcsolása"; -"screen_chat_backup_key_backup_description" = "A biztonsági mentés biztosítja, hogy ne veszítse el az üzenetelőzményeit. %1$@."; -"screen_chat_backup_key_backup_title" = "Biztonsági mentés"; +"screen_chat_backup_key_backup_description" = "Tárolja kriptográfiai személyazonosságát és üzenetkulcsait biztonságosan a kiszolgálón. Ez lehetővé teszi, hogy bármilyen új eszközön megtekinthesse üzenetelőzményeit. %1$@."; +"screen_chat_backup_key_backup_title" = "Kulcstároló"; +"screen_chat_backup_key_storage_disabled_error" = "A helyreállítás beállításához be kell kapcsolni a kulcstárolást."; +"screen_chat_backup_key_storage_toggle_description" = "Kulcsok feltöltése erről az eszközről"; +"screen_chat_backup_key_storage_toggle_title" = "Kulcstárolás engedélyezése"; "screen_chat_backup_recovery_action_change" = "Helyreállítási kulcs módosítása"; -"screen_chat_backup_recovery_action_confirm" = "Helyreállítási kulcs megerősítése"; -"screen_chat_backup_recovery_action_confirm_description" = "A csevegéselőzményei nincsenek szinkronban."; -"screen_chat_backup_recovery_action_setup" = "Helyreállítás beállítása"; +"screen_chat_backup_recovery_action_change_description" = "Ha az összes meglévő eszközét elvesztette, akkor egy helyreállítási kulccsal visszaszerezheti a kriptográfiai személyazonosságát és az üzenetelőzményeit."; +"screen_chat_backup_recovery_action_confirm_description" = "A kulcstároló jelenleg nincs szinkronizálva."; "screen_chat_backup_recovery_action_setup_description" = "Szerezzen hozzáférést a titkosított üzeneteihez, ha elvesztette az összes eszközét, vagy ha mindenütt kijelentkezett az %1$@ből."; "screen_create_account_title" = "Fiók létrehozása"; "screen_create_new_recovery_key_list_item_1" = "Nyissa meg az %1$@et egy asztali eszközön"; @@ -447,17 +481,16 @@ "screen_create_poll_anonymous_desc" = "Eredmények megjelenítése csak a szavazás befejezése után"; "screen_create_poll_anonymous_headline" = "Szavazatok elrejtése"; "screen_create_poll_answer_hint" = "%1$d. lehetőség"; -"screen_create_poll_cancel_confirmation_content_ios" = "A változtatások nem lesznek mentve"; "screen_create_poll_cancel_confirmation_title_ios" = "Szavazás elvetése"; "screen_create_poll_question_desc" = "Kérdés vagy téma"; "screen_create_poll_question_hint" = "Miről szól ez a szavazás?"; "screen_create_poll_title" = "Szavazás létrehozása"; "screen_create_room_action_create_room" = "Új szoba"; "screen_create_room_error_creating_room" = "Hiba történt a szoba létrehozásakor"; -"screen_create_room_private_option_description" = "A szobában lévő üzenetek titkosítottak. A titkosítást utólag nem lehet kikapcsolni."; -"screen_create_room_private_option_title" = "Privát szoba (csak meghívással)"; -"screen_create_room_public_option_description" = "Az üzenetek nincsenek titkosítva, és bárki elolvashatja őket. A titkosítást később is engedélyezheti."; -"screen_create_room_public_option_title" = "Nyilvános szoba (bárki)"; +"screen_create_room_private_option_description" = "Csak a meghívottak léphetnek be ebbe a szobába. Az összes üzenet végpontok közti titkosítással van védve."; +"screen_create_room_private_option_title" = "Privát szoba"; +"screen_create_room_public_option_description" = "Bárki megtalálhatja ezt a szobát.\nEzt bármikor módosíthatja a szobabeállításokban."; +"screen_create_room_public_option_title" = "Nyilvános szoba"; "screen_create_room_topic_label" = "Téma (nem kötelező)"; "screen_deactivate_account_confirmation_dialog_content" = "Erősítse meg, hogy deaktiválja a fiókját. Ez a művelet nem vonható vissza."; "screen_deactivate_account_delete_all_messages" = "Összes saját üzenet törlése"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Csoportos csevegések"; "screen_notification_settings_invite_for_me_label" = "Meghívók"; "screen_notification_settings_mentions_only_disclaimer" = "A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."; -"screen_notification_settings_mentions_section_title" = "Említések"; "screen_notification_settings_mode_all" = "Összes"; "screen_notification_settings_mode_mentions" = "Említések"; "screen_notification_settings_notification_section_title" = "Értesítés ezekről:"; @@ -573,15 +605,15 @@ "screen_qr_code_login_connection_note_secure_state_title" = "A kapcsolat nem biztonságos"; "screen_qr_code_login_device_code_subtitle" = "A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."; "screen_qr_code_login_device_code_title" = "Adja meg az alábbi számot a másik eszközén"; -"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Jelentkezzen be a másik eszközére, majd próbálja újra, vagy használjon egy másik eszközt, amelyre már bejelentkezett."; +"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Jelentkezzen be másik eszközére, majd próbálkozzon újra, vagy használjon egy másik, már bejelentkezett eszközt."; "screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Más eszköz nincs bejelentkezve"; -"screen_qr_code_login_error_cancelled_subtitle" = "A bejelentkezés megszakadt a másik eszközön."; +"screen_qr_code_login_error_cancelled_subtitle" = "A bejelentkezést megszakították a másik eszközön."; "screen_qr_code_login_error_cancelled_title" = "Bejelentkezési kérés törölve"; -"screen_qr_code_login_error_declined_subtitle" = "A bejelentkezés el lett utasítva a másik eszközön."; +"screen_qr_code_login_error_declined_subtitle" = "A bejelentkezést elutasították a másik eszközön."; "screen_qr_code_login_error_declined_title" = "A bejelentkezés elutasítva"; "screen_qr_code_login_error_expired_subtitle" = "A bejelentkezés lejárt. Próbálja újra."; "screen_qr_code_login_error_expired_title" = "A bejelentkezés nem fejeződött be időben"; -"screen_qr_code_login_error_linking_not_suported_subtitle" = "A másik eszköz nem támogatja a %@ QR-kóddal történő bejelentkezést.\n\nPróbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik eszközzel."; +"screen_qr_code_login_error_linking_not_suported_subtitle" = "A másik eszköz nem támogatja QR-kóddal történő bejelentkezést az %@be.\n\nPróbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik eszközzel."; "screen_qr_code_login_error_linking_not_suported_title" = "A QR-kód nem támogatott"; "screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "A fiókszolgáltatója nem támogatja az %1$@-et."; "screen_qr_code_login_error_sliding_sync_not_supported_title" = "Az %1$@ nem támogatott"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Válassza ezt: %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "„Új eszköz összekapcsolása”"; "screen_qr_code_login_initial_state_item_4" = "Olvassa be a QR-kódot ezzel az eszközzel"; +"screen_qr_code_login_initial_state_subtitle" = "Csak akkor érhető el, ha a fiókszolgáltató támogatja."; "screen_qr_code_login_initial_state_title" = "Nyissa meg az %1$@et egy másik eszközön a QR-kód lekéréséhez."; "screen_qr_code_login_invalid_scan_state_description" = "Használja a másik eszközön látható QR-kódot."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Hibás QR-kód"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "Az Ön ellenőrzőkódja"; "screen_recovery_key_change_description" = "Szerezzen új helyreállítási kulcsot, ha elvesztette a meglévőt. A helyreállítása kulcsa módosítása után a régi már nem fog működni."; "screen_recovery_key_change_generate_key" = "Új helyreállítási kulcs előállítása"; -"screen_recovery_key_change_generate_key_description" = "Gondoskodjon arról, hogy biztonságos helyen tárolja a helyreállítási kulcsát"; "screen_recovery_key_change_success" = "Helyreállítási kulcs lecserélve"; "screen_recovery_key_change_title" = "Módosítja a helyreállítási kulcsot?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Új helyreállítási kulcs létrehozása"; "screen_recovery_key_confirm_description" = "Győződjön meg arról, hogy senki sem látja ezt a képernyőt!"; -"screen_recovery_key_confirm_error_content" = "Próbálja meg újra megerősíteni a csevegés biztonsági mentéséhez való hozzáférését."; +"screen_recovery_key_confirm_error_content" = "Próbálja újra megerősíteni a kulcstárolóhoz való hozzáférést."; "screen_recovery_key_confirm_error_title" = "Helytelen helyreállítási kulcs"; "screen_recovery_key_confirm_key_description" = "Ha van biztonsági kulcsa vagy biztonsági jelmondata, akkor ez is fog működni."; "screen_recovery_key_confirm_key_placeholder" = "Megadás…"; "screen_recovery_key_confirm_lost_recovery_key" = "Elvesztette a helyreállítási kulcsát?"; "screen_recovery_key_confirm_success" = "Helyreállítási kulcs megerősítve"; -"screen_recovery_key_confirm_title" = "Adja meg a helyreállítási kulcsát"; "screen_recovery_key_copied_to_clipboard" = "Helyreállítási kulcs másolva"; "screen_recovery_key_generating_key" = "Előállítás…"; "screen_recovery_key_save_action" = "Helyreállítási kulcs mentése"; -"screen_recovery_key_save_description" = "Írja le a helyreállítási kulcsát valami biztonságos helyre, vagy mentse egy jelszókezelőbe."; +"screen_recovery_key_save_description" = "Írja le a helyreállítási kulcsát valami biztonságos helyre, például mentse egy jelszókezelőbe, egy titkosított jegyzetbe vagy egy fizikai széfbe."; "screen_recovery_key_save_key_description" = "Koppintson a helyreállítási kulcs másolásához"; "screen_recovery_key_save_title" = "Mentse el a helyreállítási kulcsát"; "screen_recovery_key_setup_confirmation_description" = "Ezután a lépés után nem fog tudni hozzáférni az új helyreállítási kulcsához."; "screen_recovery_key_setup_confirmation_title" = "Mentette a helyreállítási kulcsát?"; "screen_recovery_key_setup_description" = "A csevegései biztonsági mentését a helyreállítási kulcsa védi. Ha új helyreállítási kulcsra van szüksége a beállítás után, akkor a „Helyreállítási kulcs módosítása” választásával újból létrehozhat egyet."; "screen_recovery_key_setup_generate_key" = "Helyreállítási kulcs előállítása"; -"screen_recovery_key_setup_generate_key_description" = "Gondoskodjon arról, hogy biztonságos helyen tárolja a helyreállítási kulcsát"; +"screen_recovery_key_setup_generate_key_description" = "Ezt ne ossza meg senkivel!"; "screen_recovery_key_setup_success" = "A helyreállítás beállítása sikeres"; "screen_recovery_key_setup_title" = "Helyreállítás beállítása"; "screen_report_content_block_user_hint" = "Jelölje be, ha el akarja rejteni az összes jelenlegi és jövőbeli üzenetet ettől a felhasználótól"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Igen, visszaállítás most"; "screen_reset_encryption_confirmation_alert_subtitle" = "Ez a folyamat visszafordíthatatlan."; "screen_reset_encryption_confirmation_alert_title" = "Biztos, hogy visszaállítja a titkosítást?"; -"screen_reset_encryption_password_placeholder" = "Adja meg…"; "screen_reset_encryption_password_subtitle" = "Erősítse meg, hogy vissza szeretné állítani a titkosítást."; "screen_reset_encryption_password_title" = "A folytatáshoz adja meg fiókja jelszavát"; "screen_reset_identity_confirmation_subtitle" = "Arra készül, hogy belépjen a(z) %1$@ fiókjába, hogy visszaállítsa a személyazonosságát. Ezután vissza fog térni az alkalmazásba."; @@ -669,12 +699,11 @@ "screen_room_change_role_moderators_admin_section_footer" = "Az adminisztrátorok automatikusan moderátori jogosultságokkal rendelkeznek"; "screen_room_change_role_moderators_title" = "Moderátorok szerkesztése"; "screen_room_change_role_unsaved_changes_description" = "Mentetlen módosításai vannak."; -"screen_room_change_role_unsaved_changes_title" = "Menti a változtatásokat?"; "screen_room_details_add_topic_title" = "Téma hozzáadása"; "screen_room_details_already_a_member" = "Már tag"; "screen_room_details_already_invited" = "Már meghívták"; -"screen_room_details_badge_encrypted" = "Titkosítva"; -"screen_room_details_badge_not_encrypted" = "Nincs titkosítva"; +"screen_room_details_badge_encrypted" = "Titkosított"; +"screen_room_details_badge_not_encrypted" = "Nem titkosított"; "screen_room_details_badge_public" = "Nyilvános szoba"; "screen_room_details_edit_room_title" = "Szoba szerkesztése"; "screen_room_details_edition_error" = "Ismeretlen hiba történt, és az információkat nem lehetett megváltoztatni."; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Nem sikerült feloldani a szoba némítását, próbálja újra."; "screen_room_details_notification_mode_custom" = "Egyéni"; "screen_room_details_notification_mode_default" = "Alapértelmezett"; -"screen_room_details_notification_title" = "Értesítések"; "screen_room_details_share_room_title" = "Szoba megosztása"; "screen_room_details_title" = "Szobainformációk"; "screen_room_details_updating_room" = "Szoba frissítése…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Letiltás feloldása"; "screen_room_member_details_unblock_alert_description" = "Újra láthatja az összes üzenetét."; "screen_room_member_details_unblock_user" = "Felhasználó kitiltásának feloldása"; +"screen_room_member_details_verify_button_subtitle" = "Használja a webes alkalmazást a felhasználó ellenőrzéséhez."; +"screen_room_member_details_verify_button_title" = "A(z) %1$@ ellenőrzése"; "screen_room_member_list_ban_member_confirmation_action" = "Kitiltás"; "screen_room_member_list_ban_member_confirmation_description" = "Többé nem csatlakozhat ehhez a szobához, akkor sem, ha meghívják."; "screen_room_member_list_ban_member_confirmation_title" = "Biztos, hogy kitiltja ezt a tagot?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "%1$@ kitiltása"; "screen_room_member_list_manage_member_ban" = "Eltávolítás és a tag kitiltása"; "screen_room_member_list_manage_member_remove" = "Eltávolítás a szobából"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Tag eltávolítása és kitiltása"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Csak a tag eltávolítása"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Eltávolítja a tagot, és megtiltja a jövőbeni csatlakozást?"; "screen_room_member_list_manage_member_unban_action" = "Tiltás feloldása"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Kevesebb megjelenítése"; "screen_room_timeline_message_copied" = "Üzenet másolva"; "screen_room_timeline_no_permission_to_post" = "Nincs jogosultsága arra, hogy bejegyzést tegyen közzé ebben a szobában"; -"screen_room_timeline_reactions_show_less" = "Kevesebb megjelenítése"; "screen_room_timeline_reactions_show_more" = "Több megjelenítése"; "screen_room_timeline_read_marker_title" = "Új"; "screen_room_title" = "Csevegés"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Megjelölés olvasottként"; "screen_roomlist_mark_as_unread" = "Megjelölés olvasatlanként"; "screen_roomlist_room_directory_button_title" = "Összes szoba böngészése"; -"screen_server_confirmation_change_server" = "Fiókszolgáltató módosítása"; "screen_server_confirmation_message_login_element_dot_io" = "Egy privát kiszolgáló az Element alkalmazottai számára."; "screen_server_confirmation_message_login_matrix_dot_org" = "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."; "screen_server_confirmation_message_register" = "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Számok összehasonlítása"; "screen_session_verification_complete_subtitle" = "Az új munkamenete most már ellenőrizve van. Eléri a titkosított üzeneteit, és a többi felhasználó is megbízhatónak fogja látni."; "screen_session_verification_enter_recovery_key" = "Adja meg a helyreállítási kulcsot"; +"screen_session_verification_failed_subtitle" = "A kérés túllépte az időkorlátot, el lett utasítva, vagy ellenőrzési eltérés történt."; "screen_session_verification_open_existing_session_subtitle" = "Bizonyítsa, hogy valóban Ön az, hogy elérje a titkosított üzeneteinek előzményeit."; "screen_session_verification_open_existing_session_title" = "Meglévő munkamenet megnyitása"; "screen_session_verification_positive_button_canceled" = "Ellenőrzés újrapróbálása"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Várakozás az egyezésre"; "screen_session_verification_ready_subtitle" = "Egyedi emodzsik összehasonlítása."; "screen_session_verification_request_accepted_subtitle" = "Hasonlítsa össze az egyedi emodzsikat, meggyőződve arról, hogy azonos a sorrendjük."; +"screen_session_verification_request_details_timestamp" = "Bejelentkezve"; +"screen_session_verification_request_failure_title" = "Az ellenőrzés sikertelen"; +"screen_session_verification_request_footer" = "Csak akkor folytassa, ha Ön kezdeményezte ezt az ellenőrzést."; +"screen_session_verification_request_subtitle" = "Az üzenetelőzmények biztonságának megőrzése érdekében ellenőrizze a másik eszközt."; +"screen_session_verification_request_success_subtitle" = "Mostantól biztonságosan olvashat vagy küldhet üzeneteket a másik eszközén."; +"screen_session_verification_request_success_title" = "Eszköz ellenőrizve"; +"screen_session_verification_request_title" = "Ellenőrzés kérve"; "screen_session_verification_they_dont_match" = "Nem egyeznek"; "screen_session_verification_they_match" = "Megegyeznek"; "screen_session_verification_waiting_to_accept_subtitle" = "A folytatáshoz fogadja el az ellenőrzési folyamat indítási kérését a másik munkamenetében."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."; "screen_signout_key_backup_disabled_title" = "Kikapcsolta a biztonsági mentést"; "screen_signout_key_backup_offline_subtitle" = "A kulcsai mentése során bontotta a kapcsolatot. Kapcsolódjon újra, hogy a kulcsai továbbra is mentésre kerüljenek mielőtt kijelentkezik."; -"screen_signout_key_backup_offline_title" = "A kulcsai mentése még folyamatban van"; "screen_signout_key_backup_ongoing_subtitle" = "Kijelentkezés előtt várja meg a befejezését."; "screen_signout_key_backup_ongoing_title" = "A kulcsai mentése még folyamatban van"; "screen_signout_recovery_disabled_subtitle" = "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."; "screen_signout_recovery_disabled_title" = "A helyreállítás nincs beállítva"; "screen_signout_save_recovery_key_subtitle" = "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez."; -"screen_signout_save_recovery_key_title" = "Mentette a helyreállítási kulcsát?"; "screen_start_chat_error_starting_chat" = "Hiba történt a csevegés indításakor"; "screen_view_location_title" = "Hely"; "screen_welcome_bullet_1" = "A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."; @@ -919,7 +952,6 @@ "test_language_identifier" = "hu"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Hibaelhárítás"; -"troubleshoot_notifications_entry_point_title" = "Értesítések hibaelhárítása"; "troubleshoot_notifications_screen_action" = "Tesztek futtatása"; "troubleshoot_notifications_screen_action_again" = "Tesztek újbóli futtatása"; "troubleshoot_notifications_screen_failure" = "Egyes tesztek sikertelenek voltak. Ellenőrizze a részleteket."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Győződjön meg arról, hogy a UnifiedPush forgalmazói elérhetők."; "troubleshoot_notifications_test_unified_push_failure" = "Nem található forgalmazó a leküldéses értesítésekhez."; "troubleshoot_notifications_test_unified_push_title" = "Ellenőrizze a UnifiedPush szolgáltatást"; +"a11y_poll" = "Szavazás"; +"banner_set_up_recovery_submit" = "Helyreállítás beállítása"; "dialog_title_error" = "Hiba"; "dialog_title_success" = "Sikeres"; "notification_fallback_content" = "Értesítés"; "notification_invitation_action_join" = "Csatlakozás"; +"notification_invitation_action_reject" = "Elutasítás"; "notification_room_action_mark_as_read" = "Megjelölés olvasottként"; "notification_room_action_quick_reply" = "Gyors válasz"; +"screen_pinned_timeline_screen_title_empty" = "Kitűzött üzenetek"; "screen_room_mentions_at_room_title" = "Mindenki"; +"screen_account_provider_change" = "Fiókszolgáltató módosítása"; "screen_account_provider_signin_subtitle" = "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."; "screen_account_provider_signup_subtitle" = "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez."; "screen_analytics_settings_help_us_improve" = "Anonim használati adatok megosztása a problémák azonosítása érdekében."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Újra láthatja az összes üzenetét."; "screen_blocked_users_unblock_alert_title" = "Felhasználó kitiltásának feloldása"; "screen_bug_report_rash_logs_alert_title" = "Az %1$@ összeomlott a legutóbbi használata óta. Megosztja velünk az összeomlás-jelentést?"; +"screen_chat_backup_recovery_action_confirm" = "Adja meg a helyreállítási kulcsot"; +"screen_chat_backup_recovery_action_setup" = "Helyreállítás beállítása"; +"screen_create_poll_cancel_confirmation_content_ios" = "A módosításai nem lesznek mentve"; "screen_create_room_add_people_title" = "Ismerősök meghívása"; "screen_create_room_room_name_label" = "Szoba neve"; "screen_create_room_title" = "Szoba létrehozása"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Szavazás szerkesztése"; "screen_identity_use_another_device" = "Másik eszköz használata"; "screen_login_subtitle" = "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz."; +"screen_notification_settings_mentions_section_title" = "Említések"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Próbálja újra"; +"screen_recovery_key_change_generate_key_description" = "Ezt ne ossza meg senkivel!"; +"screen_recovery_key_confirm_title" = "Adja meg a helyreállítási kulcsot"; "screen_report_content_block_user" = "Felhasználó letiltása"; +"screen_reset_encryption_password_placeholder" = "Megadás…"; "screen_room_attachment_source_camera_photo" = "Fénykép készítése"; "screen_room_change_permissions_everyone" = "Mindenki"; "screen_room_change_permissions_member_moderation" = "Tagok moderálása"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Adminisztrátorok"; "screen_room_change_role_section_moderators" = "Moderátorok"; "screen_room_change_role_section_users" = "Tagok"; +"screen_room_change_role_unsaved_changes_title" = "Menti a módosításokat?"; "screen_room_details_invite_people_title" = "Ismerősök meghívása"; "screen_room_details_leave_conversation_title" = "Beszélgetés elhagyása"; "screen_room_details_leave_room_title" = "Szoba elhagyása"; +"screen_room_details_notification_title" = "Értesítések"; "screen_room_details_roles_and_permissions" = "Szerepkörök és jogosultságok"; "screen_room_details_room_name_label" = "Szoba neve"; "screen_room_details_security_title" = "Biztonság"; "screen_room_details_topic_title" = "Téma"; "screen_room_error_failed_processing_media" = "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eltávolítás és a tag kitiltása"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Csak említések és kulcsszavak"; +"screen_room_timeline_reactions_show_less" = "Kevesebb megjelenítése"; "screen_roomlist_filter_people" = "Emberek"; +"screen_server_confirmation_change_server" = "Fiókszolgáltató módosítása"; +"screen_session_verification_request_failure_subtitle" = "A kérés túllépte az időkorlátot, el lett utasítva, vagy ellenőrzési eltérés történt."; "screen_signout_confirmation_dialog_submit" = "Kijelentkezés"; "screen_signout_confirmation_dialog_title" = "Kijelentkezés"; +"screen_signout_key_backup_offline_title" = "A kulcsai mentése még folyamatban van"; "screen_signout_preference_item" = "Kijelentkezés"; +"screen_signout_save_recovery_key_title" = "Mentette a helyreállítási kulcsát?"; +"troubleshoot_notifications_entry_point_title" = "Értesítések hibaelhárítása"; diff --git a/ElementX/Resources/Localizations/id.lproj/Localizable.strings b/ElementX/Resources/Localizations/id.lproj/Localizable.strings index 2712368036..b217ee3984 100644 --- a/ElementX/Resources/Localizations/id.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/id.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Jeda"; "a11y_pin_field" = "Kolom PIN"; "a11y_play" = "Putar"; -"a11y_poll" = "Pemungutan suara"; "a11y_poll_end" = "Pemungutan suara berakhir"; "a11y_react_with" = "Bereaksi dengan %1$@"; "a11y_react_with_other_emojis" = "Reaksi dengan emoji lain"; @@ -27,20 +26,21 @@ "action_back" = "Kembali"; "action_call" = "Panggil"; "action_cancel" = "Batal"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Batalkan untuk saat ini"; "action_choose_photo" = "Pilih foto"; "action_clear" = "Hapus"; "action_close" = "Tutup"; "action_complete_verification" = "Selesaikan verifikasi"; "action_confirm" = "Konfirmasi"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Konfirmasi kata sandi"; "action_continue" = "Lanjutkan"; "action_copy" = "Salin"; "action_copy_link" = "Salin tautan"; "action_copy_link_to_message" = "Salin tautan ke pesan"; "action_create" = "Buat"; "action_create_a_room" = "Buat ruangan"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Nonaktifkan"; +"action_deactivate_account" = "Nonaktifkan akun"; "action_decline" = "Tolak"; "action_delete_poll" = "Hapus pemungutan suara"; "action_disable" = "Nonaktifkan"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Lupa kata sandi?"; "action_forward" = "Teruskan"; "action_go_back" = "Kembali"; +"action_ignore" = "Abaikan"; "action_invite" = "Undang"; "action_invite_friends" = "Undang orang-orang"; "action_invite_friends_to_app" = "Undang orang-orang ke %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Tinggalkan"; "action_leave_conversation" = "Tinggalkan percakapan"; "action_leave_room" = "Tinggalkan ruangan"; +"action_load_more" = "Muat lainnya"; "action_manage_account" = "Kelola akun"; "action_manage_devices" = "Kelola perangkat"; "action_message" = "Kirim pesan"; @@ -73,7 +75,7 @@ "action_ok" = "Oke"; "action_open_settings" = "Pengaturan"; "action_open_with" = "Buka dengan"; -"action_pin" = "Pin"; +"action_pin" = "Sematkan"; "action_quick_reply" = "Balas cepat"; "action_quote" = "Kutip"; "action_react" = "Bereaksi"; @@ -84,7 +86,7 @@ "action_report_bug" = "Laporkan kutu"; "action_report_content" = "Laporkan Konten"; "action_reset" = "Atur ulang"; -"action_reset_identity" = "Reset identity"; +"action_reset_identity" = "Atur ulang identitas"; "action_retry" = "Coba lagi"; "action_retry_decryption" = "Coba dekripsi ulang"; "action_save" = "Simpan"; @@ -93,6 +95,7 @@ "action_send_message" = "Kirim pesan"; "action_share" = "Bagikan"; "action_share_link" = "Bagikan tautan"; +"action_show" = "Tampilkan"; "action_sign_in_again" = "Masuk lagi"; "action_signout" = "Keluar dari akun"; "action_signout_anyway" = "Keluar saja"; @@ -104,18 +107,16 @@ "action_take_photo" = "Ambil foto"; "action_tap_for_options" = "Ketuk untuk opsi"; "action_try_again" = "Coba lagi"; -"action_unpin" = "Unpin"; -"action_view_in_timeline" = "View in timeline"; +"action_unpin" = "Lepaskan sematan"; +"action_view_in_timeline" = "Lihat di lini masa"; "action_view_source" = "Tampilkan sumber"; "action_yes" = "Ya"; -"action.load_more" = "Muat lainnya"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "Keluar & Tingkatkan"; +"banner_migrate_to_native_sliding_sync_description" = "Server Anda kini mendukung protokol baru yang lebih cepat. Keluar dan masuk lagi untuk memperbarui sekarang. Melakukan hal ini sekarang akan membantu Anda menghindari keluar paksa saat protokol lama dihapus nantinya."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Homeserver Anda tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi."; +"banner_migrate_to_native_sliding_sync_title" = "Peningkatan tersedia"; +"banner_set_up_recovery_content" = "Buat kunci pemulihan baru yang dapat digunakan untuk memulihkan riwayat pesan terenkripsi Anda jika Anda kehilangan akses ke perangkat Anda."; +"banner_set_up_recovery_title" = "Siapkan pemulihan"; "common_about" = "Tentang"; "common_acceptable_use_policy" = "Kebijakan penggunaan wajar"; "common_advanced_settings" = "Pengaturan tingkat lanjut"; @@ -133,10 +134,12 @@ "common_dark" = "Gelap"; "common_decryption_error" = "Kesalahan dekripsi"; "common_developer_options" = "Opsi pengembang"; +"common_device_id" = "ID Perangkat"; "common_direct_chat" = "Obrolan langsung"; "common_edited_suffix" = "(disunting)"; "common_editing" = "Penyuntingan"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Enkripsi"; "common_encryption_enabled" = "Enkripsi diaktifkan"; "common_enter_your_pin" = "Masukkan PIN Anda"; "common_error" = "Eror"; @@ -147,6 +150,7 @@ "common_favourited" = "Difavoritkan"; "common_file" = "Berkas"; "common_forward_message" = "Teruskan pesan"; +"common_frequently_used" = "Sering digunakan"; "common_gif" = "GIF"; "common_image" = "Gambar"; "common_in_reply_to" = "Membalas kepada %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Bisukan"; "common_no_results" = "Tidak ada hasil"; +"common_no_room_name" = "Tidak ada nama ruangan"; "common_offline" = "Luring"; "common_optic_id_ios" = "Optic ID"; "common_or" = "atau"; @@ -170,6 +175,8 @@ "common_permalink" = "Tautan Permanen"; "common_permission" = "Perizinan"; "common_please_wait" = "Mohon tunggu…"; +"common_poll_end_confirmation" = "Apakah Anda yakin ingin mengakhiri pemungutan suara ini?"; +"common_poll_summary" = "Pemungutan suara: %1$@"; "common_poll_total_votes" = "Total suara: %1$@"; "common_poll_undisclosed_text" = "Hasil akan terlihat setelah pemungutan suara berakhir"; "common_privacy_policy" = "Kebijakan privasi"; @@ -200,6 +207,7 @@ "common_settings" = "Pengaturan"; "common_shared_location" = "Lokasi terbagi"; "common_signing_out" = "Mengeluarkan dari akun"; +"common_something_went_wrong" = "Ada yang salah"; "common_starting_chat" = "Memulai obrolan..."; "common_sticker" = "Stiker"; "common_success" = "Berhasil"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Tentang apa ruangan ini?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Tidak dapat mendekripsi"; +"common_unable_to_decrypt_no_access" = "Anda tidak memiliki akses ke pesan ini"; "common_unable_to_invite_message" = "Undangan tidak dapat dikirim ke satu atau beberapa pengguna."; "common_unable_to_invite_title" = "Tidak dapat mengirim undangan"; "common_unlock" = "Buka kunci"; @@ -221,23 +230,30 @@ "common_username" = "Nama pengguna"; "common_verification_cancelled" = "Verifikasi dibatalkan"; "common_verification_complete" = "Verifikasi selesai"; +"common_verification_failed" = "Verifikasi gagal"; +"common_verified" = "Terverifikasi"; +"common_verify_device" = "Verifikasi perangkat"; +"common_verify_identity" = "Verifikasi identitas"; "common_video" = "Video"; "common_voice_message" = "Pesan suara"; "common_waiting" = "Menunggu…"; "common_waiting_for_decryption_key" = "Menunggu pesan ini"; +"common.copied_to_clipboard" = "Disalin ke papan klip"; "common.do_not_show_this_again" = "Jangan tampilkan ini lagi"; -"common.open_source_licenses" = "Open source licenses"; -"common.pinned" = "Pinned"; +"common.open_source_licenses" = "Lisensi sumber terbuka"; +"common.pinned" = "Disematkan"; "common.send_to" = "Kirim ke"; -"common_no_room_name" = "Tidak ada nama ruangan"; -"common_poll_end_confirmation" = "Apakah Anda yakin ingin mengakhiri pemungutan suara ini?"; -"common_poll_summary" = "Pemungutan suara: %1$@"; -"common_something_went_wrong" = "Ada yang salah"; -"common_unable_to_decrypt_no_access" = "Anda tidak memiliki akses ke pesan ini"; -"common_verify_device" = "Verifikasi perangkat"; -"confirm_recovery_key_banner_message" = "Cadangan percakapan Anda saat ini tidak tersinkron. Anda perlu mengonfirmasi kunci pemulihan Anda untuk tetap memiliki akses ke cadangan percakapan Anda."; -"confirm_recovery_key_banner_title" = "Konfirmasi kunci pemulihan Anda"; +"common.you" = "Anda"; +"common_unable_to_decrypt_insecure_device" = "Dikirim dari perangkat yang tidak aman"; +"common_unable_to_decrypt_verification_violation" = "Identitas terverifikasi pengirim telah berubah"; +"confirm_recovery_key_banner_message" = "Konfirmasikan kunci pemulihan Anda untuk mempertahankan akses ke penyimpanan kunci dan riwayat pesan Anda."; +"confirm_recovery_key_banner_primary_button_title" = "Masukkan kunci pemulihan Anda"; +"confirm_recovery_key_banner_secondary_button_title" = "Lupa kunci pemulihan Anda?"; +"confirm_recovery_key_banner_title" = "Penyimpanan kunci Anda tidak sinkron"; "crash_detection_dialog_content" = "%1$@ mengalami kemogokan saat terakhir kali digunakan. Apakah Anda ingin berbagi laporan kerusakan dengan kami?"; +"crypto_identity_change_pin_violation" = "Identitas %1$@ tampaknya telah berubah. %2$@"; +"crypto_identity_change_pin_violation_new" = "Identitas %1$@ yang %2$@ tampaknya telah berubah. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Supaya aplikasinya dapat menggunakan kamera, berikan izin dalam pengaturan sistem."; "dialog_permission_generic" = "Silakan memberikan izin dalam pengaturan sistem."; "dialog_permission_location_description_ios" = "Berikan akses dalam Pengaturan -> Lokasi."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Senyuman & Orang"; "emoji_picker_category_places" = "Wisata & Tempat"; "emoji_picker_category_symbols" = "Simbol"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Homeserver Anda perlu ditingkatkan untuk mendukung Matrix Authentication Service dan pembuatan akun."; "error_failed_creating_the_permalink" = "Gagal membuat tautan permanen"; "error_failed_loading_map" = "%1$@ tidak dapat memuat peta. Silakan coba lagi nanti."; "error_failed_loading_messages" = "Gagal memuat pesan"; @@ -268,12 +284,12 @@ "error_no_compatible_app_found" = "Tidak ada aplikasi yang kompatibel yang ditemukan untuk menangani tindakan ini."; "error_some_messages_have_not_been_sent" = "Beberapa pesan belum terkirim"; "error_unknown" = "Maaf, terjadi kesalahan"; -"event_shield_reason_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; -"event_shield_reason_sent_in_clear" = "Not encrypted."; -"event_shield_reason_unknown_device" = "Encrypted by an unknown or deleted device."; -"event_shield_reason_unsigned_device" = "Encrypted by a device not verified by its owner."; -"event_shield_reason_unverified_identity" = "Encrypted by an unverified user."; +"event_shield_reason_authenticity_not_guaranteed" = "Keaslian pesan terenkripsi ini tidak dapat dijamin pada perangkat ini."; +"event_shield_reason_previously_verified" = "Dienkripsi oleh pengguna yang telah diverifikasi sebelumnya."; +"event_shield_reason_sent_in_clear" = "Tidak dienkripsi."; +"event_shield_reason_unknown_device" = "Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."; +"event_shield_reason_unsigned_device" = "Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."; +"event_shield_reason_unverified_identity" = "Dienkripsi oleh pengguna yang tidak terverifikasi."; "full_screen_intent_banner_message" = "Untuk memastikan Anda tidak melewatkan panggilan penting, silakan ubah pengaturan Anda untuk memperbolehkan notifikasi layar penuh ketika ponsel Anda terkunci."; "full_screen_intent_banner_title" = "Tingkatkan pengalaman panggilan Anda"; "invite_friends_rich_title" = "🔐️ Bergabunglah dengan saya di %1$@"; @@ -290,16 +306,15 @@ "notification_channel_silent" = "Pemberitahuan diam"; "notification_incoming_call" = "Panggilan masuk"; "notification_inline_reply_failed" = "** Gagal mengirim — silakan buka ruangan"; -"notification_invitation_action_reject" = "Tolak"; "notification_invite_body" = "Mengundang Anda untuk mengobrol"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ mengundang Anda untuk mengobrol"; "notification_mentioned_you_body" = "Menyebutkan Anda: %1$@"; "notification_new_messages" = "Pesan Baru"; "notification_reaction_body" = "Menghapus dengan %1$@"; "notification_room_invite_body" = "Mengundang Anda untuk bergabung ke ruangan"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ mengundang Anda untuk bergabung dengan ruangan"; "notification_sender_me" = "Saya"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@ disebut atau dibalas"; "notification_test_push_notification_content" = "Anda sedang melihat pemberitahuan ini! Klik saya!"; "notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,31 +344,46 @@ "rich_text_editor_unindent" = "Hapus indentasi"; "rich_text_editor_url_placeholder" = "Tautan"; "rich_text_editor_a11y_add_attachment" = "Tambahkan lampiran"; +"rich_text_editor_composer_caption_placeholder" = "Keterangan opsional..."; "screen_advanced_settings_element_call_base_url" = "URL dasar Element Call khusus"; "screen_advanced_settings_element_call_base_url_description" = "Tetapkan URL dasar khusus untuk Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."; -"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; -"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; -"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; -"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; -"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; -"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_create_room_room_address_section_footer" = "Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."; +"screen_create_room_room_address_section_title" = "Alamat ruangan"; +"screen_create_room_room_visibility_section_title" = "Keterlihatan ruangan"; +"screen_create_room_access_section_anyone_option_description" = "Siapa pun dapat bergabung dengan ruangan ini"; +"screen_create_room_access_section_anyone_option_title" = "Siapa pun"; +"screen_create_room_access_section_header" = "Akses Ruangan"; +"screen_create_room_access_section_knocking_option_description" = "Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"; +"screen_create_room_access_section_knocking_option_title" = "Minta untuk bergabung"; +"screen_join_room_cancel_knock_action" = "Batalkan permintaan"; +"screen_join_room_cancel_knock_alert_confirmation" = "Ya, batalkan"; +"screen_join_room_cancel_knock_alert_description" = "Apakah Anda yakin ingin membatalkan permintaan Anda untuk bergabung dengan ruangan ini?"; +"screen_join_room_cancel_knock_alert_title" = "Batalkan permintaan untuk bergabung"; +"screen_join_room_knock_message_description" = "Pesan (opsional)"; +"screen_join_room_knock_sent_description" = "Anda akan menerima undangan untuk bergabung dengan ruangan jika permintaan Anda diterima."; +"screen_join_room_knock_sent_title" = "Permintaan untuk bergabung dikirim"; +"screen_pinned_timeline_empty_state_description" = "Tekan pesan dan pilih “%1$@” untuk disertakan di sini."; +"screen_pinned_timeline_empty_state_headline" = "Sematkan pesan penting agar mudah ditemukan"; +"screen_reset_encryption_password_error" = "Terjadi kesalahan yang tidak diketahui. Harap periksa apakah kata sandi akun Anda sudah benar dan coba lagi."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Tarik verifikasi dan kirim"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Anda dapat menarik verifikasi dan tetap mengirim pesan ini, atau Anda dapat membatalkan untuk saat ini dan mencoba lagi nanti setelah memverifikasi ulang %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "Pesan Anda tidak terkirim karena identitas terverifikasi %1$@ telah berubah"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Kirim pesan saja"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ menggunakan satu atau beberapa perangkat yang belum diverifikasi. Anda tetap dapat mengirim pesan, atau Anda dapat membatalkan untuk saat ini dan mencoba lagi nanti setelah %2$@ telah memverifikasi semua perangkat mereka."; +"screen_resolve_send_failure_unsigned_device_title" = "Pesan Anda tidak terkirim karena %1$@ belum memverifikasi semua perangkat"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Satu atau beberapa perangkat Anda tidak terverifikasi. Anda tetap dapat mengirim pesan, atau Anda dapat membatalkannya dan mencoba lagi nanti setelah Anda memverifikasi semua perangkat."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Pesan Anda tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda"; "screen_room_mentions_at_room_subtitle" = "Beri tahu seluruh ruangan"; -"screen_room_pinned_banner_indicator" = "%1$@ of %2$@"; -"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages"; -"screen_room_pinned_banner_loading_description" = "Loading message…"; -"screen_room_pinned_banner_view_all_button_title" = "View All"; -"screen_room_details_pinned_events_row_title" = "Pinned messages"; -"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Ubah penyedia akun"; +"screen_room_pinned_banner_indicator" = "%1$@ dari %2$@"; +"screen_room_pinned_banner_indicator_description" = "%1$@ Pesan yang disematkan"; +"screen_room_pinned_banner_loading_description" = "Memuat pesan…"; +"screen_room_pinned_banner_view_all_button_title" = "Lihat Semua"; +"screen_room_details_pinned_events_row_title" = "Pesan yang disematkan"; +"screen_roomlist_knock_event_sent_description" = "Permintaan untuk bergabung dikirim"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Pesan tidak terkirim karena identitas terverifikasi %1$@ telah berubah."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Pesan tidak terkirim karena %1$@ belum memverifikasi semua perangkat."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Pesan tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda."; "screen_account_provider_form_hint" = "Alamat homeserver"; "screen_account_provider_form_notice" = "Masukkan istilah pencarian atau alamat domain."; "screen_account_provider_form_subtitle" = "Cari perusahaan, komunitas, atau server pribadi."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Anda akan membuat akun di %@"; "screen_advanced_settings_developer_mode" = "Mode pengembang"; "screen_advanced_settings_developer_mode_description" = "Aktifkan untuk mengakses fitur dan fungsi untuk para pengembang."; +"screen_advanced_settings_media_compression_description" = "Unggah foto dan video lebih cepat dan kurangi penggunaan data"; +"screen_advanced_settings_media_compression_title" = "Optimalkan kualitas media"; "screen_advanced_settings_rich_text_editor_description" = "Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual."; "screen_advanced_settings_send_read_receipts" = "Laporan dibaca"; "screen_advanced_settings_send_read_receipts_description" = "Jika dimatikan, laporan dibaca Anda tidak akan dikirim kepada siapa pun. Anda masih akan menerima laporan dibaca dari pengguna lain."; @@ -428,14 +460,16 @@ "screen_change_server_title" = "Pilih server Anda"; "screen_chat_backup_key_backup_action_disable" = "Matikan pencadangan"; "screen_chat_backup_key_backup_action_enable" = "Nyalakan pencadangan"; -"screen_chat_backup_key_backup_description" = "Pencadangan memastikan bahwa Anda tidak akan kehilangan riwayat pesan Anda. %1$@."; -"screen_chat_backup_key_backup_title" = "Pencadangan"; +"screen_chat_backup_key_backup_description" = "Simpan identitas kriptografi Anda dan kunci-kunci pesan secara aman di server. Ini akan memungkinkan Anda untuk melihat riwayat pesan Anda di perangkat yang baru. %1$@."; +"screen_chat_backup_key_backup_title" = "Penyimpanan kunci"; +"screen_chat_backup_key_storage_disabled_error" = "Penyimpanan kunci harus diaktifkan untuk menyiapkan pemulihan."; +"screen_chat_backup_key_storage_toggle_description" = "Unggah kunci dari perangkat ini"; +"screen_chat_backup_key_storage_toggle_title" = "Izinkan penyimpanan kunci"; "screen_chat_backup_recovery_action_change" = "Ubah kunci pemulihan"; -"screen_chat_backup_recovery_action_confirm" = "Konfirmasi kunci pemulihan"; -"screen_chat_backup_recovery_action_confirm_description" = "Pencadangan percakapan Anda saat ini tidak tersinkron."; -"screen_chat_backup_recovery_action_setup" = "Siapkan pemulihan"; +"screen_chat_backup_recovery_action_change_description" = "Pulihkan identitas kriptografi dan riwayat pesan Anda dengan kunci pemulihan jika Anda kehilangan semua perangkat yang ada."; +"screen_chat_backup_recovery_action_confirm_description" = "Penyimpanan kunci Anda saat ini tidak sinkron."; "screen_chat_backup_recovery_action_setup_description" = "Dapatkan akses ke pesan terenkripsi Anda jika Anda kehilangan semua perangkat Anda atau keluar dari %1$@ di mana pun."; -"screen_create_account_title" = "Create account"; +"screen_create_account_title" = "Buat akun"; "screen_create_new_recovery_key_list_item_1" = "Buka %1$@ di perangkat desktop"; "screen_create_new_recovery_key_list_item_2" = "Masuk ke akun Anda lagi"; "screen_create_new_recovery_key_list_item_3" = "Saat diminta untuk memverifikasi perangkat Anda, pilih %1$@"; @@ -447,29 +481,28 @@ "screen_create_poll_anonymous_desc" = "Tampilkan hasil hanya setelah pemungutan suara berakhir"; "screen_create_poll_anonymous_headline" = "Pemungutan suara anonim"; "screen_create_poll_answer_hint" = "Opsi %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Perubahan Anda tidak akan disimpan"; "screen_create_poll_cancel_confirmation_title_ios" = "Batal pemungutan suara"; "screen_create_poll_question_desc" = "Pertanyaan atau topik"; "screen_create_poll_question_hint" = "Tentang apa pemungutan suara ini?"; "screen_create_poll_title" = "Buat pemungutan suara"; "screen_create_room_action_create_room" = "Ruangan baru"; "screen_create_room_error_creating_room" = "Terjadi kesalahan saat membuat ruangan"; -"screen_create_room_private_option_description" = "Pesan di ruangan ini dienkripsi. Enkripsi tidak dapat dinonaktifkan setelahnya."; -"screen_create_room_private_option_title" = "Ruangan pribadi (hanya undangan)"; -"screen_create_room_public_option_description" = "Pesan tidak dienkripsi dan siapa pun dapat membacanya. Anda dapat mengaktifkan enkripsi di kemudian hari."; -"screen_create_room_public_option_title" = "Ruang publik (siapa saja)"; +"screen_create_room_private_option_description" = "Hanya orang-orang yang diundang dapat mengakses ruangan ini. Semua pesan terenkripsi secara ujung ke ujung."; +"screen_create_room_private_option_title" = "Ruangan pribadi"; +"screen_create_room_public_option_description" = "Siapa pun dapat mencari ruangan ini.\nAnda dapat mengubah ini kapan pun dalam pengaturan ruangan."; +"screen_create_room_public_option_title" = "Ruangan publik"; "screen_create_room_topic_label" = "Topik (opsional)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Harap konfirmasi bahwa Anda ingin menonaktifkan akun Anda. Tindakan ini tidak dapat diurungkan."; +"screen_deactivate_account_delete_all_messages" = "Hapus semua pesan saya"; +"screen_deactivate_account_delete_all_messages_notice" = "Peringatan: Pengguna masa depan mungkin melihat percakapan yang tidak lengkap."; +"screen_deactivate_account_description" = "Penonaktifan akun Anda %1$@, ini akan:"; +"screen_deactivate_account_description_bold_part" = "tidak dapat diurungkan"; +"screen_deactivate_account_list_item_1" = "%1$@ akun Anda (Anda tidak dapat masuk kembali, dan ID Anda tidak dapat digunakan kembali)."; +"screen_deactivate_account_list_item_1_bold_part" = "Nonaktifkan secara permanen"; +"screen_deactivate_account_list_item_2" = "Mengeluarkan Anda dari semua ruangan obrolan."; +"screen_deactivate_account_list_item_3" = "Hapus informasi akun Anda dari server identitas kami."; +"screen_deactivate_account_list_item_4" = "Pesan Anda akan tetap terlihat oleh pengguna terdaftar tetapi tidak akan tersedia bagi pengguna baru atau tidak terdaftar jika Anda memilih untuk menghapusnya."; +"screen_deactivate_account_title" = "Nonaktifkan akun"; "screen_edit_poll_delete_confirmation" = "Apakah Anda yakin ingin menghapus pemungutan suara ini?"; "screen_edit_profile_display_name" = "Nama tampilan"; "screen_edit_profile_display_name_placeholder" = "Nama tampilan Anda"; @@ -477,18 +510,18 @@ "screen_edit_profile_error_title" = "Tidak dapat memperbarui profil"; "screen_edit_profile_title" = "Sunting profil"; "screen_edit_profile_updating_details" = "Memperbarui profil…"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; -"screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; -"screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; -"screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; -"screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; -"screen_identity_confirmation_cannot_confirm" = "Can't confirm?"; +"screen_encryption_reset_action_continue_reset" = "Lanjutkan pengaturan ulang"; +"screen_encryption_reset_bullet_1" = "Detail akun, kontak, preferensi, dan daftar obrolan Anda akan disimpan"; +"screen_encryption_reset_bullet_2" = "Anda akan kehilangan riwayat pesan yang hanya disimpan di server"; +"screen_encryption_reset_bullet_3" = "Anda perlu memverifikasi semua perangkat dan kontak yang ada lagi"; +"screen_encryption_reset_footer" = "Hanya atur ulang identitas Anda jika Anda tidak memiliki akses ke perangkat lain yang masuk dan Anda kehilangan kunci pemulihan."; +"screen_encryption_reset_title" = "Tidak dapat mengonfirmasi? Anda perlu mengatur ulang identitas Anda."; +"screen_identity_confirmation_cannot_confirm" = "Tidak dapat mengonfirmasi?"; "screen_identity_confirmation_create_new_recovery_key" = "Buat kunci pemulihan baru"; "screen_identity_confirmation_subtitle" = "Verifikasi perangkat ini untuk menyiapkan perpesanan aman."; "screen_identity_confirmation_title" = "Konfirmasi bahwa ini Anda"; "screen_identity_confirmation_use_another_device" = "Gunakan perangkat lain"; -"screen_identity_confirmation_use_recovery_key" = "Use recovery key"; +"screen_identity_confirmation_use_recovery_key" = "Gunakan kunci pemulihan"; "screen_identity_confirmed_subtitle" = "Sekarang Anda dapat membaca atau mengirim pesan dengan aman, dan siapa pun yang mengobrol dengan Anda juga dapat mempercayai perangkat ini."; "screen_identity_confirmed_title" = "Perangkat terverifikasi"; "screen_identity_waiting_on_other_device" = "Menunggu di perangkat lain…"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Obrolan grup"; "screen_notification_settings_invite_for_me_label" = "Undangan"; "screen_notification_settings_mentions_only_disclaimer" = "Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."; -"screen_notification_settings_mentions_section_title" = "Sebutan"; "screen_notification_settings_mode_all" = "Semua"; "screen_notification_settings_mode_mentions" = "Sebutan"; "screen_notification_settings_notification_section_title" = "Beri tahu saya tentang"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Pilih %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Tautkan perangkat baru”"; "screen_qr_code_login_initial_state_item_4" = "Pindai kode QR dengan perangkat ini"; +"screen_qr_code_login_initial_state_subtitle" = "Hanya tersedia jika penyedia akun Anda mendukungnya."; "screen_qr_code_login_initial_state_title" = "Buka %1$@ di perangkat lain untuk mendapatkan kode QR"; "screen_qr_code_login_invalid_scan_state_description" = "Gunakan kode QR yang ditampilkan di perangkat lain."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Kode QR salah"; @@ -605,42 +638,39 @@ "screen_qr_code_login_verify_code_title" = "Kode verifikasi Anda"; "screen_recovery_key_change_description" = "Dapatkan kunci pemulihan yang baru jika Anda kehilangan kunci pemulihan saat ini. Setelah mengganti kunci pemulihan Anda, yang lama tidak akan bekerja lagi."; "screen_recovery_key_change_generate_key" = "Buat kunci pemulihan baru"; -"screen_recovery_key_change_generate_key_description" = "Pastikan Anda dapat menyimpan kunci pemulihan Anda di tempat yang aman"; "screen_recovery_key_change_success" = "Kunci pemulihan diganti"; "screen_recovery_key_change_title" = "Ubah kunci pemulihan?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Buat kunci pemulihan baru"; "screen_recovery_key_confirm_description" = "Pastikan tidak ada yang bisa melihat layar ini!"; -"screen_recovery_key_confirm_error_content" = "Silakan coba lagi untuk mengonfirmasi akses ke cadangan percakapan Anda."; +"screen_recovery_key_confirm_error_content" = "Silakan coba lagi untuk mengonfirmasi akses ke penyimpanan kunci Anda."; "screen_recovery_key_confirm_error_title" = "Kunci pemulihan salah"; "screen_recovery_key_confirm_key_description" = "Jika Anda memiliki kunci keamanan atau frasa keamanan, ini juga bisa digunakan."; "screen_recovery_key_confirm_key_placeholder" = "Masukkan..."; "screen_recovery_key_confirm_lost_recovery_key" = "Kehilangan kunci pemulihan Anda?"; "screen_recovery_key_confirm_success" = "Kunci pemulihan dikonfirmasi"; -"screen_recovery_key_confirm_title" = "Konfirmasi kunci pemulihan Anda"; "screen_recovery_key_copied_to_clipboard" = "Kunci pemulihan disalin"; "screen_recovery_key_generating_key" = "Membuat…"; "screen_recovery_key_save_action" = "Simpan kunci pemulihan"; -"screen_recovery_key_save_description" = "Tuliskan kunci pemulihan Anda di tempat yang aman atau simpan di pengelola kata sandi."; +"screen_recovery_key_save_description" = "Tuliskan kunci pemulihan ini di tempat yang aman, seperti pengelola kata sandi, catatan terenkripsi, atau brankas fisik."; "screen_recovery_key_save_key_description" = "Ketuk untuk menyalin kunci pemulihan"; "screen_recovery_key_save_title" = "Simpan kunci pemulihan Anda"; "screen_recovery_key_setup_confirmation_description" = "Anda tidak akan dapat mengakses kunci pemulihan Anda setelah langkah ini."; "screen_recovery_key_setup_confirmation_title" = "Apakah Anda sudah menyimpan kunci pemulihan Anda?"; "screen_recovery_key_setup_description" = "Pencadangan percakapan Anda sedang dilindungi oleh sebuah kunci pemulihan. Jika Anda perlu kunci pemulihan yang baru setelah penyiapan, Anda dapat membuat ulang dengan memilih 'Ubah kunci pemulihan'."; "screen_recovery_key_setup_generate_key" = "Buat kunci pemulihan Anda"; -"screen_recovery_key_setup_generate_key_description" = "Pastikan Anda dapat menyimpan kunci pemulihan Anda di tempat yang aman"; +"screen_recovery_key_setup_generate_key_description" = "Jangan bagikan ini kepada siapa pun!"; "screen_recovery_key_setup_success" = "Penyiapan pemulihan berhasil"; "screen_recovery_key_setup_title" = "Siapkan pemulihan"; "screen_report_content_block_user_hint" = "Centang jika Anda ingin menyembunyikan semua pesan saat ini dan yang akan datang dari pengguna ini"; "screen_report_content_explanation" = "Pesan ini akan dilaporkan ke administrator homeserver Anda. Mereka tidak akan dapat membaca pesan terenkripsi apa pun."; "screen_report_content_hint" = "Alasan melaporkan konten ini"; -"screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; -"screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; -"screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; -"screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; -"screen_reset_encryption_password_title" = "Enter your account password to continue"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_encryption_confirmation_alert_action" = "Ya, atur ulang sekarang"; +"screen_reset_encryption_confirmation_alert_subtitle" = "Proses ini tidak dapat diurungkan."; +"screen_reset_encryption_confirmation_alert_title" = "Apakah Anda yakin ingin mengatur ulang identitas Anda?"; +"screen_reset_encryption_password_subtitle" = "Konfirmasikan bahwa Anda ingin mengatur ulang identitas Anda."; +"screen_reset_encryption_password_title" = "Masukkan kata sandi akun Anda untuk melanjutkan"; +"screen_reset_identity_confirmation_subtitle" = "Anda akan pergi ke akun %1$@ Anda untuk mengatur ulang identitas Anda. Setelah itu Anda akan dibawa kembali ke aplikasi."; +"screen_reset_identity_confirmation_title" = "Tidak dapat mengonfirmasi? Buka akun Anda untuk mengatur ulang identitas Anda."; "screen_room_alias_resolver_resolve_alias_failure" = "Gagal menyelesaikan alias ruangan."; "screen_room_attachment_source_camera" = "Kamera"; "screen_room_attachment_source_camera_video" = "Rekam video"; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admin secara otomatis memiliki hak moderator"; "screen_room_change_role_moderators_title" = "Sunting Moderator"; "screen_room_change_role_unsaved_changes_description" = "Anda memiliki perubahan yang belum disimpan."; -"screen_room_change_role_unsaved_changes_title" = "Simpan perubahan?"; "screen_room_details_add_topic_title" = "Tambahkan topik"; "screen_room_details_already_a_member" = "Sudah menjadi anggota"; "screen_room_details_already_invited" = "Sudah diundang"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Gagal membunyikan ruangan ini, silakan coba lagi."; "screen_room_details_notification_mode_custom" = "Khusus"; "screen_room_details_notification_mode_default" = "Bawaan"; -"screen_room_details_notification_title" = "Pemberitahuan"; "screen_room_details_share_room_title" = "Bagikan ruangan"; "screen_room_details_title" = "Info ruangan"; "screen_room_details_updating_room" = "Memperbarui ruangan…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Buka blokir"; "screen_room_member_details_unblock_alert_description" = "Anda akan dapat melihat semua pesan dari mereka lagi."; "screen_room_member_details_unblock_user" = "Buka blokir pengguna"; +"screen_room_member_details_verify_button_subtitle" = "Gunakan aplikasi web untuk memverifikasi pengguna ini."; +"screen_room_member_details_verify_button_title" = "Verifikasi %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Cekal"; "screen_room_member_list_ban_member_confirmation_description" = "Mereka tidak akan dapat bergabung ke ruangan ini lagi jika diundang."; "screen_room_member_list_ban_member_confirmation_title" = "Apakah Anda yakin ingin mencekal anggota ini?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Mencekal %1$@"; "screen_room_member_list_manage_member_ban" = "Keluarkan dan cekal anggota"; "screen_room_member_list_manage_member_remove" = "Keluarkan dari ruangan"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Keluarkan dan cekal anggota"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Hanya keluarkan anggota"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Keluarkan pengguna dan cekal pengguna bergabung lagi di masa mendatang?"; "screen_room_member_list_manage_member_unban_action" = "Batalkan pencekalan"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Tampilkan lebih sedikit"; "screen_room_timeline_message_copied" = "Pesan disalin"; "screen_room_timeline_no_permission_to_post" = "Anda tidak memiliki izin untuk mengirim di ruangan ini"; -"screen_room_timeline_reactions_show_less" = "Tampilkan lebih sedikit"; "screen_room_timeline_reactions_show_more" = "Tampilkan lebih banyak"; "screen_room_timeline_read_marker_title" = "Baru"; "screen_room_title" = "Obrolan"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Tandai sebagai dibaca"; "screen_roomlist_mark_as_unread" = "Tandai sebagai belum dibaca"; "screen_roomlist_room_directory_button_title" = "Cari semua ruangan"; -"screen_server_confirmation_change_server" = "Ubah penyedia akun"; "screen_server_confirmation_message_login_element_dot_io" = "Server pribadi untuk karyawan Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."; "screen_server_confirmation_message_register" = "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Bandingkan angka"; "screen_session_verification_complete_subtitle" = "Sesi baru Anda sekarang diverifikasi. Ini memiliki akses ke pesan terenkripsi Anda, dan pengguna lain akan melihatnya sebagai tepercaya."; "screen_session_verification_enter_recovery_key" = "Masukkan kunci pemulihan"; +"screen_session_verification_failed_subtitle" = "Entah permintaan habis waktu, permintaan ditolak, atau ada ketidakcocokan verifikasi."; "screen_session_verification_open_existing_session_subtitle" = "Buktikan bahwa ini memang Anda untuk mengakses riwayat pesan terenkripsi Anda."; "screen_session_verification_open_existing_session_title" = "Buka sesi yang sudah ada"; "screen_session_verification_positive_button_canceled" = "Verifikasi ulang"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Menunggu untuk mencocokkan"; "screen_session_verification_ready_subtitle" = "Bandingkan satu set emoji yang unik."; "screen_session_verification_request_accepted_subtitle" = "Bandingkan emoji unik, dan pastikan emoji tersebut muncul dalam urutan yang sama."; +"screen_session_verification_request_details_timestamp" = "Sudah masuk"; +"screen_session_verification_request_failure_title" = "Verifikasi gagal"; +"screen_session_verification_request_footer" = "Lanjutkan hanya jika Anda memulai verifikasi ini."; +"screen_session_verification_request_subtitle" = "Verifikasi perangkat lain untuk menjaga riwayat pesan Anda tetap aman."; +"screen_session_verification_request_success_subtitle" = "Sekarang Anda dapat membaca atau mengirim pesan dengan aman di perangkat Anda yang lain."; +"screen_session_verification_request_success_title" = "Perangkat diverifikasi"; +"screen_session_verification_request_title" = "Verifikasi diminta"; "screen_session_verification_they_dont_match" = "Mereka tidak cocok"; "screen_session_verification_they_match" = "Mereka cocok"; "screen_session_verification_waiting_to_accept_subtitle" = "Terima permintaan untuk memulai proses verifikasi di sesi Anda yang lain untuk melanjutkan."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda."; "screen_signout_key_backup_disabled_title" = "Anda telah menonaktifkan pencadangan"; "screen_signout_key_backup_offline_subtitle" = "Kunci Anda masih dicadangkan saat Anda luring. Sambungkan kembali sehingga kunci Anda dapat dicadangkan sebelum keluar."; -"screen_signout_key_backup_offline_title" = "Kunci Anda masih dicadangkan"; "screen_signout_key_backup_ongoing_subtitle" = "Mohon tunggu hingga proses ini selesai sebelum keluar."; "screen_signout_key_backup_ongoing_title" = "Kunci Anda masih dicadangkan"; "screen_signout_recovery_disabled_subtitle" = "Anda akan keluar dari sesi Anda yang terakhir. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda."; "screen_signout_recovery_disabled_title" = "Pemulihan belum disiapkan"; "screen_signout_save_recovery_key_subtitle" = "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda mungkin kehilangan akses ke pesan terenkripsi Anda."; -"screen_signout_save_recovery_key_title" = "Apakah Anda sudah menyimpan kunci pemulihan Anda?"; "screen_start_chat_error_starting_chat" = "Terjadi kesalahan saat mencoba memulai obrolan"; "screen_view_location_title" = "Lokasi"; "screen_welcome_bullet_1" = "Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."; @@ -895,12 +928,12 @@ "state_event_room_name_removed_by_you" = "Anda menghapus nama ruangan"; "state_event_room_none" = "%1$@ tidak membuat perubahan"; "state_event_room_none_by_you" = "Anda tidak membuat perubahan"; -"state_event_room_pinned_events_changed" = "%1$@ changed the pinned messages"; -"state_event_room_pinned_events_changed_by_you" = "You changed the pinned messages"; -"state_event_room_pinned_events_pinned" = "%1$@ pinned a message"; -"state_event_room_pinned_events_pinned_by_you" = "You pinned a message"; -"state_event_room_pinned_events_unpinned" = "%1$@ unpinned a message"; -"state_event_room_pinned_events_unpinned_by_you" = "You unpinned a message"; +"state_event_room_pinned_events_changed" = "%1$@ mengubah pesan yang disematkan"; +"state_event_room_pinned_events_changed_by_you" = "Anda mengubah pesan yang disematkan"; +"state_event_room_pinned_events_pinned" = "%1$@ menyematkan pesan"; +"state_event_room_pinned_events_pinned_by_you" = "Anda menyematkan pesan"; +"state_event_room_pinned_events_unpinned" = "%1$@ melepas sematan pesan"; +"state_event_room_pinned_events_unpinned_by_you" = "Anda melepas sematan pesan"; "state_event_room_reject" = "%1$@ menolak undangan"; "state_event_room_reject_by_you" = "Anda menolak undangan"; "state_event_room_remove" = "%1$@ mengeluarkan %2$@"; @@ -919,7 +952,6 @@ "test_language_identifier" = "id"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Pemecahan masalah"; -"troubleshoot_notifications_entry_point_title" = "Pecahkan masalah notifikasi"; "troubleshoot_notifications_screen_action" = "Jalankan tes"; "troubleshoot_notifications_screen_action_again" = "Jalankan tes lagi"; "troubleshoot_notifications_screen_failure" = "Beberapa tes gagal. Silakan periksa detailnya."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Pastikan distributor UnifiedPush tersedia."; "troubleshoot_notifications_test_unified_push_failure" = "Tidak ada distributor notifikasi dorongan yang ditemukan."; "troubleshoot_notifications_test_unified_push_title" = "Periksa UnifiedPush"; +"a11y_poll" = "Pemungutan suara"; +"banner_set_up_recovery_submit" = "Siapkan pemulihan"; "dialog_title_error" = "Eror"; "dialog_title_success" = "Berhasil"; "notification_fallback_content" = "Notifikasi"; "notification_invitation_action_join" = "Gabung"; +"notification_invitation_action_reject" = "Tolak"; "notification_room_action_mark_as_read" = "Tandai sebagai dibaca"; "notification_room_action_quick_reply" = "Balas cepat"; +"screen_pinned_timeline_screen_title_empty" = "Pesan yang disematkan"; "screen_room_mentions_at_room_title" = "Semua orang"; +"screen_account_provider_change" = "Ubah penyedia akun"; "screen_account_provider_signin_subtitle" = "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda."; "screen_account_provider_signup_subtitle" = "Di sinilah percakapan Anda akan berlangsung — sama seperti Anda menggunakan penyedia surel untuk menyimpan surel Anda."; "screen_analytics_settings_help_us_improve" = "Bagikan data penggunaan anonim untuk membantu kami mengidentifikasi masalah."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Anda akan dapat melihat semua pesan dari mereka lagi."; "screen_blocked_users_unblock_alert_title" = "Buka blokir pengguna"; "screen_bug_report_rash_logs_alert_title" = "%1$@ mengalami kemogokan saat terakhir kali digunakan. Apakah Anda ingin berbagi laporan kerusakan dengan kami?"; +"screen_chat_backup_recovery_action_confirm" = "Masukkan kunci pemulihan"; +"screen_chat_backup_recovery_action_setup" = "Siapkan pemulihan"; +"screen_create_poll_cancel_confirmation_content_ios" = "Perubahan Anda tidak akan disimpan"; "screen_create_room_add_people_title" = "Undang orang-orang"; "screen_create_room_room_name_label" = "Nama ruangan"; "screen_create_room_title" = "Buat ruangan"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Sunting pemungutan suara"; "screen_identity_use_another_device" = "Gunakan perangkat lain"; "screen_login_subtitle" = "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi."; +"screen_notification_settings_mentions_section_title" = "Sebutan"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Coba lagi"; +"screen_recovery_key_change_generate_key_description" = "Jangan bagikan ini kepada siapa pun!"; +"screen_recovery_key_confirm_title" = "Masukkan kunci pemulihan Anda"; "screen_report_content_block_user" = "Blokir pengguna"; +"screen_reset_encryption_password_placeholder" = "Masukkan..."; "screen_room_attachment_source_camera_photo" = "Ambil foto"; "screen_room_change_permissions_everyone" = "Semua orang"; "screen_room_change_permissions_member_moderation" = "Moderasi anggota"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Admin"; "screen_room_change_role_section_moderators" = "Moderator"; "screen_room_change_role_section_users" = "Anggota"; +"screen_room_change_role_unsaved_changes_title" = "Simpan perubahan?"; "screen_room_details_invite_people_title" = "Undang orang-orang"; "screen_room_details_leave_conversation_title" = "Tinggalkan percakapan"; "screen_room_details_leave_room_title" = "Tinggalkan ruangan"; +"screen_room_details_notification_title" = "Notifikasi"; "screen_room_details_roles_and_permissions" = "Peran dan perizinan"; "screen_room_details_room_name_label" = "Nama ruangan"; "screen_room_details_security_title" = "Keamanan"; "screen_room_details_topic_title" = "Topik"; "screen_room_error_failed_processing_media" = "Gagal memproses media untuk diunggah, silakan coba lagi."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Keluarkan dan cekal anggota"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Sebutan dan Kata Kunci saja"; +"screen_room_timeline_reactions_show_less" = "Tampilkan lebih sedikit"; "screen_roomlist_filter_people" = "Orang"; +"screen_server_confirmation_change_server" = "Ubah penyedia akun"; +"screen_session_verification_request_failure_subtitle" = "Entah permintaan habis waktu, permintaan ditolak, atau ada ketidakcocokan verifikasi."; "screen_signout_confirmation_dialog_submit" = "Keluar dari akun"; "screen_signout_confirmation_dialog_title" = "Keluar dari akun"; +"screen_signout_key_backup_offline_title" = "Kunci Anda masih dicadangkan"; "screen_signout_preference_item" = "Keluar dari akun"; +"screen_signout_save_recovery_key_title" = "Apakah Anda sudah menyimpan kunci pemulihan Anda?"; +"troubleshoot_notifications_entry_point_title" = "Pecahkan masalah notifikasi"; diff --git a/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict index b4e966a3b9..2787aa3503 100644 --- a/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict @@ -180,10 +180,8 @@ NSStringPluralRuleType NSStringFormatValueTypeKey d - one - %1$d Pinned message other - %1$d Pinned messages + %1$d Pesan yang disematkan screen_room_member_list_header_title diff --git a/ElementX/Resources/Localizations/it.lproj/Localizable.strings b/ElementX/Resources/Localizations/it.lproj/Localizable.strings index f3b48eb9dc..ef7bddaf9f 100644 --- a/ElementX/Resources/Localizations/it.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/it.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausa"; "a11y_pin_field" = "Campo del PIN"; "a11y_play" = "Riproduci"; -"a11y_poll" = "Sondaggio"; "a11y_poll_end" = "Sondaggio terminato"; "a11y_react_with" = "Reagisci con %1$@"; "a11y_react_with_other_emojis" = "Reagisci con altri emoji"; @@ -27,20 +26,21 @@ "action_back" = "Indietro"; "action_call" = "Chiama"; "action_cancel" = "Annulla"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Annulla per ora"; "action_choose_photo" = "Scegli foto"; "action_clear" = "Cancella"; "action_close" = "Chiudi"; "action_complete_verification" = "Completa verifica"; "action_confirm" = "Conferma"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Conferma password"; "action_continue" = "Continua"; "action_copy" = "Copia"; "action_copy_link" = "Copia collegamento"; "action_copy_link_to_message" = "Copia collegamento al messaggio"; "action_create" = "Crea"; "action_create_a_room" = "Crea una stanza"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Disattiva"; +"action_deactivate_account" = "Disattiva account"; "action_decline" = "Rifiuta"; "action_delete_poll" = "Elimina sondaggio"; "action_disable" = "Disabilita"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Password dimenticata?"; "action_forward" = "Inoltra"; "action_go_back" = "Indietro"; +"action_ignore" = "Ignore"; "action_invite" = "Invita"; "action_invite_friends" = "Invita persone"; "action_invite_friends_to_app" = "Invita persone su %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Esci"; "action_leave_conversation" = "Abbandona la conversazione"; "action_leave_room" = "Esci dalla stanza"; +"action_load_more" = "Carica altro"; "action_manage_account" = "Gestisci account"; "action_manage_devices" = "Gestisci dispositivi"; "action_message" = "Invia messaggio"; @@ -93,6 +95,7 @@ "action_send_message" = "Invia messaggio"; "action_share" = "Condividi"; "action_share_link" = "Condividi collegamento"; +"action_show" = "Show"; "action_sign_in_again" = "Accedi di nuovo"; "action_signout" = "Disconnetti"; "action_signout_anyway" = "Disconnetti comunque"; @@ -105,17 +108,15 @@ "action_tap_for_options" = "Tocca per le opzioni"; "action_try_again" = "Riprova"; "action_unpin" = "Rimuovi dai fissati"; -"action_view_in_timeline" = "View in timeline"; +"action_view_in_timeline" = "Visualizza nella conversazione"; "action_view_source" = "Vedi codice sorgente"; "action_yes" = "Sì"; -"action.load_more" = "Carica altro"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "Esci e aggiorna"; +"banner_migrate_to_native_sliding_sync_description" = "Il tuo server ora supporta un nuovo protocollo più veloce. Esci e rientra per effettuare l'aggiornamento. Se lo fai ora, eviterai una disconnessione forzata quando il vecchio protocollo verrà rimosso in seguito."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Il tuo homeserver non supporta più il vecchio protocollo. Esci e rientra per continuare a usare l'app."; +"banner_migrate_to_native_sliding_sync_title" = "Aggiornamento disponibile"; +"banner_set_up_recovery_content" = "Genera una nuova chiave di recupero che può essere usata per ripristinare la cronologia dei messaggi crittografati nel caso in cui tu perda l'accesso ai tuoi dispositivi."; +"banner_set_up_recovery_title" = "Configura il ripristino"; "common_about" = "Informazioni"; "common_acceptable_use_policy" = "Regole sull'utilizzo consentito"; "common_advanced_settings" = "Impostazioni avanzate"; @@ -133,10 +134,12 @@ "common_dark" = "Scuro"; "common_decryption_error" = "Errore di decrittazione"; "common_developer_options" = "Opzioni sviluppatore"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Conversazione diretta"; "common_edited_suffix" = "(modificato)"; "common_editing" = "Modifica in corso"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Crittografia abilitata"; "common_enter_your_pin" = "Inserisci il PIN"; "common_error" = "Errore"; @@ -147,6 +150,7 @@ "common_favourited" = "Preferita"; "common_file" = "File"; "common_forward_message" = "Inoltra messaggio"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Immagine"; "common_in_reply_to" = "In risposta a %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderno"; "common_mute" = "Silenzia"; "common_no_results" = "Nessun risultato"; +"common_no_room_name" = "Nessun nome della stanza"; "common_offline" = "Non in linea"; "common_optic_id_ios" = "Optic ID"; "common_or" = "o"; @@ -170,6 +175,8 @@ "common_permalink" = "Collegamento permanente"; "common_permission" = "Autorizzazione"; "common_please_wait" = "Attendere prego..."; +"common_poll_end_confirmation" = "Vuoi davvero terminare questo sondaggio?"; +"common_poll_summary" = "Sondaggio: %1$@"; "common_poll_total_votes" = "Voti totali: %1$@"; "common_poll_undisclosed_text" = "I risultati verranno mostrati al termine del sondaggio"; "common_privacy_policy" = "Informativa sulla privacy"; @@ -200,6 +207,7 @@ "common_settings" = "Impostazioni"; "common_shared_location" = "Posizione condivisa"; "common_signing_out" = "Disconnessione"; +"common_something_went_wrong" = "Qualcosa è andato storto"; "common_starting_chat" = "Avvio della conversazione..."; "common_sticker" = "Adesivo"; "common_success" = "Operazione riuscita"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Di cosa parla questa stanza?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Impossibile decrittografare"; +"common_unable_to_decrypt_no_access" = "Non hai accesso a questo messaggio"; "common_unable_to_invite_message" = "Non è stato possibile spedire inviti a uno o più utenti."; "common_unable_to_invite_title" = "Impossibile inviare inviti"; "common_unlock" = "Sblocca"; @@ -221,23 +230,30 @@ "common_username" = "Nome utente"; "common_verification_cancelled" = "Verifica annullata"; "common_verification_complete" = "Verifica completata"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verifica dispositivo"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Messaggio vocale"; "common_waiting" = "In attesa…"; "common_waiting_for_decryption_key" = "In attesa del messaggio"; +"common.copied_to_clipboard" = "Copiato negli appunti"; "common.do_not_show_this_again" = "Non mostrarlo più"; "common.open_source_licenses" = "Licenze open source"; -"common.pinned" = "Pinned"; +"common.pinned" = "Fissato"; "common.send_to" = "Invia a"; -"common_no_room_name" = "Nessun nome della stanza"; -"common_poll_end_confirmation" = "Vuoi davvero terminare questo sondaggio?"; -"common_poll_summary" = "Sondaggio: %1$@"; -"common_something_went_wrong" = "Qualcosa è andato storto"; -"common_unable_to_decrypt_no_access" = "Non hai accesso a questo messaggio"; -"common_verify_device" = "Verifica dispositivo"; +"common.you" = "Tu"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Il backup della chat non è attualmente sincronizzato. Devi confermare la chiave di recupero per mantenere l'accesso al backup della chat."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Inserisci la chiave di recupero"; "crash_detection_dialog_content" = "%1$@ si è chiuso inaspettatamente l'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull'arresto anomalo?"; +"crypto_identity_change_pin_violation" = "L'identità di %1$@ sembra essere cambiata. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Per permettere all'applicazione di usare la fotocamera, concedi l'autorizzazione nelle impostazioni di sistema."; "dialog_permission_generic" = "Concedi l'autorizzazione nelle impostazioni di sistema."; "dialog_permission_location_description_ios" = "Concedi l'accesso in Impostazioni -> Condividi la mia posizione."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Faccine & Persone"; "emoji_picker_category_places" = "Viaggi & Luoghi"; "emoji_picker_category_symbols" = "Simboli"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Il tuo homeserver deve essere aggiornato per supportare il Matrix Authentication Service e la creazione di account."; "error_failed_creating_the_permalink" = "Impossibile creare il collegamento permanente"; "error_failed_loading_map" = "%1$@ non è riuscito a caricare la mappa. Riprova più tardi."; "error_failed_loading_messages" = "Caricamento dei messaggi non riuscito"; @@ -269,7 +285,7 @@ "error_some_messages_have_not_been_sent" = "Alcuni messaggi non sono stati inviati"; "error_unknown" = "Siamo spiacenti, si è verificato un errore"; "event_shield_reason_authenticity_not_guaranteed" = "L'autenticità di questo messaggio cifrato non può essere garantita su questo dispositivo."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; +"event_shield_reason_previously_verified" = "Cifrato da un utente precedentemente verificato."; "event_shield_reason_sent_in_clear" = "Non cifrato."; "event_shield_reason_unknown_device" = "Cifrato da un dispositivo sconosciuto o eliminato."; "event_shield_reason_unsigned_device" = "Cifrato da un dispositivo non verificato dal proprietario."; @@ -290,16 +306,15 @@ "notification_channel_silent" = "Notifiche silenziose"; "notification_incoming_call" = "Chiamata in arrivo"; "notification_inline_reply_failed" = "** Invio fallito - si prega di aprire la stanza"; -"notification_invitation_action_reject" = "Rifiuta"; "notification_invite_body" = "Ti ha invitato ad una conversazione"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ ti ha invitato ad una conversazione"; "notification_mentioned_you_body" = "Ti ha menzionato: %1$@"; "notification_new_messages" = "Nuovi messaggi"; "notification_reaction_body" = "Ha reagito con %1$@"; "notification_room_invite_body" = "Ti ha invitato ad entrare nella stanza"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ ti ha invitato a unirti alla stanza"; "notification_sender_me" = "Io"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@ menzionato o risposto"; "notification_test_push_notification_content" = "Stai visualizzando la notifica! Cliccami!"; "notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,31 +344,46 @@ "rich_text_editor_unindent" = "Rientro a sinistra"; "rich_text_editor_url_placeholder" = "Collegamento"; "rich_text_editor_a11y_add_attachment" = "Aggiungi allegato"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "URL base di Element Call personalizzato"; "screen_advanced_settings_element_call_base_url_description" = "Imposta un URL di base personalizzato per Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL non valido, assicurati di includere il protocollo (http/https) e l'indirizzo corretto."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Premi su un messaggio e scegli “%1$@” per includerlo qui."; "screen_pinned_timeline_empty_state_headline" = "Fissa i messaggi importanti così che possano essere trovati facilmente"; -"screen_pinned_timeline_screen_title_empty" = "Messaggi fissati"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; -"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; -"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; -"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; -"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_reset_encryption_password_error" = "Si è verificato un errore sconosciuto. Controlla che la password del tuo account sia corretta e riprova."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Ritira la verifica e invia"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Puoi ritirare la tua verifica e inviare comunque questo messaggio, oppure annullarlo per ora e riprovare più tardi dopo aver riverificato %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "Il tuo messaggio non è stato inviato perché l'identità verificata di %1$@ è cambiata."; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Invia comunque il messaggio"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ sta usando uno o più dispositivi non verificati. Puoi inviare il messaggio in ogni caso, oppure annullarlo e riprovare più tardi quando %2$@ avrà verificato tutti i suoi dispositivi."; +"screen_resolve_send_failure_unsigned_device_title" = "Il tuo messaggio non è stato inviato perché %1$@ non ha verificato tutti i dispositivi."; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Uno o più dispositivi non sono verificati. Puoi inviare il messaggio comunque, oppure annullarlo e riprovare più tardi dopo aver verificato tutti i tuoi dispositivi."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Il tuo messaggio non è stato inviato perché non hai verificato uno o più dispositivi."; "screen_room_mentions_at_room_subtitle" = "Notifica l'intera stanza"; "screen_room_pinned_banner_indicator" = "%1$@ di %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Messaggi fissati"; "screen_room_pinned_banner_loading_description" = "Caricamento messaggio…"; "screen_room_pinned_banner_view_all_button_title" = "Mostra tutti"; "screen_room_details_pinned_events_row_title" = "Messaggi fissati"; -"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Cambia fornitore dell'account"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Messaggio non inviato perché l'identità verificata di %1$@ è cambiata."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Messaggio non inviato perché %1$@ non ha verificato tutti i dispositivi."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Messaggio non inviato perché non hai verificato uno o più dispositivi."; "screen_account_provider_form_hint" = "Indirizzo dell'homeserver"; "screen_account_provider_form_notice" = "Inserisci un termine di ricerca o un indirizzo di dominio."; "screen_account_provider_form_subtitle" = "Cerca un'azienda, una comunità o un server privato."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Stai per creare un account su %@"; "screen_advanced_settings_developer_mode" = "Modalità sviluppatore"; "screen_advanced_settings_developer_mode_description" = "Attiva per avere accesso alle funzionalità per sviluppatori."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Disattiva l'editor di testo avanzato per scrivere manualmente in Markdown"; "screen_advanced_settings_send_read_receipts" = "Ricevute di visualizzazione"; "screen_advanced_settings_send_read_receipts_description" = "Se disattivato, le tue ricevute di visualizzazione non verranno inviate a nessuno. Riceverai comunque ricevute di visualizzazione da altri utenti."; @@ -430,12 +462,14 @@ "screen_chat_backup_key_backup_action_enable" = "Attiva il backup"; "screen_chat_backup_key_backup_description" = "Il backup ti garantisce di non perdere la cronologia dei messaggi. %1$@."; "screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Cambia la chiave di recupero"; -"screen_chat_backup_recovery_action_confirm" = "Conferma la chiave di recupero"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Il backup delle conversazioni non è attualmente sincronizzato."; -"screen_chat_backup_recovery_action_setup" = "Configura il recupero"; "screen_chat_backup_recovery_action_setup_description" = "Ottieni l'accesso ai tuoi messaggi cifrati se perdi tutti i tuoi dispositivi o se sei disconnesso da %1$@ ovunque."; -"screen_create_account_title" = "Create account"; +"screen_create_account_title" = "Crea account"; "screen_create_new_recovery_key_list_item_1" = "Apri %1$@ in un dispositivo desktop"; "screen_create_new_recovery_key_list_item_2" = "Accedi nuovamente al tuo account"; "screen_create_new_recovery_key_list_item_3" = "Quando ti viene chiesto di verificare il tuo dispositivo, seleziona %1$@"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Mostra i risultati solo al termine del sondaggio"; "screen_create_poll_anonymous_headline" = "Nascondi voti"; "screen_create_poll_answer_hint" = "Opzione %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Le tue modifiche non verranno salvate"; "screen_create_poll_cancel_confirmation_title_ios" = "Annulla sondaggio"; "screen_create_poll_question_desc" = "Domanda o argomento"; "screen_create_poll_question_hint" = "Di cosa parla il sondaggio?"; @@ -459,17 +492,17 @@ "screen_create_room_public_option_description" = "I messaggi non sono cifrati e chiunque può leggerli. Puoi attivare la crittografia in un secondo momento."; "screen_create_room_public_option_title" = "Stanza pubblica (chiunque)"; "screen_create_room_topic_label" = "Argomento (facoltativo)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Conferma di voler disattivare il tuo account. Questa azione è irreversibile."; +"screen_deactivate_account_delete_all_messages" = "Elimina tutti i miei messaggi"; +"screen_deactivate_account_delete_all_messages_notice" = "Attenzione: gli utenti futuri potrebbero vedere conversazioni incomplete."; +"screen_deactivate_account_description" = "La disattivazione del tuo account è %1$@ , quindi:"; +"screen_deactivate_account_description_bold_part" = "irreversibile"; +"screen_deactivate_account_list_item_1" = "%1$@ il tuo account (non puoi riaccedere e il tuo ID non può essere riutilizzato)."; +"screen_deactivate_account_list_item_1_bold_part" = "Disattiva permanentemente"; +"screen_deactivate_account_list_item_2" = "Ti rimuove da tutte le stanze di chat."; +"screen_deactivate_account_list_item_3" = "Elimina le informazioni del tuo account dal nostro server di identità."; +"screen_deactivate_account_list_item_4" = "I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli."; +"screen_deactivate_account_title" = "Disattivazione dell'account"; "screen_edit_poll_delete_confirmation" = "Vuoi davvero eliminare questo sondaggio?"; "screen_edit_profile_display_name" = "Nome visualizzato"; "screen_edit_profile_display_name_placeholder" = "Il tuo nome visualizzato"; @@ -477,7 +510,7 @@ "screen_edit_profile_error_title" = "Impossibile aggiornare il profilo"; "screen_edit_profile_title" = "Modifica profilo"; "screen_edit_profile_updating_details" = "Aggiornamento del profilo…"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; +"screen_encryption_reset_action_continue_reset" = "Continua il ripristino"; "screen_encryption_reset_bullet_1" = "I dettagli del tuo account, i contatti, le preferenze e l'elenco delle conversazioni verranno conservati"; "screen_encryption_reset_bullet_2" = "Perderai la cronologia dei messaggi esistente"; "screen_encryption_reset_bullet_3" = "Dovrai verificare nuovamente tutti i dispositivi e i contatti esistenti"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Chat di gruppo"; "screen_notification_settings_invite_for_me_label" = "Inviti"; "screen_notification_settings_mentions_only_disclaimer" = "Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."; -"screen_notification_settings_mentions_section_title" = "Menzioni"; "screen_notification_settings_mode_all" = "Tutto"; "screen_notification_settings_mode_mentions" = "Menzioni"; "screen_notification_settings_notification_section_title" = "Avvisami per"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Seleziona %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "\"Collega un nuovo dispositivo\""; "screen_qr_code_login_initial_state_item_4" = "Scansiona il codice QR con questo dispositivo"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Apri %1$@ su un altro dispositivo per ottenere il codice QR"; "screen_qr_code_login_invalid_scan_state_description" = "Usa il codice QR mostrato sull'altro dispositivo."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Codice QR sbagliato"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Il tuo codice di verifica"; "screen_recovery_key_change_description" = "Ottieni una nuova chiave di recupero se hai perso quella esistente. Dopo averla cambiata, quella vecchia non funzionerà più."; "screen_recovery_key_change_generate_key" = "Genera una nuova chiave di recupero"; -"screen_recovery_key_change_generate_key_description" = "Assicurati di conservare la chiave di recupero in un posto sicuro"; "screen_recovery_key_change_success" = "Chiave di recupero cambiata"; "screen_recovery_key_change_title" = "Cambiare la chiave di recupero?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Crea una nuova chiave di recupero"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Inserisci..."; "screen_recovery_key_confirm_lost_recovery_key" = "Hai perso la chiave di recupero?"; "screen_recovery_key_confirm_success" = "Chiave di recupero confermata"; -"screen_recovery_key_confirm_title" = "Inserisci la tua chiave di recupero"; "screen_recovery_key_copied_to_clipboard" = "Chiave di recupero copiata"; "screen_recovery_key_generating_key" = "Generazione…"; "screen_recovery_key_save_action" = "Salva la chiave di recupero"; @@ -636,11 +667,10 @@ "screen_reset_encryption_confirmation_alert_action" = "Sì, reimposta ora"; "screen_reset_encryption_confirmation_alert_subtitle" = "Questo processo è irreversibile."; "screen_reset_encryption_confirmation_alert_title" = "Sei sicuro di voler reimpostare la crittografia?"; -"screen_reset_encryption_password_placeholder" = "Inserisci…"; "screen_reset_encryption_password_subtitle" = "Conferma di voler reimpostare la crittografia."; "screen_reset_encryption_password_title" = "Inserisci la password del tuo account per continuare"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_identity_confirmation_subtitle" = "Stai per accedere al tuo account di %1$@ per ripristinare la tua identità. Dopodiché verrai riportato all'app."; +"screen_reset_identity_confirmation_title" = "Non riesci a confermare? Vai al tuo account per ripristinare la tua identità."; "screen_room_alias_resolver_resolve_alias_failure" = "Impossibile risolvere l'alias della stanza."; "screen_room_attachment_source_camera" = "Fotocamera"; "screen_room_attachment_source_camera_video" = "Registra video"; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Gli amministratori hanno automaticamente i privilegi di moderatore"; "screen_room_change_role_moderators_title" = "Modifica moderatori"; "screen_room_change_role_unsaved_changes_description" = "Hai delle modifiche non salvate."; -"screen_room_change_role_unsaved_changes_title" = "Salvare le modifiche?"; "screen_room_details_add_topic_title" = "Aggiungi argomento"; "screen_room_details_already_a_member" = "Già membro"; "screen_room_details_already_invited" = "Già invitato"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Disattivazione del silenzioso di questa stanza fallita, riprova."; "screen_room_details_notification_mode_custom" = "Personalizzato"; "screen_room_details_notification_mode_default" = "Predefinito"; -"screen_room_details_notification_title" = "Notifiche"; "screen_room_details_share_room_title" = "Condividi stanza"; "screen_room_details_title" = "Informazioni sulla stanza"; "screen_room_details_updating_room" = "Aggiornamento della stanza…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Sblocca"; "screen_room_member_details_unblock_alert_description" = "Potrai vedere di nuovo tutti i suoi messaggi."; "screen_room_member_details_unblock_user" = "Sblocca utente"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Escludi"; "screen_room_member_list_ban_member_confirmation_description" = "Non potrà entrare nuovamente in questa stanza se invitato."; "screen_room_member_list_ban_member_confirmation_title" = "Vuoi davvero escludere questo membro?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Esclusione di %1$@"; "screen_room_member_list_manage_member_ban" = "Rimuovi ed escludi"; "screen_room_member_list_manage_member_remove" = "Rimuovi dalla stanza"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Rimuovi ed escludi"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Rimuovi soltanto"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Rimuovere e vietare l'accesso in futuro?"; "screen_room_member_list_manage_member_unban_action" = "Riammetti"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Mostra meno"; "screen_room_timeline_message_copied" = "Messaggio copiato"; "screen_room_timeline_no_permission_to_post" = "Non sei autorizzato a postare in questa stanza"; -"screen_room_timeline_reactions_show_less" = "Mostra meno"; "screen_room_timeline_reactions_show_more" = "Mostra di più"; "screen_room_timeline_read_marker_title" = "Nuovo"; "screen_room_title" = "Conversazione"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Segna come letto"; "screen_roomlist_mark_as_unread" = "Segna come non letto"; "screen_roomlist_room_directory_button_title" = "Sfoglia tutte le stanze"; -"screen_server_confirmation_change_server" = "Cambia fornitore dell'account"; "screen_server_confirmation_message_login_element_dot_io" = "Un server privato per i dipendenti di Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix è una rete aperta per comunicazioni sicure e decentralizzate."; "screen_server_confirmation_message_register" = "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Confronta i numeri"; "screen_session_verification_complete_subtitle" = "La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."; "screen_session_verification_enter_recovery_key" = "Inserisci la chiave di recupero"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Dimostra la tua identità per accedere alla cronologia dei messaggi crittografati."; "screen_session_verification_open_existing_session_title" = "Apri una sessione esistente"; "screen_session_verification_positive_button_canceled" = "Riprova la verifica"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "In attesa di un riscontro"; "screen_session_verification_ready_subtitle" = "Confronta un set unico di emoji."; "screen_session_verification_request_accepted_subtitle" = "Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Non corrispondono"; "screen_session_verification_they_match" = "Corrispondono"; "screen_session_verification_waiting_to_accept_subtitle" = "Accetta la richiesta di avviare il processo di verifica nell'altra sessione per continuare."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l'accesso ai tuoi messaggi cifrati."; "screen_signout_key_backup_disabled_title" = "Hai disattivato il backup"; "screen_signout_key_backup_offline_subtitle" = "Il backup delle chiavi era ancora in corso quando sei andato offline. Riconnettiti per eseguire il backup delle chiavi prima di uscire."; -"screen_signout_key_backup_offline_title" = "Il backup delle chiavi è ancora in corso"; "screen_signout_key_backup_ongoing_subtitle" = "Attendi il completamento dell'operazione prima di uscire."; "screen_signout_key_backup_ongoing_title" = "Il backup delle chiavi è ancora in corso"; "screen_signout_recovery_disabled_subtitle" = "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l'accesso ai tuoi messaggi cifrati."; "screen_signout_recovery_disabled_title" = "Recupero non impostato"; "screen_signout_save_recovery_key_subtitle" = "Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l'accesso ai tuoi messaggi cifrati."; -"screen_signout_save_recovery_key_title" = "Hai salvato la chiave di recupero?"; "screen_start_chat_error_starting_chat" = "Si è verificato un errore durante il tentativo di avviare una chat"; "screen_view_location_title" = "Posizione"; "screen_welcome_bullet_1" = "Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell'anno."; @@ -919,7 +952,6 @@ "test_language_identifier" = "it"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Risoluzione dei problemi"; -"troubleshoot_notifications_entry_point_title" = "Risoluzione di problemi delle notifiche"; "troubleshoot_notifications_screen_action" = "Esegui i test"; "troubleshoot_notifications_screen_action_again" = "Esegui nuovamente i test"; "troubleshoot_notifications_screen_failure" = "Alcuni test sono falliti. Si prega di controllare i dettagli."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Assicurati che i distributori UnifiedPush siano disponibili."; "troubleshoot_notifications_test_unified_push_failure" = "Nessun distributore di notifiche push trovato."; "troubleshoot_notifications_test_unified_push_title" = "Controlla UnifiedPush"; +"a11y_poll" = "Sondaggio"; +"banner_set_up_recovery_submit" = "Configura il recupero"; "dialog_title_error" = "Errore"; "dialog_title_success" = "Operazione riuscita"; "notification_fallback_content" = "Notifica"; "notification_invitation_action_join" = "Entra"; +"notification_invitation_action_reject" = "Rifiuta"; "notification_room_action_mark_as_read" = "Segna come letto"; "notification_room_action_quick_reply" = "Risposta rapida"; +"screen_pinned_timeline_screen_title_empty" = "Messaggi fissati"; "screen_room_mentions_at_room_title" = "Tutti"; +"screen_account_provider_change" = "Cambia fornitore dell'account"; "screen_account_provider_signin_subtitle" = "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email."; "screen_account_provider_signup_subtitle" = "Qui è dove vivranno le tue conversazioni — proprio come useresti un fornitore di posta elettronica per conservare le tue email."; "screen_analytics_settings_help_us_improve" = "Condividi dati di utilizzo anonimi per aiutarci a identificare problemi."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Potrai vedere di nuovo tutti i suoi messaggi."; "screen_blocked_users_unblock_alert_title" = "Sblocca utente"; "screen_bug_report_rash_logs_alert_title" = "%1$@ si è chiuso inaspettatamente l'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull'arresto anomalo?"; +"screen_chat_backup_recovery_action_confirm" = "Inserisci la chiave di recupero"; +"screen_chat_backup_recovery_action_setup" = "Configura il recupero"; +"screen_create_poll_cancel_confirmation_content_ios" = "Le modifiche non verranno salvate"; "screen_create_room_add_people_title" = "Invita persone"; "screen_create_room_room_name_label" = "Nome stanza"; "screen_create_room_title" = "Crea una stanza"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Modifica sondaggio"; "screen_identity_use_another_device" = "Usa un altro dispositivo"; "screen_login_subtitle" = "Matrix è una rete aperta per comunicazioni sicure e decentralizzate."; +"screen_notification_settings_mentions_section_title" = "Menzioni"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Riprova"; +"screen_recovery_key_change_generate_key_description" = "Assicurati di conservare la chiave di recupero in un posto sicuro"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Blocca utente"; +"screen_reset_encryption_password_placeholder" = "Inserisci..."; "screen_room_attachment_source_camera_photo" = "Scatta foto"; "screen_room_change_permissions_everyone" = "Tutti"; "screen_room_change_permissions_member_moderation" = "Moderazione dei membri"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Amministratori"; "screen_room_change_role_section_moderators" = "Moderatori"; "screen_room_change_role_section_users" = "Membri"; +"screen_room_change_role_unsaved_changes_title" = "Salvare le modifiche?"; "screen_room_details_invite_people_title" = "Invita persone"; "screen_room_details_leave_conversation_title" = "Abbandona la conversazione"; "screen_room_details_leave_room_title" = "Esci dalla stanza"; +"screen_room_details_notification_title" = "Notifiche"; "screen_room_details_roles_and_permissions" = "Ruoli e autorizzazioni"; "screen_room_details_room_name_label" = "Nome stanza"; "screen_room_details_security_title" = "Sicurezza"; "screen_room_details_topic_title" = "Argomento"; "screen_room_error_failed_processing_media" = "Elaborazione del file multimediale da caricare fallita, riprova."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Rimuovi ed escludi"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Solo menzioni e parole chiave"; +"screen_room_timeline_reactions_show_less" = "Mostra meno"; "screen_roomlist_filter_people" = "Persone"; +"screen_server_confirmation_change_server" = "Cambia fornitore dell'account"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Disconnetti"; "screen_signout_confirmation_dialog_title" = "Disconnetti"; +"screen_signout_key_backup_offline_title" = "Il backup delle chiavi è ancora in corso"; "screen_signout_preference_item" = "Disconnetti"; +"screen_signout_save_recovery_key_title" = "Hai salvato la chiave di recupero?"; +"troubleshoot_notifications_entry_point_title" = "Risoluzione di problemi delle notifiche"; diff --git a/ElementX/Resources/Localizations/ka.lproj/Localizable.strings b/ElementX/Resources/Localizations/ka.lproj/Localizable.strings index c8c3acb277..33314f91c3 100644 --- a/ElementX/Resources/Localizations/ka.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ka.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "პაუზა"; "a11y_pin_field" = "PIN ველი"; "a11y_play" = "დაკვრა"; -"a11y_poll" = "გამოკითხვა"; "a11y_poll_end" = "დასრულდა გამოკითხვა"; "a11y_react_with" = "რეაგირება %1$@-ით"; "a11y_react_with_other_emojis" = "რეაგირება სხვა ემოჯით"; @@ -41,6 +40,7 @@ "action_create" = "შექმნა"; "action_create_a_room" = "ოთახის შექმნა"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "უარყოფა"; "action_delete_poll" = "გამოკითხვის წაშლა"; "action_disable" = "გამორთვა"; @@ -54,6 +54,7 @@ "action_forgot_password" = "დაგავიწყდათ პაროლი?"; "action_forward" = "გადაგზავნა"; "action_go_back" = "Go back"; +"action_ignore" = "Ignore"; "action_invite" = "მოწვევა"; "action_invite_friends" = "ხალხის მოწვევა"; "action_invite_friends_to_app" = "ადამიანების დამატება %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "დატოვება"; "action_leave_conversation" = "Leave conversation"; "action_leave_room" = "ოთახის დატოვება"; +"action_load_more" = "მეტის ჩატვირთვა"; "action_manage_account" = "ანგარიშის მართვა"; "action_manage_devices" = "მოწყობილობების მართვა"; "action_message" = "Message"; @@ -93,6 +95,7 @@ "action_send_message" = "შეტყობინების გაგზავნა"; "action_share" = "გაზიარება"; "action_share_link" = "ბმულის გაზიარება"; +"action_show" = "Show"; "action_sign_in_again" = "ხელახლა შედით"; "action_signout" = "გამოსვლა"; "action_signout_anyway" = "მაინც გასვლა"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "წყაროს ნახვა"; "action_yes" = "დიახ"; -"action.load_more" = "მეტის ჩატვირთვა"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "შესახებ"; "common_acceptable_use_policy" = "მისაღები გამოყენების პოლიტიკა"; "common_advanced_settings" = "გაფართოებული პარამეტრები"; @@ -133,10 +134,12 @@ "common_dark" = "მუქი"; "common_decryption_error" = "გაშიფვრის შეცდომა"; "common_developer_options" = "დეველოპერის პარამეტრები"; +"common_device_id" = "Device ID"; "common_direct_chat" = "პირდაპირი ჩატი"; "common_edited_suffix" = "(რედაქტირებულია)"; "common_editing" = "რედაქტირება"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "დაშიფვრა ჩართულია"; "common_enter_your_pin" = "შეიყვანეთ თქვენი PIN"; "common_error" = "შეცდომა"; @@ -147,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "ფაილი"; "common_forward_message" = "შეტყობინების გადაგზავნა"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "სურათი"; "common_in_reply_to" = "%1$@-ს პასუხად"; @@ -162,6 +166,7 @@ "common_modern" = "თანამედროვე"; "common_mute" = "დადუმება"; "common_no_results" = "შედეგი არ არის"; +"common_no_room_name" = "No room name"; "common_offline" = "ხაზგარეშე"; "common_optic_id_ios" = "ოპტიკური ID"; "common_or" = "or"; @@ -170,6 +175,8 @@ "common_permalink" = "მუდმივი ბმული"; "common_permission" = "ნებართვა"; "common_please_wait" = "Please wait…"; +"common_poll_end_confirmation" = "დარწმუნებული ხართ, რომ გსურთ ამ გამოკითხვის დასრულება?"; +"common_poll_summary" = "გამოკითხვა: %1$@"; "common_poll_total_votes" = "სულ ხმები: %1$@"; "common_poll_undisclosed_text" = "შედეგები გამოკითხვის დასრულების შემდეგ გამოჩნდება"; "common_privacy_policy" = "კონფიდენციალურობის პოლიტიკა"; @@ -200,6 +207,7 @@ "common_settings" = "პარამეტრები"; "common_shared_location" = "გაზიარებული მდებარეობა"; "common_signing_out" = "გასვლა…"; +"common_something_went_wrong" = "Something went wrong"; "common_starting_chat" = "ჩატის დაწყება..."; "common_sticker" = "სტიკერი"; "common_success" = "წარმატება"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "რა თემებს ეხება ეს ოთახი?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "გაშიფვრა ვერ მოხერხდა"; +"common_unable_to_decrypt_no_access" = "You don't have access to this message"; "common_unable_to_invite_message" = "მოსაწვევები ვერ გაეგზავნა ერთ ან მეტ მომხმარებელს."; "common_unable_to_invite_title" = "მოწვევის (ების) გაგზავნა შეუძლებელია"; "common_unlock" = "განბლოკვა"; @@ -221,23 +230,30 @@ "common_username" = "მომხმარებლის სახელი"; "common_verification_cancelled" = "დადასტურება გაუქმდა"; "common_verification_complete" = "დადასტურება დასრულებულია"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "დაადასტურეთ მოწყობილობა"; +"common_verify_identity" = "Verify identity"; "common_video" = "ვიდეო"; "common_voice_message" = "ხმოვანი შეტყობინება"; "common_waiting" = "მოცდა..."; "common_waiting_for_decryption_key" = "ლოდინი ამ შეტყობინებისათვის"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Send to"; -"common_no_room_name" = "No room name"; -"common_poll_end_confirmation" = "დარწმუნებული ხართ, რომ გსურთ ამ გამოკითხვის დასრულება?"; -"common_poll_summary" = "გამოკითხვა: %1$@"; -"common_something_went_wrong" = "Something went wrong"; -"common_unable_to_decrypt_no_access" = "You don't have access to this message"; -"common_verify_device" = "დაადასტურეთ მოწყობილობა"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "თქვენი ჩეთების სარეზერვო ასლი ამჟამად არ არის სინქრონიზებული. თქვენ უნდა შეიყვანოთ თქვენი აღდგენის გასაღები, რათა შეინარჩუნოთ წვდომა ჩეთების სარეზერვო ასლზე."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "შეიყვანეთ აღდგენის გასაღები"; "crash_detection_dialog_content" = "%1$@ ავარიულად გაითიშა ბოლოს გამოიყენებისას. გსურთ, გამოგვიგზავნოთ ავარიული გათიშვის ჟურნალი?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "იმისათვის, რომ აპლიკაციამ გამოიყენოს კამერა, გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."; "dialog_permission_generic" = "გთხოვთ, მიანიჭოთ ნებართვა სისტემის პარამეტრებში."; "dialog_permission_location_description_ios" = "მიანიჭეთ წვდომა პარამეტრებში -> ლოკაციაში."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "ჩუმი შეტყობინებები"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** გაგზავნა ვერ მოხერხდა - გთხოვთ, გახსნათ ოთახი"; -"notification_invitation_action_reject" = "უარყოფა"; "notification_invite_body" = "მოგიწვიათ ჩატში"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "მოგახსენათ: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "აბზაცის გარეშე"; "rich_text_editor_url_placeholder" = "Ბმული"; "rich_text_editor_a11y_add_attachment" = "დაამატეთ დანართი"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "მორგებული Element-ის ზარის საბაზისო URL"; "screen_advanced_settings_element_call_base_url_description" = "დააყენეთ საბაზისო URL Element-ის ზარებისათვის."; "screen_advanced_settings_element_call_base_url_validation_error" = "არასწორი URL, გთხოვთ, დარწმუნდეთ, რომ შეიტანეთ პროტოკოლი (http/https) და სწორი მისამართი."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "ანგარიშის მიმწოდებლის შეცვლა"; "screen_account_provider_form_hint" = "სახლის სერვერის მისამართი"; "screen_account_provider_form_notice" = "შეიყვანეთ საძიებო სიტყვა ან დომენის მისამართი."; "screen_account_provider_form_subtitle" = "მოძებნეთ კომპანია, საზოგადოება ან კერძო სერვერი."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "თქვენ აპირებთ ანგარიშის შექმნას %@-ში"; "screen_advanced_settings_developer_mode" = "დეველოპერის რეჟიმი"; "screen_advanced_settings_developer_mode_description" = "ჩართეთ დეველოპერების ფუნქციებზე წვდომა."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "გამორთეთ მდიდარი ტექსტის რედაქტორი, რათა ხელით აკრიფოთ Markdown."; "screen_advanced_settings_send_read_receipts" = "Read receipts"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -429,11 +461,13 @@ "screen_chat_backup_key_backup_action_disable" = "სარეზერვო ასლის გამორთვა"; "screen_chat_backup_key_backup_action_enable" = "სარეზერვო ასლის ჩართვა"; "screen_chat_backup_key_backup_description" = "სარეზერვო ასლი უზრუნველყოფს იმას, რომ თქვენ შეტყობინებების ისტორიას არ დაკარგავთ. %1$@"; -"screen_chat_backup_key_backup_title" = "სარეზერვო ასლი"; +"screen_chat_backup_key_backup_title" = "გასაღების საცავი"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "აღდგენის გასაღების შეცვლა"; -"screen_chat_backup_recovery_action_confirm" = "შეიყვანეთ აღდგენის გასაღები"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "თქვენი ჩატის სარეზერვო ასლი ამჟამად არ არის სინქრონიზებული."; -"screen_chat_backup_recovery_action_setup" = "აღდგენის დაყენება"; "screen_chat_backup_recovery_action_setup_description" = "მიიღეთ წვდომა თქვენს დაშიფრულ შეტყობინებებზე, თუ დაკარგავთ თქვენს ყველა მოწყობილობას ან გამოხვალთ სისტემიდან %1$@-დან ყველგან."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "შედეგების ჩვენება მხოლოდ გამოკითხვის დასრულების შემდეგ"; "screen_create_poll_anonymous_headline" = "ხმების დამალვა"; "screen_create_poll_answer_hint" = "ვარიანტი %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "თქვენი ცვლილებები არ შეინახება"; "screen_create_poll_cancel_confirmation_title_ios" = "გამოკითხვის გაუქმება"; "screen_create_poll_question_desc" = "კითხვა ან თემა"; "screen_create_poll_question_hint" = "რას ეხება გამოკითხვა?"; @@ -455,9 +488,9 @@ "screen_create_room_action_create_room" = "ახალი ოთახი"; "screen_create_room_error_creating_room" = "ოთახის შექმნისას შეცდომა მოხდა"; "screen_create_room_private_option_description" = "ამ ოთახში შეტყობინებები დაშიფრულია. შემდგომ დაშიფვრის გამორთვა შეუძლებელია."; -"screen_create_room_private_option_title" = "კერძო ოთახი (მხოლოდ მოწვევა)"; -"screen_create_room_public_option_description" = "შეტყობინებები არ არის დაშიფრული და ყველას შეუძლია მათი წაკითხვა. შეგიძლიათ ჩართოთ დაშიფვრა მოგვიანებით."; -"screen_create_room_public_option_title" = "საჯარო ოთახი (ნებისმიერი)"; +"screen_create_room_private_option_title" = "კერძო ოთახი"; +"screen_create_room_public_option_description" = "ყველას ამ ოთახის მოძებნა შეუძლია.\nთქვენ ნებისმიერ დროს შეგიძლიათ ამის შეცვლა ოთახის პარამეტრებში."; +"screen_create_room_public_option_title" = "საჯარო ოთახი"; "screen_create_room_topic_label" = "თემა (სურვილისამებრ)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; "screen_deactivate_account_delete_all_messages" = "Delete all my messages"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "პროფილის განახლება..."; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "მოწვევები არ არის"; "screen_invites_invited_you" = "%1$@ (%2$@) მოგიწვიათ"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "ჯგუფური ჩატები"; "screen_notification_settings_invite_for_me_label" = "მოსაწვევები"; "screen_notification_settings_mentions_only_disclaimer" = "თქვენი სახლის სერვერი არ უჭერს მხარს ამ პარამეტრს დაშიფრულ ოთახებში, ზოგიერთ ოთახში შეიძლება არ მიიღოთ შეტყობინება."; -"screen_notification_settings_mentions_section_title" = "ხსენებები"; "screen_notification_settings_mode_all" = "ყველა"; "screen_notification_settings_mode_mentions" = "ხსენებები"; "screen_notification_settings_notification_section_title" = "ჩემი შეტყობინება შემდეგისთვის:"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "მიიღეთ ახალი აღდგენის გასაღები, თუ დაკარგეთ არსებული. აღდგენის გასაღების შეცვლის შემდეგ, ძველი აღარ იმუშავებს."; "screen_recovery_key_change_generate_key" = "ახალი აღდგენის გასაღების შექმნა"; -"screen_recovery_key_change_generate_key_description" = "დარწმუნდით, რომ შეგიძლიათ შეინახოთ თქვენი აღდგენის გასაღები სადმე უსაფრთხო ადგილას"; "screen_recovery_key_change_success" = "აღდგენის გასაღები შეიცვალა"; "screen_recovery_key_change_title" = "გსურთ აღდგენის გასაღების შეცვლა?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "შეყვანა"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "აღდგენის გასაღები დადასტურებულია"; -"screen_recovery_key_confirm_title" = "შეიყვანეთ თქვენი აღდგენის გასაღები"; "screen_recovery_key_copied_to_clipboard" = "დაკოპირებულია აღდგენის გასაღები"; "screen_recovery_key_generating_key" = "გენერირება..."; "screen_recovery_key_save_action" = "აღდგენის გასაღების შენახვა"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Edit Moderators"; "screen_room_change_role_unsaved_changes_description" = "You have unsaved changes."; -"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_add_topic_title" = "თემის დამატება"; "screen_room_details_already_a_member" = "უკვე წევრია"; "screen_room_details_already_invited" = "უკვე მოწვეულია"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "ამ ოთახის დადუმების მოხსნა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."; "screen_room_details_notification_mode_custom" = "მორგებული"; "screen_room_details_notification_mode_default" = "ნაგულისხმევი"; -"screen_room_details_notification_title" = "შეტყობინებები"; "screen_room_details_share_room_title" = "ოთახის გაზიარება"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "ოთახის განახლება..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "განბლოკვა"; "screen_room_member_details_unblock_alert_description" = "თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."; "screen_room_member_details_unblock_user" = "Მომხმარებლის განბლოკვა"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Ban"; "screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; "screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banning %1$@"; "screen_room_member_list_manage_member_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove" = "Remove from room"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; "screen_room_member_list_manage_member_unban_action" = "Unban"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "ნაკლების ჩვენება"; "screen_room_timeline_message_copied" = "შეტყობინება დაკოპირდა"; "screen_room_timeline_no_permission_to_post" = "თქვენ არ გაქვთ ამ ოთახში გამოქვეყნების ნებართვა"; -"screen_room_timeline_reactions_show_less" = "ნაკლების ჩვენება"; "screen_room_timeline_reactions_show_more" = "მეტის ჩვენება"; "screen_room_timeline_read_marker_title" = "ახალი"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Mark as read"; "screen_roomlist_mark_as_unread" = "Mark as unread"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "შეცვალეთ ანგარიშის მომწოდებელი"; "screen_server_confirmation_message_login_element_dot_io" = "კერძო სერვერი Element-ის თანამშრომლებისთვის."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."; "screen_server_confirmation_message_register" = "აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "შეადარეთ რიცხვები"; "screen_session_verification_complete_subtitle" = "თქვენი ახალი სესია დადასტურებულია. მას აქვს წვდომა დაშიფრულ შეტყობინებებზე და სხვა მომხმარებლები მას სანდოდ ხედავენ."; "screen_session_verification_enter_recovery_key" = "Enter recovery key"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "დაამტკიცეთ, რომ ეს თქვენ ხართ, რათა მიიღოთ წვდომა თქვენი დაშიფრული შეტყობინებების ისტორიასთან."; "screen_session_verification_open_existing_session_title" = "არსებული სესიის გახსნა"; "screen_session_verification_positive_button_canceled" = "დადასტურების ხელახლა ცდა"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "ველოდებით დამთხვევას"; "screen_session_verification_ready_subtitle" = "შეადარეთ ემოციების უნიკალური ნაკრები."; "screen_session_verification_request_accepted_subtitle" = "შეადარეთ უნიკალური ემოჯი, დარწმუნდით, რომ ისინი ერთი დ იმავე თანმიმდევრობით გამოჩნდნენ."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "ისინი არ ემთხვევიან ერთმანეთს"; "screen_session_verification_they_match" = "ისინი ემთხვევიან ერთმანეთს"; "screen_session_verification_waiting_to_accept_subtitle" = "მიიღეთ დადასტურების მოთხოვნა თქვენს სხვა სესიაში ამ პროცესის გასაგრძელებლად."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."; "screen_signout_key_backup_disabled_title" = "თქვენ გამორთეთ სარეზერვო ასლი"; "screen_signout_key_backup_offline_subtitle" = "თქვენი გასაღებების სარეზერვო ასლის შექმნა მიმდინარეობდა იმ დროს, როდესაც გამოხვედით. დაკავშირდით ისევ ისე, რომ სარეზერვო ასლი შეიქმნას ანგარიშიდან გამოსვლის გარეშე."; -"screen_signout_key_backup_offline_title" = "თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"; "screen_signout_key_backup_ongoing_subtitle" = "გთხოვთ დაელოდეთ ამის დასრულებას სისტემიდან გამოსვლამდე."; "screen_signout_key_backup_ongoing_title" = "თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"; "screen_signout_recovery_disabled_subtitle" = "თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, დაკარგავთ წვდომას თქვენს დაშიფრულ შეტყობინებებზე."; "screen_signout_recovery_disabled_title" = "აღდგენა არ არის დაყენებული"; "screen_signout_save_recovery_key_subtitle" = "თქვენ აპირებთ გასვლას თქვენი ბოლო სესიიდან. თუ ახლა გამოხვალთ, შესაძლოა დაკარგოთ წვდომა თქვენს დაშიფრულ შეტყობინებებზე."; -"screen_signout_save_recovery_key_title" = "შეინახეთ თქვენი აღდგენის გასაღები?"; "screen_start_chat_error_starting_chat" = "ჩატის დაწყების მცდელობისას შეცდომა მოხდა"; "screen_view_location_title" = "ადგილმდებარეობა"; "screen_welcome_bullet_1" = "ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."; @@ -919,7 +952,6 @@ "test_language_identifier" = "ka"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "გამოკითხვა"; +"banner_set_up_recovery_submit" = "აღდგენის დაყენება"; "dialog_title_error" = "შეცდომა"; "dialog_title_success" = "წარმატება"; "notification_fallback_content" = "შეტყობინება"; "notification_invitation_action_join" = "გაწევრიანება"; +"notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Mark as read"; "notification_room_action_quick_reply" = "Სწრაფი პასუხი"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "ყველა"; +"screen_account_provider_change" = "შეცვალეთ ანგარიშის მომწოდებელი"; "screen_account_provider_signin_subtitle" = "აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."; "screen_account_provider_signup_subtitle" = "აქ იქნება თქვენი საუბრები - ისევე, როგორც თქვენ ელ. ფოსტაში ინახება თქვენი ელ.წერილები."; "screen_analytics_settings_help_us_improve" = "გააზიარეთ ანონიმური გამოყენების მონაცემები, რათა დაგვეხმაროთ პრობლემების იდენტიფიცირებაში."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."; "screen_blocked_users_unblock_alert_title" = "Მომხმარებლის განბლოკვა"; "screen_bug_report_rash_logs_alert_title" = "%1$@ ავარიულად გაითიშა ბოლოს გამოიყენებისას. გსურთ, გამოგვიგზავნოთ ავარიული გათიშვის ჟურნალი?"; +"screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; +"screen_chat_backup_recovery_action_setup" = "აღდგენის დაყენება"; +"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "ხალხის მოწვევა"; "screen_create_room_room_name_label" = "ოთახის სახელი"; "screen_create_room_title" = "ოთახის შექმნა"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "გამოკითხვის რედაქტირება"; "screen_identity_use_another_device" = "Use another device"; "screen_login_subtitle" = "Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის."; +"screen_notification_settings_mentions_section_title" = "ხსენებები"; "screen_qr_code_login_invalid_scan_state_retry_button" = "ხელახლა ცდა"; +"screen_recovery_key_change_generate_key_description" = "დარწმუნდით, რომ შეგიძლიათ შეინახოთ თქვენი აღდგენის გასაღები სადმე უსაფრთხო ადგილას"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "მომხმარებლის დაბლოკვა"; +"screen_reset_encryption_password_placeholder" = "შეყვანა"; "screen_room_attachment_source_camera_photo" = "ფოტოს გადაღება"; "screen_room_change_permissions_everyone" = "ყველა"; "screen_room_change_permissions_member_moderation" = "Member moderation"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Admins"; "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Members"; +"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_invite_people_title" = "ხალხის მოწვევა"; "screen_room_details_leave_conversation_title" = "Leave conversation"; "screen_room_details_leave_room_title" = "ოთახის დატოვება"; +"screen_room_details_notification_title" = "შეტყობინებები"; "screen_room_details_roles_and_permissions" = "Roles and permissions"; "screen_room_details_room_name_label" = "ოთახის სახელი"; "screen_room_details_security_title" = "უსაფრთხოება"; "screen_room_details_topic_title" = "თემა"; "screen_room_error_failed_processing_media" = "მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_notification_settings_mode_mentions_and_keywords" = "მხოლოდ ხსენებები და საკვანძო სიტყვები"; +"screen_room_timeline_reactions_show_less" = "ნაკლების ჩვენება"; "screen_roomlist_filter_people" = "ხალხი"; +"screen_server_confirmation_change_server" = "შეცვალეთ ანგარიშის მომწოდებელი"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "გამოსვლა"; "screen_signout_confirmation_dialog_title" = "გამოსვლა"; +"screen_signout_key_backup_offline_title" = "თქვენი გასაღებების სარეზერვო ასლი ჯერ კიდევ შექმნის პროცესშია"; "screen_signout_preference_item" = "გამოსვლა"; +"screen_signout_save_recovery_key_title" = "შეინახეთ თქვენი აღდგენის გასაღები?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/nl.lproj/Localizable.strings b/ElementX/Resources/Localizations/nl.lproj/Localizable.strings index b340743bc5..a149e82a7b 100644 --- a/ElementX/Resources/Localizations/nl.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/nl.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pauzeren"; "a11y_pin_field" = "PIN-veld"; "a11y_play" = "Afspelen"; -"a11y_poll" = "Peiling"; "a11y_poll_end" = "Beeïndigde peiling"; "a11y_react_with" = "Reageer met %1$@"; "a11y_react_with_other_emojis" = "Reageer met andere emoji's"; @@ -27,20 +26,21 @@ "action_back" = "Terug"; "action_call" = "Bellen"; "action_cancel" = "Annuleren"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Voor nu annuleren"; "action_choose_photo" = "Kies foto"; "action_clear" = "Wissen"; "action_close" = "Sluiten"; "action_complete_verification" = "Verificatie voltooien"; "action_confirm" = "Bevestigen"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Bevestig wachtwoord"; "action_continue" = "Voortzetten"; "action_copy" = "Kopiëren"; "action_copy_link" = "Kopieer link"; "action_copy_link_to_message" = "Kopieer link naar bericht"; "action_create" = "Aanmaken"; "action_create_a_room" = "Creëer een kamer"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Sluiten"; +"action_deactivate_account" = "Account sluiten"; "action_decline" = "Weigeren"; "action_delete_poll" = "Peiling verwijderen"; "action_disable" = "Uitschakelen"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Wachtwoord vergeten?"; "action_forward" = "Doorsturen"; "action_go_back" = "Terug"; +"action_ignore" = "Ignore"; "action_invite" = "Uitnodigen"; "action_invite_friends" = "Mensen uitnodigen"; "action_invite_friends_to_app" = "Nodig mensen uit voor %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Verlaten"; "action_leave_conversation" = "Gesprek verlaten"; "action_leave_room" = "Ruimte verlaten"; +"action_load_more" = "Meer laden"; "action_manage_account" = "Account beheren"; "action_manage_devices" = "Apparaten beheren"; "action_message" = "Bericht"; @@ -73,18 +75,18 @@ "action_ok" = "OK"; "action_open_settings" = "Instellingen"; "action_open_with" = "Openen met"; -"action_pin" = "Pin"; +"action_pin" = "Vastmaken"; "action_quick_reply" = "Snel antwoord"; "action_quote" = "Citeren"; "action_react" = "Reageren"; -"action_reject" = "Reject"; +"action_reject" = "Weiger"; "action_remove" = "Verwijderen"; "action_reply" = "Antwoorden"; "action_reply_in_thread" = "Antwoord in subchat"; "action_report_bug" = "Probleem melden"; "action_report_content" = "Inhoud melden"; "action_reset" = "Opnieuw instellen"; -"action_reset_identity" = "Reset identity"; +"action_reset_identity" = "Identiteit opnieuw instellen"; "action_retry" = "Opnieuw proberen"; "action_retry_decryption" = "Decryptie opnieuw proberen"; "action_save" = "Opslaan"; @@ -93,6 +95,7 @@ "action_send_message" = "Bericht verzenden"; "action_share" = "Delen"; "action_share_link" = "Link delen"; +"action_show" = "Toon"; "action_sign_in_again" = "Log opnieuw in"; "action_signout" = "Uitloggen"; "action_signout_anyway" = "Toch uitloggen"; @@ -104,18 +107,16 @@ "action_take_photo" = "Foto maken"; "action_tap_for_options" = "Tik voor opties"; "action_try_again" = "Probeer het opnieuw"; -"action_unpin" = "Unpin"; -"action_view_in_timeline" = "View in timeline"; +"action_unpin" = "Losmaken"; +"action_view_in_timeline" = "Bekijk in tijdlijn"; "action_view_source" = "Bron weergeven"; "action_yes" = "Ja"; -"action.load_more" = "Meer laden"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "Uitloggen & Upgraden"; +"banner_migrate_to_native_sliding_sync_description" = "Je server ondersteunt nu een nieuw, sneller protocol. Log uit en log opnieuw in om nu te upgraden. Als je dit nu doet, voorkom je dat je geforceerd uitlogt wordt wanneer het oude protocol later wordt verwijderd."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Je homeserver ondersteunt het oude protocol niet meer. Log uit en log opnieuw in om de app te blijven gebruiken."; +"banner_migrate_to_native_sliding_sync_title" = "Upgrade beschikbaar"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Over"; "common_acceptable_use_policy" = "Beleid inzake redelijk gebruik"; "common_advanced_settings" = "Geavanceerde instellingen"; @@ -133,10 +134,12 @@ "common_dark" = "Donker"; "common_decryption_error" = "Decryptie fout"; "common_developer_options" = "Ontwikkelaarsopties"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Directe chat"; "common_edited_suffix" = "(bewerkt)"; "common_editing" = "Bewerken"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryptie ingeschakeld"; "common_enter_your_pin" = "Voer je pincode in"; "common_error" = "Fout"; @@ -147,6 +150,7 @@ "common_favourited" = "Favoriet gemarkeerd"; "common_file" = "Bestand"; "common_forward_message" = "Bericht doorsturen"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Afbeelding"; "common_in_reply_to" = "Als antwoord op %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Dempen"; "common_no_results" = "Geen resultaten"; +"common_no_room_name" = "Geen kamernaam"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "of"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalink"; "common_permission" = "Toestemming"; "common_please_wait" = "Even geduld..."; +"common_poll_end_confirmation" = "Weet je zeker dat je deze peiling wilt beëindigen?"; +"common_poll_summary" = "Peiling: %1$@"; "common_poll_total_votes" = "Totaal aantal stemmen: %1$@"; "common_poll_undisclosed_text" = "Resultaten worden getoond nadat de peiling is afgelopen"; "common_privacy_policy" = "Privacybeleid"; @@ -200,6 +207,7 @@ "common_settings" = "Instellingen"; "common_shared_location" = "Gedeelde locatie"; "common_signing_out" = "Uitloggen"; +"common_something_went_wrong" = "Er is iets misgegaan"; "common_starting_chat" = "Chat starten…"; "common_sticker" = "Sticker"; "common_success" = "Geslaagd"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Waar gaat deze kamer over?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Kan niet ontsleutelen"; +"common_unable_to_decrypt_no_access" = "Je hebt geen toegang tot dit bericht"; "common_unable_to_invite_message" = "Uitnodigingen konden niet naar een of meerdere gebruikers worden verzonden."; "common_unable_to_invite_title" = "Kan uitnodiging(en) niet verzenden"; "common_unlock" = "Ontgrendelen"; @@ -221,23 +230,30 @@ "common_username" = "Gebruikersnaam"; "common_verification_cancelled" = "Verificatie geannuleerd"; "common_verification_complete" = "Verificatie voltooid"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Geverifieerd"; +"common_verify_device" = "Apparaat verifiëren"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Spraakbericht"; "common_waiting" = "Wachten…"; "common_waiting_for_decryption_key" = "Wachten op dit bericht"; -"common.do_not_show_this_again" = "Do not show this again"; -"common.open_source_licenses" = "Open source licenses"; -"common.pinned" = "Pinned"; -"common.send_to" = "Send to"; -"common_no_room_name" = "Geen kamernaam"; -"common_poll_end_confirmation" = "Weet je zeker dat je deze peiling wilt beëindigen?"; -"common_poll_summary" = "Peiling: %1$@"; -"common_something_went_wrong" = "Er is iets misgegaan"; -"common_unable_to_decrypt_no_access" = "Je hebt geen toegang tot dit bericht"; -"common_verify_device" = "Apparaat verifiëren"; +"common.copied_to_clipboard" = "Gekopieerd naar klembord"; +"common.do_not_show_this_again" = "Dit niet meer weergeven"; +"common.open_source_licenses" = "Open-sourcelicenties"; +"common.pinned" = "Vastgezet"; +"common.send_to" = "Sturen naar"; +"common.you" = "Jij"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Je chatback-up is momenteel niet gesynchroniseerd. Je moet je herstelsleutel invoeren om toegang te behouden tot je chatback-up."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Voer je herstelsleutel in"; "crash_detection_dialog_content" = "%1$@ crashte de laatste keer dat het werd gebruikt. Wil je een crashrapport met ons delen?"; +"crypto_identity_change_pin_violation" = "%1$@'s identiteit lijkt te zijn veranderd. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@'s %2$@ identiteit lijkt te zijn veranderd. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Geef toestemming in de systeeminstellingen om de applicatie de camera te laten gebruiken."; "dialog_permission_generic" = "Geef hiervoor toestemming in de systeeminstellingen."; "dialog_permission_location_description_ios" = "Verleen toegang via Instellingen -> Locatie."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Smileys & Mensen"; "emoji_picker_category_places" = "Reizen & Locaties"; "emoji_picker_category_symbols" = "Symbolen"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Je homeserver moet worden geüpgraded om de Matrix Authentication Service en het aanmaken van accounts te ondersteunen."; "error_failed_creating_the_permalink" = "Het aanmaken van de permanente link is mislukt"; "error_failed_loading_map" = "%1$@ kon de kaart niet laden. Probeer het later opnieuw."; "error_failed_loading_messages" = "Het laden van berichten is mislukt"; @@ -268,14 +284,14 @@ "error_no_compatible_app_found" = "Er is geen compatibele app gevonden om deze actie uit te voeren."; "error_some_messages_have_not_been_sent" = "Sommige berichten zijn niet verzonden"; "error_unknown" = "Sorry, er is een fout opgetreden"; -"event_shield_reason_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; -"event_shield_reason_sent_in_clear" = "Not encrypted."; -"event_shield_reason_unknown_device" = "Encrypted by an unknown or deleted device."; -"event_shield_reason_unsigned_device" = "Encrypted by a device not verified by its owner."; -"event_shield_reason_unverified_identity" = "Encrypted by an unverified user."; +"event_shield_reason_authenticity_not_guaranteed" = "De echtheid van dit versleutelde bericht kan op dit apparaat niet worden gegarandeerd."; +"event_shield_reason_previously_verified" = "Versleuteld door een eerder geverifieerde gebruiker."; +"event_shield_reason_sent_in_clear" = "Niet versleuteld."; +"event_shield_reason_unknown_device" = "Versleuteld door een onbekend of verwijderd apparaat."; +"event_shield_reason_unsigned_device" = "Versleuteld door een apparaat dat niet is geverifieerd door de eigenaar."; +"event_shield_reason_unverified_identity" = "Versleuteld door een niet-geverifieerde gebruiker."; "full_screen_intent_banner_message" = "To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."; -"full_screen_intent_banner_title" = "Enhance your call experience"; +"full_screen_intent_banner_title" = "Verbeter je gesprekservaring"; "invite_friends_rich_title" = "🔐️ Sluit je bij mij aan op %1$@"; "invite_friends_text" = "Hé, praat met me op %1$@: %2$@"; "leave_conversation_alert_subtitle" = "Weet je zeker dat je dit gesprek wilt verlaten? Dit gesprek is niet openbaar en je kunt niet opnieuw deelnemen zonder een uitnodiging."; @@ -286,20 +302,19 @@ "notification_channel_call" = "Bellen"; "notification_channel_listening_for_events" = "Wachten op gebeurtenissen"; "notification_channel_noisy" = "Luide meldingen"; -"notification_channel_ringing_calls" = "Ringing calls"; +"notification_channel_ringing_calls" = "Overgaande oproepen"; "notification_channel_silent" = "Stille meldingen"; -"notification_incoming_call" = "Incoming call"; +"notification_incoming_call" = "Inkomende oproep"; "notification_inline_reply_failed" = "** Verzenden is mislukt - open de kamer"; -"notification_invitation_action_reject" = "Afwijzen"; "notification_invite_body" = "Nodigde je uit om te chatten"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ nodigde je uit om te chatten"; "notification_mentioned_you_body" = "Heeft je genoemd: %1$@"; "notification_new_messages" = "Nieuwe berichten"; "notification_reaction_body" = "Reageerde met %1$@"; "notification_room_invite_body" = "Nodigde je uit om tot de kamer toe te treden"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ nodigde je uit om tot de kamer toe te treden"; "notification_sender_me" = "Mij"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@ heeft vermeld of beantwoord"; "notification_test_push_notification_content" = "Je bekijkt de melding! Klik hier!"; "notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,31 +344,46 @@ "rich_text_editor_unindent" = "Inspringing ongedaan maken"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Bijlage toevoegen"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Aangepaste basis-URL voor Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Stel een aangepaste basis-URL in voor Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Ongeldige URL, zorg ervoor dat je het protocol (http/https) en het juiste adres invult."; -"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; -"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; -"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; -"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; -"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; -"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Iedereen kan toetreden tot deze kamer"; +"screen_create_room_access_section_anyone_option_title" = "Iedereen"; +"screen_create_room_access_section_header" = "Toegang tot de kamer"; +"screen_create_room_access_section_knocking_option_description" = "Iedereen kan vragen om toe te treden tot de kamer, maar een beheerder of moderator moet het verzoek accepteren"; +"screen_create_room_access_section_knocking_option_title" = "Vraag om toe te treden"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Bericht (optioneel)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Verzoek om toe te treden verzonden"; +"screen_pinned_timeline_empty_state_description" = "Druk op een bericht en kies „%1$@” om het hier toe te voegen."; +"screen_pinned_timeline_empty_state_headline" = "Zet belangrijke berichten vast zodat ze gemakkelijk te vinden zijn"; +"screen_reset_encryption_password_error" = "Er is een onbekende fout opgetreden. Controleer of het wachtwoord van je account juist is en probeer het opnieuw."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Verificatie intrekken en verzenden"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Je kunt je verificatie intrekken en dit bericht toch verzenden, of je kunt het voorlopig annuleren en het later opnieuw proberen nadat je %1$@ opnieuw hebt geverifieerd."; +"screen_resolve_send_failure_changed_identity_title" = "Je bericht is niet verzonden omdat %1$@'s geverifieerde identiteit is gewijzigd"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Bericht toch versturen"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ gebruikt een of meer niet-geverifieerde apparaten. Je kunt het bericht toch verzenden, of je kunt het voorlopig annuleren en het later opnieuw proberen nadat %2$@ alle apparaten heeft geverifieerd."; +"screen_resolve_send_failure_unsigned_device_title" = "Je bericht is niet verzonden omdat %1$@ niet alle apparaten heeft geverifieerd"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Een of meer van je apparaten zijn niet geverifieerd. Je kunt het bericht toch verzenden, of je kunt het voorlopig annuleren en het later opnieuw proberen nadat je al je apparaten hebt geverifieerd."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Je bericht is niet verzonden omdat je een of meerdere apparaten niet geverifieerd hebt"; "screen_room_mentions_at_room_subtitle" = "Stuur een melding naar de hele kamer"; -"screen_room_pinned_banner_indicator" = "%1$@ of %2$@"; -"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages"; -"screen_room_pinned_banner_loading_description" = "Loading message…"; -"screen_room_pinned_banner_view_all_button_title" = "View All"; -"screen_room_details_pinned_events_row_title" = "Pinned messages"; -"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Wijzig accountprovider"; +"screen_room_pinned_banner_indicator" = "%1$@ van %2$@"; +"screen_room_pinned_banner_indicator_description" = "%1$@ Vastgezette berichten"; +"screen_room_pinned_banner_loading_description" = "Bericht laden..."; +"screen_room_pinned_banner_view_all_button_title" = "Bekijk alles"; +"screen_room_details_pinned_events_row_title" = "Vastgezette berichten"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Bericht niet verzonden omdat %1$@'s geverifieerde identiteit is gewijzigd."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Bericht niet verzonden omdat %1$@ niet alle apparaten heeft geverifieerd."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Bericht is niet verzonden omdat je een of meerdere apparaten niet geverifieerd hebt"; "screen_account_provider_form_hint" = "Homeserver-adres"; "screen_account_provider_form_notice" = "Voer een zoekterm of een domeinnaam in."; "screen_account_provider_form_subtitle" = "Zoek naar een bedrijf, community of privéserver."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Je staat op het punt een account aan te maken op %@"; "screen_advanced_settings_developer_mode" = "Ontwikkelaarsmodus"; "screen_advanced_settings_developer_mode_description" = "Schakel in om toegang te krijgen tot tools en functies voor ontwikkelaars."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Schakel de uitgebreide tekstverwerker uit om Markdown handmatig te typen."; "screen_advanced_settings_send_read_receipts" = "Leesbevestigingen"; "screen_advanced_settings_send_read_receipts_description" = "Indien uitgeschakeld worden er geen leesbevestigingen verstuurd. Je ontvangt nog steeds leesbevestigingen van andere gebruikers."; @@ -428,14 +460,16 @@ "screen_change_server_title" = "Selecteer je server"; "screen_chat_backup_key_backup_action_disable" = "Back-up uitschakelen"; "screen_chat_backup_key_backup_action_enable" = "Back-up inschakelen"; -"screen_chat_backup_key_backup_description" = "Een back-up maken zorgt ervoor dat je je berichtgeschiedenis niet verliest. %1$@."; +"screen_chat_backup_key_backup_description" = "Sla je cryptografische identiteit en berichtsleutels veilig op de server op. Zo kun je je berichtgeschiedenis bekijken op nieuwe apparaten. %1$@."; "screen_chat_backup_key_backup_title" = "Back-up"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Herstelsleutel wijzigen"; -"screen_chat_backup_recovery_action_confirm" = "Voer herstelsleutel in"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Je chatback-up is momenteel niet gesynchroniseerd."; -"screen_chat_backup_recovery_action_setup" = "Herstelmogelijkheid instellen"; "screen_chat_backup_recovery_action_setup_description" = "Krijg toegang tot je versleutelde berichten als je al je apparaten kwijtraakt of overal uit %1$@ bent uitgelogd."; -"screen_create_account_title" = "Create account"; +"screen_create_account_title" = "Account aanmaken"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ op een desktopapparaat"; "screen_create_new_recovery_key_list_item_2" = "Log opnieuw in op je account"; "screen_create_new_recovery_key_list_item_3" = "Wanneer je wordt gevraagd om je apparaat te verifiëren, selecteer %1$@"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Resultaten pas weergeven nadat de peiling is afgelopen"; "screen_create_poll_anonymous_headline" = "Stemmen verbergen"; "screen_create_poll_answer_hint" = "Optie %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Je wijzigingen worden niet opgeslagen"; "screen_create_poll_cancel_confirmation_title_ios" = "Peiling annuleren"; "screen_create_poll_question_desc" = "Vraag of onderwerp"; "screen_create_poll_question_hint" = "Waar gaat de peiling over?"; @@ -455,21 +488,21 @@ "screen_create_room_action_create_room" = "Nieuwe kamer"; "screen_create_room_error_creating_room" = "Er is een fout opgetreden bij het aanmaken van de kamer"; "screen_create_room_private_option_description" = "Berichten in deze kamer zijn versleuteld. Versleuteling kan achteraf niet worden uitgeschakeld."; -"screen_create_room_private_option_title" = "Privé kamer (alleen op uitnodiging)"; -"screen_create_room_public_option_description" = "Berichten zijn niet versleuteld en iedereen kan ze lezen. Je kunt versleuteling later inschakelen."; -"screen_create_room_public_option_title" = "Openbare kamer (iedereen)"; +"screen_create_room_private_option_title" = "Privé kamer"; +"screen_create_room_public_option_description" = "Iedereen kan deze kamer vinden.\nJe kunt dit op elk gewenst moment wijzigen in de kamerinstellingen."; +"screen_create_room_public_option_title" = "Openbare kamer"; "screen_create_room_topic_label" = "Onderwerp (optioneel)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Bevestig dat je je account wilt sluiten. Deze actie kan niet ongedaan worden gemaakt."; +"screen_deactivate_account_delete_all_messages" = "Verwijder al mijn berichten"; +"screen_deactivate_account_delete_all_messages_notice" = "Waarschuwing: Toekomstige gebruikers kunnen onvolledige gesprekken te zien krijgen."; +"screen_deactivate_account_description" = "Je account sluiten is %1$@, het zal:"; +"screen_deactivate_account_description_bold_part" = "onomkeerbaar"; +"screen_deactivate_account_list_item_1" = "Je account %1$@ (je kunt niet opnieuw inloggen en je ID kan niet opnieuw worden gebruikt)"; +"screen_deactivate_account_list_item_1_bold_part" = "permanent uitschakelen"; +"screen_deactivate_account_list_item_2" = "Je verwijderen uit alle chatrooms."; +"screen_deactivate_account_list_item_3" = "Je accountgegevens verwijderen van onze identiteitsserver."; +"screen_deactivate_account_list_item_4" = "Je berichten zijn nog steeds zichtbaar voor geregistreerde gebruikers, maar niet beschikbaar voor nieuwe of niet-geregistreerde gebruikers als je ervoor kiest ze te verwijderen."; +"screen_deactivate_account_title" = "Account sluiten"; "screen_edit_poll_delete_confirmation" = "Weet je zeker dat je deze peiling wilt verwijderen?"; "screen_edit_profile_display_name" = "Weergavenaam"; "screen_edit_profile_display_name_placeholder" = "Je weergavenaam"; @@ -477,18 +510,18 @@ "screen_edit_profile_error_title" = "Kan profiel niet bijwerken"; "screen_edit_profile_title" = "Profiel bewerken"; "screen_edit_profile_updating_details" = "Profiel bijwerken…"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; -"screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; -"screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; -"screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; -"screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; -"screen_identity_confirmation_cannot_confirm" = "Can't confirm?"; +"screen_encryption_reset_action_continue_reset" = "Doorgaan met opnieuw instellen"; +"screen_encryption_reset_bullet_1" = "Je accountgegevens, contacten, voorkeuren en chatlijst worden bewaard"; +"screen_encryption_reset_bullet_2" = "Je verliest alle berichtgeschiedenis die alleen op de server is opgeslagen"; +"screen_encryption_reset_bullet_3" = "Je moet al je bestaande apparaten en contacten opnieuw verifiëren"; +"screen_encryption_reset_footer" = "Stel je identiteit alleen opnieuw in als je geen toegang hebt tot een ander aangemeld apparaat en je je herstelsleutel kwijt bent."; +"screen_encryption_reset_title" = "Kun je dit niet bevestigen? Je zult je identiteit opnieuw moeten instellen."; +"screen_identity_confirmation_cannot_confirm" = "Kan ik dit niet bevestigen?"; "screen_identity_confirmation_create_new_recovery_key" = "Maak een nieuwe herstelsleutel"; "screen_identity_confirmation_subtitle" = "Verifieer dit apparaat om beveiligde berichten in te stellen."; "screen_identity_confirmation_title" = "Bevestig dat jij het bent"; -"screen_identity_confirmation_use_another_device" = "Use another device"; -"screen_identity_confirmation_use_recovery_key" = "Use recovery key"; +"screen_identity_confirmation_use_another_device" = "Gebruik een ander apparaat"; +"screen_identity_confirmation_use_recovery_key" = "Gebruik de herstelsleutel"; "screen_identity_confirmed_subtitle" = "Nu kun je veilig berichten lezen of verzenden, en iedereen met wie je chat kan dit apparaat ook vertrouwen."; "screen_identity_confirmed_title" = "Apparaat geverifieerd"; "screen_identity_waiting_on_other_device" = "Wachten op ander apparaat..."; @@ -513,7 +546,7 @@ "screen_key_backup_disable_description_point_1" = "Geen berichtgeschiedenis hebben van versleutelde berichten op nieuwe apparaten"; "screen_key_backup_disable_description_point_2" = "Toegang verliezen tot je versleutelde berichten als je overal uit %1$@ bent uitgelogd."; "screen_key_backup_disable_title" = "Weet je zeker dat je de back-up wilt uitschakelen?"; -"screen_login_error_deactivated_account" = "Dit account is gedeactiveerd."; +"screen_login_error_deactivated_account" = "Dit account is gesloten."; "screen_login_error_invalid_credentials" = "Onjuiste gebruikersnaam en/of wachtwoord"; "screen_login_error_invalid_user_id" = "Dit is geen geldige gebruikers-ID. Verwacht formaat: '@user:homeserver.org'"; "screen_login_error_refresh_tokens" = "Deze server is geconfigureerd om verversingstokens te gebruiken. Deze worden niet ondersteund bij inloggen met een wachtwoord."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Groep chats"; "screen_notification_settings_invite_for_me_label" = "Uitnodigingen"; "screen_notification_settings_mentions_only_disclaimer" = "Je homeserver ondersteunt deze optie niet in versleutelde kamers; in sommige kamers krijg je mogelijk geen meldingen."; -"screen_notification_settings_mentions_section_title" = "Vermeldingen"; "screen_notification_settings_mode_all" = "Alles"; "screen_notification_settings_mode_mentions" = "Vermeldingen"; "screen_notification_settings_notification_section_title" = "Stuur me een melding voor"; @@ -573,24 +605,25 @@ "screen_qr_code_login_connection_note_secure_state_title" = "Verbinding niet veilig"; "screen_qr_code_login_device_code_subtitle" = "Daar word je gevraagd om de twee cijfers in te voeren die op dit apparaat worden weergegeven."; "screen_qr_code_login_device_code_title" = "Voer het onderstaande nummer in op je andere apparaat"; -"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Sign in to your other device and then try again, or use another device that’s already signed in."; -"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Other device not signed in"; +"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Log in op een ander apparaat en probeer opnieuw, of gebruik een ander apparaat dat al is ingelogd."; +"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Ander apparaat is niet ingelogd"; "screen_qr_code_login_error_cancelled_subtitle" = "De aanmelding is geannuleerd op het andere apparaat."; "screen_qr_code_login_error_cancelled_title" = "Login verzoek geannuleerd"; "screen_qr_code_login_error_declined_subtitle" = "De aanmelding is geweigerd op het andere apparaat."; "screen_qr_code_login_error_declined_title" = "Aanmelden geweigerd"; "screen_qr_code_login_error_expired_subtitle" = "Aanmelden is verlopen. Probeer het opnieuw."; "screen_qr_code_login_error_expired_title" = "De aanmelding was niet op tijd voltooid"; -"screen_qr_code_login_error_linking_not_suported_subtitle" = "Your other device does not support signing in to %@ with a QR code.\n\nTry signing in manually, or scan the QR code with another device."; +"screen_qr_code_login_error_linking_not_suported_subtitle" = "Jouw andere apparaat ondersteunt geen inloggen op %@ met een QR code.\n\nProbeer handmatig in te loggen, of scan de QR code met een ander apparaat."; "screen_qr_code_login_error_linking_not_suported_title" = "QR-code wordt niet ondersteund"; -"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Your account provider does not support %1$@."; -"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ not supported"; +"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Je accountprovider ondersteunt geen %1$@."; +"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ wordt niet ondersteund"; "screen_qr_code_login_initial_state_button_title" = "Klaar om te scannen"; "screen_qr_code_login_initial_state_item_1" = "Open %1$@ op een desktopapparaat"; "screen_qr_code_login_initial_state_item_2" = "Klik op je afbeelding"; "screen_qr_code_login_initial_state_item_3" = "Selecteer %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Nieuw apparaat koppelen”"; "screen_qr_code_login_initial_state_item_4" = "Scan de QR-code met dit apparaat"; +"screen_qr_code_login_initial_state_subtitle" = "Alleen beschikbaar als je accountprovider dit ondersteunt."; "screen_qr_code_login_initial_state_title" = "Open %1$@ op een ander apparaat om de QR-code te krijgen"; "screen_qr_code_login_invalid_scan_state_description" = "Gebruik de QR-code die op het andere apparaat wordt weergegeven."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Verkeerde QR-code"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Je verificatiecode"; "screen_recovery_key_change_description" = "Maak een nieuwe herstelsleutel aan als je je bestaande kwijt bent. Nadat je je herstelsleutel hebt gewijzigd, werkt je oude herstelsleutel niet meer."; "screen_recovery_key_change_generate_key" = "Genereer een nieuwe herstelsleutel"; -"screen_recovery_key_change_generate_key_description" = "Zorg ervoor dat je je herstelsleutel op een veilige plek kunt bewaren"; "screen_recovery_key_change_success" = "Herstelsleutel gewijzigd"; "screen_recovery_key_change_title" = "Herstelsleutel wijzigen?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Maak een nieuwe herstelsleutel"; @@ -616,31 +648,29 @@ "screen_recovery_key_confirm_key_placeholder" = "Voer in..."; "screen_recovery_key_confirm_lost_recovery_key" = "Herstelsleutel kwijt?"; "screen_recovery_key_confirm_success" = "Herstelsleutel bevestigd"; -"screen_recovery_key_confirm_title" = "Voer je herstelsleutel in"; "screen_recovery_key_copied_to_clipboard" = "Herstelsleutel gekopieerd"; "screen_recovery_key_generating_key" = "Genereren..."; "screen_recovery_key_save_action" = "Herstelsleutel opslaan"; -"screen_recovery_key_save_description" = "Noteer je herstelsleutel op een veilige plek of bewaar deze in een wachtwoordmanager."; +"screen_recovery_key_save_description" = "Bewaar je herstelsleutel op een veilige plek, zoals in een wachtwoordbeheerder, een versleutelde notitie of in een fysieke kluis."; "screen_recovery_key_save_key_description" = "Tik om de herstelsleutel te kopiëren"; "screen_recovery_key_save_title" = "Sla je herstelsleutel op"; "screen_recovery_key_setup_confirmation_description" = "Na deze stap kun je je nieuwe herstelsleutel niet meer inzien."; "screen_recovery_key_setup_confirmation_title" = "Heb je je herstelsleutel opgeslagen?"; "screen_recovery_key_setup_description" = "Je chatback-up wordt beschermd door een herstelsleutel. Als je na de installatie een nieuwe herstelsleutel nodig hebt, kun je deze opnieuw aanmaken door 'Herstelsleutel wijzigen' te selecteren."; "screen_recovery_key_setup_generate_key" = "Genereer je herstelsleutel"; -"screen_recovery_key_setup_generate_key_description" = "Zorg ervoor dat je je herstelsleutel op een veilige plek kunt bewaren"; +"screen_recovery_key_setup_generate_key_description" = "Deel dit met niemand!"; "screen_recovery_key_setup_success" = "Herstelmogelijkheid succesvol ingesteld"; "screen_recovery_key_setup_title" = "Herstelmogelijkheid instellen"; "screen_report_content_block_user_hint" = "Vink aan als je alle huidige en toekomstige berichten van deze gebruiker wilt verbergen"; "screen_report_content_explanation" = "Dit bericht wordt gerapporteerd aan de beheerder van je homeserver. Ze zullen geen versleutelde berichten kunnen lezen."; "screen_report_content_hint" = "Reden voor het melden van deze inhoud"; -"screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; -"screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; -"screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; -"screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; -"screen_reset_encryption_password_title" = "Enter your account password to continue"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_encryption_confirmation_alert_action" = "Ja, nu opnieuw instellen"; +"screen_reset_encryption_confirmation_alert_subtitle" = "Dit proces is onomkeerbaar."; +"screen_reset_encryption_confirmation_alert_title" = "Weet je zeker dat je je identiteit opnieuw wilt instellen?"; +"screen_reset_encryption_password_subtitle" = "Bevestig dat je je identiteit opnieuw wilt instellen."; +"screen_reset_encryption_password_title" = "Voer het wachtwoord van je account in om verder te gaan"; +"screen_reset_identity_confirmation_subtitle" = "Je staat op het punt naar je %1$@ account te gaan om je identiteit opnieuw in te stellen. Daarna kom je terug naar de app."; +"screen_reset_identity_confirmation_title" = "Kun je dit niet bevestigen? Ga naar je account om je identiteit opnieuw in te stellen."; "screen_room_alias_resolver_resolve_alias_failure" = "Kan het kameradres niet vinden."; "screen_room_attachment_source_camera" = "Camera"; "screen_room_attachment_source_camera_video" = "Video opnemen"; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Beheerders hebben automatisch moderatorrechten"; "screen_room_change_role_moderators_title" = "Moderators bewerken"; "screen_room_change_role_unsaved_changes_description" = "Je hebt niet-opgeslagen wijzigingen"; -"screen_room_change_role_unsaved_changes_title" = "Wijzigingen opslaan?"; "screen_room_details_add_topic_title" = "Onderwerp toevoegen"; "screen_room_details_already_a_member" = "Reeds lid"; "screen_room_details_already_invited" = "Reeds uitgenodigd"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Het dempen opheffen voor deze kamer is mislukt. Probeer het opnieuw."; "screen_room_details_notification_mode_custom" = "Aangepast"; "screen_room_details_notification_mode_default" = "Standaard"; -"screen_room_details_notification_title" = "Meldingen"; "screen_room_details_share_room_title" = "Kamer delen"; "screen_room_details_title" = "Kamer info"; "screen_room_details_updating_room" = "Kamer bijwerken…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Deblokkeren"; "screen_room_member_details_unblock_alert_description" = "Je zult alle berichten van hen weer kunnen zien."; "screen_room_member_details_unblock_user" = "Gebruiker deblokkeren"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Verbannen"; "screen_room_member_list_ban_member_confirmation_description" = "Ze kunnen niet meer toetreden tot deze kamer als ze worden uitgenodigd."; "screen_room_member_list_ban_member_confirmation_title" = "Weet je zeker dat je dit lid wilt verbannen?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "%1$@ verbannen"; "screen_room_member_list_manage_member_ban" = "Lid verwijderen en verbannen"; "screen_room_member_list_manage_member_remove" = "Verwijderen uit kamer"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Lid verwijderen en verbannen"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Alleen lid verwijderen"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Lid verwijderen en toekomstige deelname verbieden?"; "screen_room_member_list_manage_member_unban_action" = "Ontbannen"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Toon minder"; "screen_room_timeline_message_copied" = "Bericht gekopieerd"; "screen_room_timeline_no_permission_to_post" = "Je hebt geen toestemming om berichten in deze kamer te plaatsen"; -"screen_room_timeline_reactions_show_less" = "Minder tonen"; "screen_room_timeline_reactions_show_more" = "Meer tonen"; "screen_room_timeline_read_marker_title" = "Nieuw"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Markeren als gelezen"; "screen_roomlist_mark_as_unread" = "Markeren als ongelezen"; "screen_roomlist_room_directory_button_title" = "Blader door alle kamers"; -"screen_server_confirmation_change_server" = "Accountprovider wijzigen"; "screen_server_confirmation_message_login_element_dot_io" = "Een privéserver voor medewerkers van Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."; "screen_server_confirmation_message_register" = "Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Vergelijk getallen"; "screen_session_verification_complete_subtitle" = "Je nieuwe sessie is nu geverifieerd. Het heeft toegang tot je versleutelde berichten en andere gebruikers zullen het als vertrouwd beschouwen."; "screen_session_verification_enter_recovery_key" = "Voer herstelsleutel in"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Bewijs dat jij het bent om toegang te krijgen tot je versleutelde berichtgeschiedenis."; "screen_session_verification_open_existing_session_title" = "Open een bestaande sessie"; "screen_session_verification_positive_button_canceled" = "Verificatie opnieuw proberen"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Wachten om te vergelijken"; "screen_session_verification_ready_subtitle" = "Vergelijk een unieke combinatie van emoji's."; "screen_session_verification_request_accepted_subtitle" = "Vergelijk de unieke emoji's, ze dienen in dezelfde volgorde te worden weergegeven."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Ze komen niet overeen"; "screen_session_verification_they_match" = "Ze komen overeen"; "screen_session_verification_waiting_to_accept_subtitle" = "Accepteer het verzoek tot verificatie in je andere sessie om door te gaan."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."; "screen_signout_key_backup_disabled_title" = "Je hebt de back-up uitgeschakeld"; "screen_signout_key_backup_offline_subtitle" = "De backup van je sleutels was nog bezig toen je offline ging. Maak opnieuw verbinding zodat er een back-up van je sleutels kan worden gemaakt voordat je uitlogt."; -"screen_signout_key_backup_offline_title" = "De backup van je sleutels is nog bezig"; "screen_signout_key_backup_ongoing_subtitle" = "Wacht tot dit voltooid is voordat je uitlogt."; "screen_signout_key_backup_ongoing_title" = "De backup van je sleutels is nog bezig"; "screen_signout_recovery_disabled_subtitle" = "Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, verlies je de toegang tot je versleutelde berichten."; "screen_signout_recovery_disabled_title" = "Herstelmogelijkheid niet ingesteld"; "screen_signout_save_recovery_key_subtitle" = "Je staat op het punt uit te loggen bij je laatste sessie. Als je je nu uitlogt, kan het dat je de toegang tot je versleutelde berichten verliest."; -"screen_signout_save_recovery_key_title" = "Heb je je herstelsleutel opgeslagen?"; "screen_start_chat_error_starting_chat" = "Er is een fout opgetreden bij het starten van een chat"; "screen_view_location_title" = "Locatie"; "screen_welcome_bullet_1" = "Oproepen, peilingen, zoekopdrachten en meer zullen later dit jaar worden toegevoegd."; @@ -895,12 +928,12 @@ "state_event_room_name_removed_by_you" = "Je hebt de kamernaam verwijderd"; "state_event_room_none" = "%1$@ heeft geen wijzigingen aangebracht"; "state_event_room_none_by_you" = "Je hebt geen wijzigingen aangebracht"; -"state_event_room_pinned_events_changed" = "%1$@ changed the pinned messages"; -"state_event_room_pinned_events_changed_by_you" = "You changed the pinned messages"; -"state_event_room_pinned_events_pinned" = "%1$@ pinned a message"; -"state_event_room_pinned_events_pinned_by_you" = "You pinned a message"; -"state_event_room_pinned_events_unpinned" = "%1$@ unpinned a message"; -"state_event_room_pinned_events_unpinned_by_you" = "You unpinned a message"; +"state_event_room_pinned_events_changed" = "%1$@ heeft de vastgezette berichten gewijzigd"; +"state_event_room_pinned_events_changed_by_you" = "Je hebt de vastgezette berichten gewijzigd"; +"state_event_room_pinned_events_pinned" = "%1$@ heeft een bericht vastgezet"; +"state_event_room_pinned_events_pinned_by_you" = "Je hebt een bericht vastgezet"; +"state_event_room_pinned_events_unpinned" = "%1$@ heeft een bericht losgemaakt"; +"state_event_room_pinned_events_unpinned_by_you" = "Je hebt een bericht losgemaakt"; "state_event_room_reject" = "%1$@ heeft de uitnodiging afgewezen"; "state_event_room_reject_by_you" = "Je hebt de uitnodiging afgewezen"; "state_event_room_remove" = "%1$@ heeft %2$@ verwijderd"; @@ -919,7 +952,6 @@ "test_language_identifier" = "en"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Problemen oplossen"; -"troubleshoot_notifications_entry_point_title" = "Problemen met meldingen oplossen"; "troubleshoot_notifications_screen_action" = "Tests uitvoeren"; "troubleshoot_notifications_screen_action_again" = "Tests opnieuw uitvoeren"; "troubleshoot_notifications_screen_failure" = "Sommige tests zijn mislukt. Controleer de details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ervoor zorgen dat UnifiedPush verdelers beschikbaar zijn."; "troubleshoot_notifications_test_unified_push_failure" = "Geen push-verdelers gevonden."; "troubleshoot_notifications_test_unified_push_title" = "UnifiedPush controleren"; +"a11y_poll" = "Peiling"; +"banner_set_up_recovery_submit" = "Herstelmogelijkheid instellen"; "dialog_title_error" = "Fout"; "dialog_title_success" = "Geslaagd"; "notification_fallback_content" = "Melding"; "notification_invitation_action_join" = "Deelnemen"; +"notification_invitation_action_reject" = "Weiger"; "notification_room_action_mark_as_read" = "Markeren als gelezen"; "notification_room_action_quick_reply" = "Snel antwoord"; +"screen_pinned_timeline_screen_title_empty" = "Vastgezette berichten"; "screen_room_mentions_at_room_title" = "Iedereen"; +"screen_account_provider_change" = "Wijzig accountprovider"; "screen_account_provider_signin_subtitle" = "Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."; "screen_account_provider_signup_subtitle" = "Dit is waar je gesprekken zullen worden bewaard — net zoals je een e-mailprovider zou gebruiken om je e-mails te bewaren."; "screen_analytics_settings_help_us_improve" = "Deel anonieme gebruiksgegevens om ons te helpen problemen te identificeren."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Je zult alle berichten van hen weer kunnen zien."; "screen_blocked_users_unblock_alert_title" = "Gebruiker deblokkeren"; "screen_bug_report_rash_logs_alert_title" = "%1$@ crashte de laatste keer dat het werd gebruikt. Wil je een crashrapport met ons delen?"; +"screen_chat_backup_recovery_action_confirm" = "Voer herstelsleutel in"; +"screen_chat_backup_recovery_action_setup" = "Herstelmogelijkheid instellen"; +"screen_create_poll_cancel_confirmation_content_ios" = "Je wijzigingen worden niet opgeslagen"; "screen_create_room_add_people_title" = "Mensen uitnodigen"; "screen_create_room_room_name_label" = "Naam van de kamer"; "screen_create_room_title" = "Creëer een kamer"; @@ -988,10 +1028,14 @@ "screen_dm_details_unblock_user" = "Gebruiker deblokkeren"; "screen_edit_poll_delete_confirmation_title" = "Peiling verwijderen"; "screen_edit_poll_title" = "Peiling wijzigen"; -"screen_identity_use_another_device" = "Use another device"; +"screen_identity_use_another_device" = "Gebruik een ander apparaat"; "screen_login_subtitle" = "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie."; +"screen_notification_settings_mentions_section_title" = "Vermeldingen"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Probeer het opnieuw"; +"screen_recovery_key_change_generate_key_description" = "Deel dit met niemand!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Gebruiker blokkeren"; +"screen_reset_encryption_password_placeholder" = "Voer in..."; "screen_room_attachment_source_camera_photo" = "Foto maken"; "screen_room_change_permissions_everyone" = "Iedereen"; "screen_room_change_permissions_member_moderation" = "Moderatie van leden"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Beheerders"; "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Leden"; +"screen_room_change_role_unsaved_changes_title" = "Wijzigingen opslaan?"; "screen_room_details_invite_people_title" = "Mensen uitnodigen"; "screen_room_details_leave_conversation_title" = "Gesprek verlaten"; "screen_room_details_leave_room_title" = "Ruimte verlaten"; +"screen_room_details_notification_title" = "Meldingen"; "screen_room_details_roles_and_permissions" = "Rollen en rechten"; "screen_room_details_room_name_label" = "Naam van de kamer"; "screen_room_details_security_title" = "Beveiliging"; "screen_room_details_topic_title" = "Onderwerp"; "screen_room_error_failed_processing_media" = "Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Lid verwijderen en verbannen"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Alleen vermeldingen en trefwoorden"; +"screen_room_timeline_reactions_show_less" = "Toon minder"; "screen_roomlist_filter_people" = "Personen"; +"screen_server_confirmation_change_server" = "Wijzig accountprovider"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Uitloggen"; "screen_signout_confirmation_dialog_title" = "Uitloggen"; +"screen_signout_key_backup_offline_title" = "De backup van je sleutels is nog bezig"; "screen_signout_preference_item" = "Uitloggen"; +"screen_signout_save_recovery_key_title" = "Heb je je herstelsleutel opgeslagen?"; +"troubleshoot_notifications_entry_point_title" = "Problemen met meldingen oplossen"; diff --git a/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict index d02360d9f9..a4b7f469a6 100644 --- a/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict @@ -205,9 +205,9 @@ NSStringFormatValueTypeKey d one - %1$d Pinned message + %1$d Vastgezet bericht other - %1$d Pinned messages + %1$d Vastgezette berichten screen_room_member_list_header_title diff --git a/ElementX/Resources/Localizations/pl.lproj/Localizable.strings b/ElementX/Resources/Localizations/pl.lproj/Localizable.strings index 1c594d2565..56841a16e6 100644 --- a/ElementX/Resources/Localizations/pl.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pl.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Wstrzymaj"; "a11y_pin_field" = "Pole PIN"; "a11y_play" = "Odtwórz"; -"a11y_poll" = "Ankieta"; "a11y_poll_end" = "Zakończona ankieta"; "a11y_react_with" = "Zareaguj z %1$@"; "a11y_react_with_other_emojis" = "Zareaguj innym emoji"; @@ -27,20 +26,21 @@ "action_back" = "Wróć"; "action_call" = "Zadzwoń"; "action_cancel" = "Anuluj"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Anuluj na razie"; "action_choose_photo" = "Wybierz zdjęcie"; "action_clear" = "Wyczyść"; "action_close" = "Zamknij"; "action_complete_verification" = "Dokończ weryfikację"; "action_confirm" = "Potwierdź"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Potwierdź hasło"; "action_continue" = "Kontynuuj"; "action_copy" = "Kopiuj"; "action_copy_link" = "Kopiuj link"; "action_copy_link_to_message" = "Kopiuj link do wiadomości"; "action_create" = "Utwórz"; "action_create_a_room" = "Utwórz pokój"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Dezaktywuj"; +"action_deactivate_account" = "Dezaktywuj konto"; "action_decline" = "Odrzuć"; "action_delete_poll" = "Usuń ankietę"; "action_disable" = "Wyłącz"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Nie pamiętasz hasła?"; "action_forward" = "Przekaż dalej"; "action_go_back" = "Wróć"; +"action_ignore" = "Ignoruj"; "action_invite" = "Zaproś"; "action_invite_friends" = "Zaproś znajomych"; "action_invite_friends_to_app" = "Zaproś znajomych do %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Opuść"; "action_leave_conversation" = "Opuść rozmowę"; "action_leave_room" = "Opuść pokój"; +"action_load_more" = "Załaduj więcej"; "action_manage_account" = "Zarządzaj kontem"; "action_manage_devices" = "Zarządzaj urządzeniami"; "action_message" = "Wiadomość"; @@ -93,6 +95,7 @@ "action_send_message" = "Wyślij wiadomość"; "action_share" = "Udostępnij"; "action_share_link" = "Udostępnij link"; +"action_show" = "Pokaż"; "action_sign_in_again" = "Zaloguj się ponownie"; "action_signout" = "Wyloguj"; "action_signout_anyway" = "Wyloguj mimo to"; @@ -105,17 +108,15 @@ "action_tap_for_options" = "Stuknij, by wyświetlić opcje"; "action_try_again" = "Spróbuj ponownie"; "action_unpin" = "Odepnij"; -"action_view_in_timeline" = "View in timeline"; +"action_view_in_timeline" = "Wyświetl na osi czasu"; "action_view_source" = "Wyświetl źródło"; "action_yes" = "Tak"; -"action.load_more" = "Załaduj więcej"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "Wyloguj się i zaktualizuj"; +"banner_migrate_to_native_sliding_sync_description" = "Twój serwer obsługuje teraz nowy, szybszy protokół. Wyloguj się i zaloguj ponownie, aby uaktualnić teraz. Zrobienie tego teraz pomoże uniknąć wymuszonego wylogowania, gdy stary protokół zostanie później usunięty."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Twój serwer domowy już nie wspiera starego protokołu. Zaloguj się ponownie, aby kontynuować korzystanie z aplikacji."; +"banner_migrate_to_native_sliding_sync_title" = "Dostępna aktualizacja"; +"banner_set_up_recovery_content" = "Wygeneruj nowy klucz przywracania, którego można użyć do przywrócenia historii wiadomości szyfrowanych w przypadku utraty dostępu do swoich urządzeń."; +"banner_set_up_recovery_title" = "Skonfiguruj przywracanie"; "common_about" = "O programie"; "common_acceptable_use_policy" = "Polityka użytkowania"; "common_advanced_settings" = "Ustawienia zaawansowane"; @@ -133,10 +134,12 @@ "common_dark" = "Ciemny"; "common_decryption_error" = "Błąd deszyfrowania"; "common_developer_options" = "Opcje programisty"; +"common_device_id" = "ID urządzenia"; "common_direct_chat" = "Czat prywatny"; "common_edited_suffix" = "(edytowane)"; "common_editing" = "Edytowanie"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Szyfrowanie"; "common_encryption_enabled" = "Szyfrowanie włączone"; "common_enter_your_pin" = "Wprowadź kod PIN"; "common_error" = "Błąd"; @@ -147,6 +150,7 @@ "common_favourited" = "Ulubione"; "common_file" = "Plik"; "common_forward_message" = "Przekaż wiadomość"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Zdjęcie"; "common_in_reply_to" = "W odpowiedzi do %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Nowoczesny"; "common_mute" = "Wycisz"; "common_no_results" = "Brak wyników"; +"common_no_room_name" = "Brak nazwy pokoju"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "lub"; @@ -170,6 +175,8 @@ "common_permalink" = "Link bezpośredni"; "common_permission" = "Uprawnienie"; "common_please_wait" = "Proszę czekać..."; +"common_poll_end_confirmation" = "Jesteś pewien, że chcesz zakończyć tę ankietę?"; +"common_poll_summary" = "Ankieta: %1$@"; "common_poll_total_votes" = "Łączna liczba głosów: %1$@"; "common_poll_undisclosed_text" = "Wyniki zostaną wyświetlone po zakończeniu ankiety"; "common_privacy_policy" = "Polityka prywatności"; @@ -200,6 +207,7 @@ "common_settings" = "Ustawienia"; "common_shared_location" = "Udostępniona lokalizacja"; "common_signing_out" = "Wylogowywanie"; +"common_something_went_wrong" = "Coś poszło nie tak"; "common_starting_chat" = "Rozpoczynanie czatu…"; "common_sticker" = "Naklejka"; "common_success" = "Sukces"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "O czym jest ten pokój?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Nie można odszyfrować"; +"common_unable_to_decrypt_no_access" = "Nie masz uprawnień do tej wiadomości"; "common_unable_to_invite_message" = "Nie udało się wysłać zaproszenia do jednego lub więcej użytkowników."; "common_unable_to_invite_title" = "Nie można wysłać zaproszeń"; "common_unlock" = "Odblokuj"; @@ -221,23 +230,30 @@ "common_username" = "Nazwa użytkownika"; "common_verification_cancelled" = "Weryfikacja anulowana"; "common_verification_complete" = "Weryfikacja zakończona"; +"common_verification_failed" = "Weryfikacja nie powiodła się"; +"common_verified" = "Zweryfikowano"; +"common_verify_device" = "Weryfikuj urządzenie"; +"common_verify_identity" = "Verify identity"; "common_video" = "Film"; "common_voice_message" = "Wiadomość głosowa"; "common_waiting" = "Oczekiwanie…"; "common_waiting_for_decryption_key" = "Oczekiwanie na tę wiadomość"; +"common.copied_to_clipboard" = "Skopiowano do schowka"; "common.do_not_show_this_again" = "Nie pokazuj ponownie"; "common.open_source_licenses" = "Licencje open-source"; -"common.pinned" = "Pinned"; +"common.pinned" = "Przypięte"; "common.send_to" = "Wyślij do"; -"common_no_room_name" = "Brak nazwy pokoju"; -"common_poll_end_confirmation" = "Jesteś pewien, że chcesz zakończyć tę ankietę?"; -"common_poll_summary" = "Ankieta: %1$@"; -"common_something_went_wrong" = "Coś poszło nie tak"; -"common_unable_to_decrypt_no_access" = "Nie masz uprawnień do tej wiadomości"; -"common_verify_device" = "Weryfikuj urządzenie"; +"common.you" = "Ty"; +"common_unable_to_decrypt_insecure_device" = "Wysłane z niebezpiecznego urządzenia"; +"common_unable_to_decrypt_verification_violation" = "Zweryfikowana tożsamość nadawcy uległa zmianie"; "confirm_recovery_key_banner_message" = "Twoja kopia zapasowa czatu jest obecnie niezsynchronizowana. Aby zachować dostęp do kopii zapasowej czatu, musisz potwierdzić klucz odzyskiwania."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Wprowadź swój klucz przywracania"; "crash_detection_dialog_content" = "%1$@ uległ awarii podczas ostatniego użycia. Czy chcesz przesłać nam raport o awarii?"; +"crypto_identity_change_pin_violation" = "Tożsamość %1$@ mogła ulec zmianie. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Aby umożliwić aplikacji korzystanie z aparatu, prosimy o udzielenie zezwolenia w ustawieniach systemowych."; "dialog_permission_generic" = "Proszę nadać uprawnienia w ustawieniach systemowych."; "dialog_permission_location_description_ios" = "Przyznaj dostęp w Ustawienia -> Lokalizacja."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Buźki i osoby"; "emoji_picker_category_places" = "Podróż i miejsca"; "emoji_picker_category_symbols" = "Symbole"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Twój serwer domowy wymaga aktualizacji, aby uzyskać wsparcie usługi Matrix Authentication Service i tworzenia kont."; "error_failed_creating_the_permalink" = "Nie udało się utworzyć linku bezpośredniego"; "error_failed_loading_map" = "%1$@ nie mogło wczytać mapy. Spróbuj ponownie później."; "error_failed_loading_messages" = "Nie udało się załadować wiadomości"; @@ -269,8 +285,8 @@ "error_some_messages_have_not_been_sent" = "Niektóre wiadomości nie zostały wysłane"; "error_unknown" = "Przepraszamy, wystąpił błąd"; "event_shield_reason_authenticity_not_guaranteed" = "Autentyczność tej wiadomości szyfrowanej nie jest gwarantowana na tym urządzeniu."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; -"event_shield_reason_sent_in_clear" = "Not encrypted."; +"event_shield_reason_previously_verified" = "Zaszyfrowane przez wcześniej zweryfikowanego użytkownika."; +"event_shield_reason_sent_in_clear" = "Nieszyfrowany."; "event_shield_reason_unknown_device" = "Zaszyfrowana przez nieznane lub usunięte urządzenie."; "event_shield_reason_unsigned_device" = "Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela."; "event_shield_reason_unverified_identity" = "Zaszyfrowana przez niezweryfikowanego użytkownika."; @@ -290,16 +306,15 @@ "notification_channel_silent" = "Ciche powiadomienia"; "notification_incoming_call" = "Przychodzące połączenie"; "notification_inline_reply_failed" = "** Nie udało się wysłać - proszę otworzyć pokój"; -"notification_invitation_action_reject" = "Odrzuć"; "notification_invite_body" = "Zaprosił(a) cię do czatu"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ zaprosił Cię do czatu"; "notification_mentioned_you_body" = "Wspomniano o Tobie: %1$@"; "notification_new_messages" = "Nowe wiadomości"; "notification_reaction_body" = "Zareagował z %1$@"; "notification_room_invite_body" = "Zaprosił Cię do dołączenia do pokoju"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ zaprosił Cię do pokoju"; "notification_sender_me" = "Ja"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@ wspomniał lub odpowiedział"; "notification_test_push_notification_content" = "Wyświetlasz powiadomienie! Kliknij mnie!"; "notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,31 +344,46 @@ "rich_text_editor_unindent" = "Bez wcięcia"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Dodaj załącznik"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Własny bazowy URL dla połączeń Element"; "screen_advanced_settings_element_call_base_url_description" = "Ustaw własny bazowy URL dla połączeń Element"; "screen_advanced_settings_element_call_base_url_validation_error" = "Nieprawidłowy adres URL, upewnij się, że zawiera protokół (http/https) i poprawny adres."; -"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; -"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; -"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; -"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; -"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; -"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_create_room_room_address_section_footer" = "Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, będziesz potrzebował adres pokoju."; +"screen_create_room_room_address_section_title" = "Adres pokoju"; +"screen_create_room_room_visibility_section_title" = "Widoczność pomieszczenia"; +"screen_create_room_access_section_anyone_option_description" = "Każdy może dołączyć do tego pokoju"; +"screen_create_room_access_section_anyone_option_title" = "Wszyscy"; +"screen_create_room_access_section_header" = "Dostęp do pokoju"; +"screen_create_room_access_section_knocking_option_description" = "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić prośbę"; +"screen_create_room_access_section_knocking_option_title" = "Poproś o dołączenie"; +"screen_join_room_cancel_knock_action" = "Anuluj prośbę"; +"screen_join_room_cancel_knock_alert_confirmation" = "Tak, anuluj"; +"screen_join_room_cancel_knock_alert_description" = "Czy na pewno chcesz anulować prośbę o dołączenie do tego pokoju?"; +"screen_join_room_cancel_knock_alert_title" = "Anuluj prośbę o dołączenie"; +"screen_join_room_knock_message_description" = "Wiadomość (opcjonalne)"; +"screen_join_room_knock_sent_description" = "Otrzymasz zaproszenie dołączenia do pokoju, jeśli prośba zostanie zaakceptowana."; +"screen_join_room_knock_sent_title" = "Wysłano prośbę o dołączenie"; +"screen_pinned_timeline_empty_state_description" = "Naciśnij wiadomość i wybierz “%1$@”, aby dołączyć tutaj."; +"screen_pinned_timeline_empty_state_headline" = "Przypinaj ważne wiadomości, aby można było je łatwo znaleźć"; +"screen_reset_encryption_password_error" = "Wystąpił nieznany błąd. Sprawdź, czy hasło jest poprawne i spróbuj ponownie."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Wycofaj weryfikację i wyślij"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Możesz wycofać weryfikację i wysłać wiadomość mimo wszystko lub anulować i spróbować ponownie po ponownej weryfikacji %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "Twoja wiadomość nie została wysłana, ponieważ tożsamość %1$@ uległa zmianie."; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Wyślij wiadomość mimo to"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ korzysta z jednego lub więcej niezweryfikowanych urządzeń. Wyślij wiadomość mimo to lub anuluj i spróbuj ponownie, gdy %2$@ zweryfikuje wszystkie swoje urządzenia."; +"screen_resolve_send_failure_unsigned_device_title" = "Twoja wiadomość nie została wysłana, ponieważ %1$@ nie zweryfikował swoich wszystkich urządzeń"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Jedno lub więcej z Twoich urządzeń jest niezweryfikowanych. Wyślij wiadomość mimo to lub anuluj i spróbuj ponownie po zweryfikowaniu wszystkich swoich urządzeń."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Twoja wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń."; "screen_room_mentions_at_room_subtitle" = "Powiadom cały pokój"; "screen_room_pinned_banner_indicator" = "%1$@ z %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ przypiętych wiadomości"; -"screen_room_pinned_banner_loading_description" = "Loading message…"; +"screen_room_pinned_banner_loading_description" = "Wczytywanie wiadomości..."; "screen_room_pinned_banner_view_all_button_title" = "Wyświetl wszystkie"; -"screen_room_details_pinned_events_row_title" = "Pinned messages"; -"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Zmień dostawcę konta"; +"screen_room_details_pinned_events_row_title" = "Przypięte wiadomości"; +"screen_roomlist_knock_event_sent_description" = "Wysłano prośbę o dołączenie"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Wiadomość nie została wysłana, ponieważ tożsamość %1$@ uległa zmianie."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Wiadomość nie została wysłana, ponieważ %1$@ nie zweryfikował wszystkich urządzeń."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń."; "screen_account_provider_form_hint" = "Adres serwera domowego"; "screen_account_provider_form_notice" = "Wprowadź wyszukiwane hasło lub adres domeny."; "screen_account_provider_form_subtitle" = "Szukaj serwera firmowego, społeczności lub prywatnego."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Zamierzasz założyć konto na %@"; "screen_advanced_settings_developer_mode" = "Tryb programisty"; "screen_advanced_settings_developer_mode_description" = "Włącz, aby uzyskać dostęp do funkcji dla deweloperów."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Wyłącz edytor tekstu bogatego, aby pisać tekst Markdown ręcznie."; "screen_advanced_settings_send_read_receipts" = "Potwierdzenia odczytania"; "screen_advanced_settings_send_read_receipts_description" = "Gdy wyłączona, Twoje potwierdzenia odczytania nie zostaną wysłane. Potwierdzenia od innych wciąż będą odbierane."; @@ -428,14 +460,16 @@ "screen_change_server_title" = "Wybierz swój serwer"; "screen_chat_backup_key_backup_action_disable" = "Wyłącz backup"; "screen_chat_backup_key_backup_action_enable" = "Włącz backup"; -"screen_chat_backup_key_backup_description" = "Backup zapewnia, że nie stracisz swojej historii wiadomości. %1$@"; -"screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_backup_description" = "Bezpiecznie przechowuj swoją tożsamość kryptograficzną i klucze wiadomości na serwerze. Umożliwi to przeglądanie historii wiadomości na każdym nowym urządzeniu. %1$@"; +"screen_chat_backup_key_backup_title" = "Magazyn kluczy"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Prześlij klucze z tego urządzenia"; +"screen_chat_backup_key_storage_toggle_title" = "Zezwól na magazynowanie kluczy"; "screen_chat_backup_recovery_action_change" = "Zmień klucz przywracania"; -"screen_chat_backup_recovery_action_confirm" = "Wprowadź klucz przywracania"; +"screen_chat_backup_recovery_action_change_description" = "Odzyskaj swoją tożsamość kryptograficzną i historię wiadomości za pomocą klucza przywracania, jeśli utraciłeś dostęp do wszystkich swoich urządzeń."; "screen_chat_backup_recovery_action_confirm_description" = "Backup czatu jest niezsynchronizowany."; -"screen_chat_backup_recovery_action_setup" = "Skonfiguruj przywracanie"; "screen_chat_backup_recovery_action_setup_description" = "Uzyskaj dostęp do swoich wiadomości szyfrowanych, jeśli utracisz wszystkie swoje urządzenia lub zostaniesz wylogowany z %1$@."; -"screen_create_account_title" = "Create account"; +"screen_create_account_title" = "Utwórz konto"; "screen_create_new_recovery_key_list_item_1" = "Otwórz %1$@ na urządzeniu stacjonarnym"; "screen_create_new_recovery_key_list_item_2" = "Zaloguj się ponownie na swoje konto"; "screen_create_new_recovery_key_list_item_3" = "Gdy pojawi się prośba o weryfikację urządzenia, wybierz %1$@"; @@ -447,29 +481,28 @@ "screen_create_poll_anonymous_desc" = "Pokaż wyniki dopiero po zakończeniu ankiety"; "screen_create_poll_anonymous_headline" = "Ukryj głosy"; "screen_create_poll_answer_hint" = "Opcja %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Twoje zmiany nie zostaną zapisane"; "screen_create_poll_cancel_confirmation_title_ios" = "Anuluj ankietę"; "screen_create_poll_question_desc" = "Pytanie lub temat"; "screen_create_poll_question_hint" = "Czego dotyczy ankieta?"; "screen_create_poll_title" = "Utwórz ankietę"; "screen_create_room_action_create_room" = "Nowy pokój"; "screen_create_room_error_creating_room" = "Wystąpił błąd w trakcie tworzenia pokoju"; -"screen_create_room_private_option_description" = "Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."; -"screen_create_room_private_option_title" = "Pokój prywatny (tylko zaproszenie)"; -"screen_create_room_public_option_description" = "Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."; -"screen_create_room_public_option_title" = "Pokój publiczny (wszyscy)"; +"screen_create_room_private_option_description" = "Tylko zaproszone osoby mogą dołączyć do tego pokoju. Wszystkie wiadomości są szyfrowane end-to-end."; +"screen_create_room_private_option_title" = "Pokój prywatny"; +"screen_create_room_public_option_description" = "Każdy może znaleźć ten pokój.\nMożesz to zmienić w ustawieniach pokoju."; +"screen_create_room_public_option_title" = "Pokój publiczny"; "screen_create_room_topic_label" = "Temat (opcjonalnie)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Potwierdź dezaktywacje konta. Tej akcji nie można cofnąć."; +"screen_deactivate_account_delete_all_messages" = "Usuń wszystkie moje wiadomości"; +"screen_deactivate_account_delete_all_messages_notice" = "Ostrzeżenie: Przyszli użytkownicy mogą zobaczyć niekompletne rozmowy."; +"screen_deactivate_account_description" = "Dezaktywacja konta jest %1$@, zostanie:"; +"screen_deactivate_account_description_bold_part" = "nieodwracalna"; +"screen_deactivate_account_list_item_1" = "%1$@ twoje konto (nie będziesz mógł się zalogować, a twoje ID przepadnie)."; +"screen_deactivate_account_list_item_1_bold_part" = "Permanentnie wyłączy"; +"screen_deactivate_account_list_item_2" = "Usunie Ciebie ze wszystkich pokoi rozmów."; +"screen_deactivate_account_list_item_3" = "Usunięte wszystkie dane konta z naszego serwera tożsamości."; +"screen_deactivate_account_list_item_4" = "Twoje wiadomości wciąż będą widoczne dla zarejestrowanych użytkowników, ale nie będą dostępne dla nowych lub niezarejestrowanych użytkowników, jeśli je usuniesz."; +"screen_deactivate_account_title" = "Dezaktywuj konto"; "screen_edit_poll_delete_confirmation" = "Czy na pewno chcesz usunąć tę ankietę?"; "screen_edit_profile_display_name" = "Wyświetlana nazwa"; "screen_edit_profile_display_name_placeholder" = "Twoja wyświetlana nazwa"; @@ -477,7 +510,7 @@ "screen_edit_profile_error_title" = "Nie można zaktualizować profilu"; "screen_edit_profile_title" = "Edytuj profil"; "screen_edit_profile_updating_details" = "Aktualizuję profil…"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; +"screen_encryption_reset_action_continue_reset" = "Kontynuuj resetowanie"; "screen_encryption_reset_bullet_1" = "Szczegóły konta, kontakty, preferencje i lista czatów zostaną zachowane"; "screen_encryption_reset_bullet_2" = "Utracisz istniejącą historię wiadomości"; "screen_encryption_reset_bullet_3" = "Wymagana będzie ponowna weryfikacja istniejących urządzeń i kontaktów"; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "Brak zaproszeń"; "screen_invites_invited_you" = "%1$@ (%2$@) zaprosił Cię"; "screen_join_room_join_action" = "Dołącz do pokoju"; -"screen_join_room_knock_action" = "Zapukaj, by dołączyć"; +"screen_join_room_knock_action" = "Wyślij prośbę o dołączenie"; "screen_join_room_space_not_supported_description" = "%1$@ jeszcze nie obsługuje przestrzeni. Uzyskaj dostęp do przestrzeni w wersji web."; "screen_join_room_space_not_supported_title" = "Przestrzenie nie są jeszcze obsługiwane"; "screen_join_room_subtitle_knock" = "Kliknij przycisk poniżej, aby powiadomić administratora pokoju. Po zatwierdzeniu będziesz mógł dołączyć do rozmowy."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Czaty grupowe"; "screen_notification_settings_invite_for_me_label" = "Zaproszenia"; "screen_notification_settings_mentions_only_disclaimer" = "Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi."; -"screen_notification_settings_mentions_section_title" = "Wzmianki"; "screen_notification_settings_mode_all" = "Wszystkie"; "screen_notification_settings_mode_mentions" = "Wzmianki"; "screen_notification_settings_notification_section_title" = "Powiadamiaj mnie przez"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Wybierz %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Powiąż nowe urządzenie”"; "screen_qr_code_login_initial_state_item_4" = "Zeskanuj kod QR za pomocą tego urządzenia"; +"screen_qr_code_login_initial_state_subtitle" = "Dostępne tylko wtedy, gdy Twój dostawca konta obsługuje tę funkcję."; "screen_qr_code_login_initial_state_title" = "Otwórz %1$@ na innym urządzeniu, aby uzyskać kod QR"; "screen_qr_code_login_invalid_scan_state_description" = "Użyj kodu QR widocznego na drugim urządzeniu."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Błędny kod QR"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Twój kod weryfikacyjny"; "screen_recovery_key_change_description" = "Uzyskaj nowy klucz przywracania, jeśli straciłeś dostęp do obecnego. Po zmianie klucza przywracania stary nie będzie już działał."; "screen_recovery_key_change_generate_key" = "Generuj nowy klucz przywracania"; -"screen_recovery_key_change_generate_key_description" = "Upewnij się, że klucz przywracania będzie trzymany w bezpiecznym miejscu"; "screen_recovery_key_change_success" = "Zmieniono klucz przywracania"; "screen_recovery_key_change_title" = "Zmienić klucz przywracania?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Utwórz nowy klucz przywracania"; @@ -616,18 +648,17 @@ "screen_recovery_key_confirm_key_placeholder" = "Wprowadź..."; "screen_recovery_key_confirm_lost_recovery_key" = "Zgubiłeś swój kod przywracania?"; "screen_recovery_key_confirm_success" = "Potwierdzono klucz przywracania"; -"screen_recovery_key_confirm_title" = "Wprowadź klucz przywracania"; "screen_recovery_key_copied_to_clipboard" = "Skopiowano klucz przywracania"; "screen_recovery_key_generating_key" = "Generuję..."; "screen_recovery_key_save_action" = "Zapisz klucz przywracania"; -"screen_recovery_key_save_description" = "Zapisz klucz przywracania w bezpiecznym miejscu lub zapisz go w menedżerze haseł."; +"screen_recovery_key_save_description" = "Zapisz klucz przywracania w bezpiecznym miejscu, np. w menedżerze haseł, notatce szyfrowanej lub sejfie."; "screen_recovery_key_save_key_description" = "Stuknij, by skopiować klucz przywracania"; "screen_recovery_key_save_title" = "Zapisz klucz przywracania"; "screen_recovery_key_setup_confirmation_description" = "Po tym kroku nie będziesz mieć dostępu do nowego klucza przywracania."; "screen_recovery_key_setup_confirmation_title" = "Czy zapisałeś swój klucz przywracania?"; "screen_recovery_key_setup_description" = "Backup czatu jest chroniony przez klucz przywracania. Jeśli potrzebujesz utworzyć nowy klucz, możesz to zrobić wybierając `Zmień klucz przywracania`."; "screen_recovery_key_setup_generate_key" = "Wygeneruj klucz przywracania"; -"screen_recovery_key_setup_generate_key_description" = "Upewnij się, że klucz przywracania możesz przechowywać w bezpiecznym miejscu"; +"screen_recovery_key_setup_generate_key_description" = "Nie udostępniaj tego nikomu!"; "screen_recovery_key_setup_success" = "Skonfigurowano przywracanie pomyślnie"; "screen_recovery_key_setup_title" = "Skonfiguruj przywracanie"; "screen_report_content_block_user_hint" = "Sprawdź, czy chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika."; @@ -636,11 +667,10 @@ "screen_reset_encryption_confirmation_alert_action" = "Tak, zresetuj teraz"; "screen_reset_encryption_confirmation_alert_subtitle" = "Tego procesu nie można odwrócić."; "screen_reset_encryption_confirmation_alert_title" = "Czy na pewno chcesz zresetować szyfrowanie?"; -"screen_reset_encryption_password_placeholder" = "Wprowadź..."; "screen_reset_encryption_password_subtitle" = "Potwierdź, że chcesz zresetować szyfrowanie."; "screen_reset_encryption_password_title" = "Wprowadź hasło, aby kontynuować"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_identity_confirmation_subtitle" = "Zostaniesz przeniesiony na swoje konto %1$@, aby zresetować tożsamość. Wrócisz do aplikacji po zakończeniu."; +"screen_reset_identity_confirmation_title" = "Nie możesz potwierdzić? Przejdź do swojego konta i zresetuj swoją tożsamość."; "screen_room_alias_resolver_resolve_alias_failure" = "Nie udało się uzyskać aliasu pokoju."; "screen_room_attachment_source_camera" = "Kamera"; "screen_room_attachment_source_camera_video" = "Nagraj film"; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Administratorzy automatycznie mają uprawnienia moderatora"; "screen_room_change_role_moderators_title" = "Edytuj moderatorów"; "screen_room_change_role_unsaved_changes_description" = "Masz niezapisane zmiany."; -"screen_room_change_role_unsaved_changes_title" = "Zapisać zmiany?"; "screen_room_details_add_topic_title" = "Dodaj temat"; "screen_room_details_already_a_member" = "Jest już członkiem"; "screen_room_details_already_invited" = "Już zaproszony"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Nie udało się wyłączyć wyciszenia tego pokoju. Spróbuj ponownie."; "screen_room_details_notification_mode_custom" = "Niestandardowy"; "screen_room_details_notification_mode_default" = "Domyślny"; -"screen_room_details_notification_title" = "Powiadomienia"; "screen_room_details_share_room_title" = "Udostępnij pokój"; "screen_room_details_title" = "Informacje pokoju"; "screen_room_details_updating_room" = "Aktualizuję pokój…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Odblokuj"; "screen_room_member_details_unblock_alert_description" = "Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."; "screen_room_member_details_unblock_user" = "Odblokuj użytkownika"; +"screen_room_member_details_verify_button_subtitle" = "Użyj aplikacji internetowej, aby zweryfikować tego użytkownika."; +"screen_room_member_details_verify_button_title" = "Zweryfikuj %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Zbanuj"; "screen_room_member_list_ban_member_confirmation_description" = "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni."; "screen_room_member_list_ban_member_confirmation_title" = "Czy na pewno chcesz zbanować tego członka?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banowanie %1$@"; "screen_room_member_list_manage_member_ban" = "Usuń i zbanuj członka"; "screen_room_member_list_manage_member_remove" = "Usuń z pokoju"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Usuń i zbanuj członka"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Tylko usuń członka"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Usunąć członka i zablokować możliwość dołączenia w przyszłości?"; "screen_room_member_list_manage_member_unban_action" = "Odbanuj"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Pokaż mniej"; "screen_room_timeline_message_copied" = "Skopiowano wiadomość"; "screen_room_timeline_no_permission_to_post" = "Nie masz uprawnień, aby pisać w tym pokoju"; -"screen_room_timeline_reactions_show_less" = "Pokaż mniej"; "screen_room_timeline_reactions_show_more" = "Pokaż więcej"; "screen_room_timeline_read_marker_title" = "Nowe"; "screen_room_title" = "Czat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Oznacz jako przeczytane"; "screen_roomlist_mark_as_unread" = "Oznacz jako nieprzeczytane"; "screen_roomlist_room_directory_button_title" = "Przeglądaj wszystkie pokoje"; -"screen_server_confirmation_change_server" = "Zmień dostawcę konta"; "screen_server_confirmation_message_login_element_dot_io" = "Serwer prywatny dla pracowników Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."; "screen_server_confirmation_message_register" = "Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Porównaj liczby"; "screen_session_verification_complete_subtitle" = "Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."; "screen_session_verification_enter_recovery_key" = "Wprowadź klucz przywracania"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Udowodnij, że to ty, aby uzyskać dostęp do historii zaszyfrowanych wiadomości."; "screen_session_verification_open_existing_session_title" = "Otwórz istniejącą sesję"; "screen_session_verification_positive_button_canceled" = "Ponów weryfikację"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Oczekiwanie na dopasowanie"; "screen_session_verification_ready_subtitle" = "Porównaj unikalny zestaw emoji."; "screen_session_verification_request_accepted_subtitle" = "Porównaj unikalne emoji, upewniając się, że pojawiły się w tej samej kolejności."; +"screen_session_verification_request_details_timestamp" = "Zalogowano"; +"screen_session_verification_request_failure_title" = "Weryfikacja nie powiodła się"; +"screen_session_verification_request_footer" = "Kontynuuj tylko, jeśli to Ty zainicjowałeś tę weryfikację."; +"screen_session_verification_request_subtitle" = "Zweryfikuj drugie urządzenie, aby zabezpieczyć historię wiadomości."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Urządzenie zweryfikowane"; +"screen_session_verification_request_title" = "Zażądano weryfikacji"; "screen_session_verification_they_dont_match" = "Nie pasują do siebie"; "screen_session_verification_they_match" = "Pasują do siebie"; "screen_session_verification_waiting_to_accept_subtitle" = "Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."; "screen_signout_key_backup_disabled_title" = "Wyłączyłeś backup"; "screen_signout_key_backup_offline_subtitle" = "Twoje klucze były nadal archiwizowane po przejściu w tryb offline. Połącz się ponownie, aby zapisać w chmurze przed wylogowaniem."; -"screen_signout_key_backup_offline_title" = "Twoje klucze są nadal archiwizowane"; "screen_signout_key_backup_ongoing_subtitle" = "Zanim się wylogujesz, poczekaj na zakończenie operacji."; "screen_signout_key_backup_ongoing_title" = "Twoje klucze są nadal archiwizowane"; "screen_signout_recovery_disabled_subtitle" = "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."; "screen_signout_recovery_disabled_title" = "Nie ustawiono przywracania"; "screen_signout_save_recovery_key_subtitle" = "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych."; -"screen_signout_save_recovery_key_title" = "Czy zapisałeś swój klucz przywracania?"; "screen_start_chat_error_starting_chat" = "Wystąpił błąd podczas próby rozpoczęcia czatu"; "screen_view_location_title" = "Lokalizacja"; "screen_welcome_bullet_1" = "Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku."; @@ -919,14 +952,13 @@ "test_language_identifier" = "pl"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Rozwiązywanie problemów"; -"troubleshoot_notifications_entry_point_title" = "Powiadomienia rozwiązywania problemów"; "troubleshoot_notifications_screen_action" = "Uruchom testy"; "troubleshoot_notifications_screen_action_again" = "Uruchom testy ponownie"; "troubleshoot_notifications_screen_failure" = "Niektóre testy się nie powiodły. Sprawdź szczegóły."; "troubleshoot_notifications_screen_notice" = "Uruchom testy, aby wykryć potencjalne problemy z konfiguracją, jeśli powiadomienia nie działają prawidłowo."; "troubleshoot_notifications_screen_quick_fix_action" = "Spróbuj naprawić"; "troubleshoot_notifications_screen_success" = "Wszystkie testy przebiegły pomyślnie."; -"troubleshoot_notifications_screen_title" = "Powiadomienia rozwiązywania problemów"; +"troubleshoot_notifications_screen_title" = "Rozwiązywanie problemów powiadomień"; "troubleshoot_notifications_screen_waiting" = "Niektóre testy wymagają Twojej uwagi. Sprawdź szczegóły."; "troubleshoot_notifications_test_check_permission_description" = "Sprawdź, czy aplikacja może wyświetlać powiadomienia."; "troubleshoot_notifications_test_check_permission_title" = "Sprawdź uprawnienia"; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Upewnij się, że dystrybutorzy UnifiedPush są dostępni."; "troubleshoot_notifications_test_unified_push_failure" = "Nie znaleziono dystrybutorów push."; "troubleshoot_notifications_test_unified_push_title" = "Sprawdź UnifiedPush"; +"a11y_poll" = "Ankieta"; +"banner_set_up_recovery_submit" = "Skonfiguruj przywracanie"; "dialog_title_error" = "Błąd"; "dialog_title_success" = "Sukces"; "notification_fallback_content" = "Powiadomienie"; "notification_invitation_action_join" = "Dołącz"; +"notification_invitation_action_reject" = "Odrzuć"; "notification_room_action_mark_as_read" = "Oznacz jako przeczytane"; "notification_room_action_quick_reply" = "Szybka odpowiedź"; +"screen_pinned_timeline_screen_title_empty" = "Przypięte wiadomości"; "screen_room_mentions_at_room_title" = "Wszyscy"; +"screen_account_provider_change" = "Zmień dostawcę konta"; "screen_account_provider_signin_subtitle" = "Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."; "screen_account_provider_signup_subtitle" = "Tutaj będą przechowywane Twoje konwersacje - w podobnej formie jak wiadomości widnieją na skrzynce e-mail."; "screen_analytics_settings_help_us_improve" = "Udostępniaj anonimowe dane użytkowania, aby pomóc nam identyfikować problemy."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."; "screen_blocked_users_unblock_alert_title" = "Odblokuj użytkownika"; "screen_bug_report_rash_logs_alert_title" = "%1$@ uległ awarii podczas ostatniego użycia. Czy chcesz przesłać nam raport o awarii?"; +"screen_chat_backup_recovery_action_confirm" = "Wprowadź klucz przywracania"; +"screen_chat_backup_recovery_action_setup" = "Skonfiguruj przywracanie"; +"screen_create_poll_cancel_confirmation_content_ios" = "Zmiany nie zostaną zapisane"; "screen_create_room_add_people_title" = "Zaproś znajomych"; "screen_create_room_room_name_label" = "Nazwa pokoju"; "screen_create_room_title" = "Utwórz pokój"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Edytuj ankietę"; "screen_identity_use_another_device" = "Użyj innego urządzenia"; "screen_login_subtitle" = "Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji."; +"screen_notification_settings_mentions_section_title" = "Wzmianki"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Spróbuj ponownie"; +"screen_recovery_key_change_generate_key_description" = "Nie udostępniaj tego nikomu!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Zablokuj użytkownika"; +"screen_reset_encryption_password_placeholder" = "Wprowadź..."; "screen_room_attachment_source_camera_photo" = "Zrób zdjęcie"; "screen_room_change_permissions_everyone" = "Wszyscy"; "screen_room_change_permissions_member_moderation" = "Moderacja członków"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administratorzy"; "screen_room_change_role_section_moderators" = "Moderatorzy"; "screen_room_change_role_section_users" = "Członków"; +"screen_room_change_role_unsaved_changes_title" = "Zapisać zmiany?"; "screen_room_details_invite_people_title" = "Zaproś znajomych"; "screen_room_details_leave_conversation_title" = "Opuść rozmowę"; "screen_room_details_leave_room_title" = "Opuść pokój"; +"screen_room_details_notification_title" = "Powiadomienia"; "screen_room_details_roles_and_permissions" = "Role i uprawnienia"; "screen_room_details_room_name_label" = "Nazwa pokoju"; "screen_room_details_security_title" = "Bezpieczeństwo"; "screen_room_details_topic_title" = "Temat"; "screen_room_error_failed_processing_media" = "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Usuń i zbanuj członka"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Tylko wzmianki i słowa kluczowe"; +"screen_room_timeline_reactions_show_less" = "Pokaż mniej"; "screen_roomlist_filter_people" = "Osoby"; +"screen_server_confirmation_change_server" = "Zmień dostawcę konta"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Wyloguj"; "screen_signout_confirmation_dialog_title" = "Wyloguj"; +"screen_signout_key_backup_offline_title" = "Twoje klucze są nadal archiwizowane"; "screen_signout_preference_item" = "Wyloguj"; +"screen_signout_save_recovery_key_title" = "Czy zapisałeś swój klucz przywracania?"; +"troubleshoot_notifications_entry_point_title" = "Rozwiązywanie problemów powiadomień"; diff --git a/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict index fdf4328867..794f787cf5 100644 --- a/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict @@ -229,9 +229,11 @@ NSStringFormatValueTypeKey d one - %1$d Pinned message - other - %1$d Pinned messages + %1$d przypięta wiadomość + few + %1$d przypięte wiadomości + many + %1$d przypiętych wiadomości screen_room_member_list_header_title diff --git a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings index 0b4aa301e5..659c5bf139 100644 --- a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausar"; "a11y_pin_field" = "Campo de PIN"; "a11y_play" = "Reproduzir"; -"a11y_poll" = "Enquete"; "a11y_poll_end" = "Enquete encerrada"; "a11y_react_with" = "Reagir com %1$@"; "a11y_react_with_other_emojis" = "Reaja com outros emojis"; @@ -41,6 +40,7 @@ "action_create" = "Criar"; "action_create_a_room" = "Criar uma sala"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Recusar"; "action_delete_poll" = "Excluir Enquete"; "action_disable" = "Desabilitar"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Esqueceu a senha?"; "action_forward" = "Encaminhar"; "action_go_back" = "Voltar"; +"action_ignore" = "Ignore"; "action_invite" = "Convidar"; "action_invite_friends" = "Convidar pessoas"; "action_invite_friends_to_app" = "Convidar pessoas para %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Sair"; "action_leave_conversation" = "Sair da conversa"; "action_leave_room" = "Sair da sala"; +"action_load_more" = "Carregar mais"; "action_manage_account" = "Gerenciar conta"; "action_manage_devices" = "Gerenciar dispositivos"; "action_message" = "Mensagem"; @@ -93,6 +95,7 @@ "action_send_message" = "Enviar mensagem"; "action_share" = "Compartilhar"; "action_share_link" = "Compartilhar link"; +"action_show" = "Show"; "action_sign_in_again" = "Iniciar sessão novamente"; "action_signout" = "Sair"; "action_signout_anyway" = "Sair mesmo assim"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Ver fonte"; "action_yes" = "Sim"; -"action.load_more" = "Carregar mais"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Sobre"; "common_acceptable_use_policy" = "Política de uso aceitável"; "common_advanced_settings" = "Configurações avançadas"; @@ -133,10 +134,12 @@ "common_dark" = "Escuro"; "common_decryption_error" = "Erro de descriptografia"; "common_developer_options" = "Opções do desenvolvedor"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Conversa privada"; "common_edited_suffix" = "(editado)"; "common_editing" = "Editando"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Criptografia ativada"; "common_enter_your_pin" = "Insira seu PIN"; "common_error" = "Erro"; @@ -147,6 +150,7 @@ "common_favourited" = "Favoritado"; "common_file" = "Arquivo"; "common_forward_message" = "Encaminhar mensagem"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Imagem"; "common_in_reply_to" = "Em resposta a %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderno"; "common_mute" = "Silenciar"; "common_no_results" = "Sem resultados"; +"common_no_room_name" = "Sem nome de sala"; "common_offline" = "Offline"; "common_optic_id_ios" = "ID ótico"; "common_or" = "ou"; @@ -170,6 +175,8 @@ "common_permalink" = "Link permanente"; "common_permission" = "Permissão"; "common_please_wait" = "Por favor, aguarde..."; +"common_poll_end_confirmation" = "Tem certeza de que deseja encerrar esta enquete?"; +"common_poll_summary" = "Enquete: %1$@"; "common_poll_total_votes" = "Total de votos: %1$@"; "common_poll_undisclosed_text" = "Os resultados serão exibidos após o término da enquete"; "common_privacy_policy" = "Política de Privacidade"; @@ -200,6 +207,7 @@ "common_settings" = "Configurações"; "common_shared_location" = "Localização compartilhada"; "common_signing_out" = "Saindo"; +"common_something_went_wrong" = "Algo deu errado"; "common_starting_chat" = "Iniciando o chat..."; "common_sticker" = "Adesivo"; "common_success" = "Sucesso"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Sobre o que é essa sala?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Não é possível descriptografar"; +"common_unable_to_decrypt_no_access" = "Você não tem acesso a esta mensagem"; "common_unable_to_invite_message" = "Não foi possível enviar convites para um ou mais usuários."; "common_unable_to_invite_title" = "Não foi possível enviar o(s) convite(s)"; "common_unlock" = "Desbloquear"; @@ -221,23 +230,30 @@ "common_username" = "Nome do usuário"; "common_verification_cancelled" = "Verificação cancelada"; "common_verification_complete" = "Verificação concluída"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verificar dispositivo"; +"common_verify_identity" = "Verify identity"; "common_video" = "Vídeo"; "common_voice_message" = "Mensagem de voz"; "common_waiting" = "Esperando..."; "common_waiting_for_decryption_key" = "Aguardando esta mensagem"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Não mostrar isto novamente"; "common.open_source_licenses" = "Licenças de código aberto"; "common.pinned" = "Pinned"; "common.send_to" = "Enviar para"; -"common_no_room_name" = "Sem nome de sala"; -"common_poll_end_confirmation" = "Tem certeza de que deseja encerrar esta enquete?"; -"common_poll_summary" = "Enquete: %1$@"; -"common_something_went_wrong" = "Algo deu errado"; -"common_unable_to_decrypt_no_access" = "Você não tem acesso a esta mensagem"; -"common_verify_device" = "Verificar dispositivo"; -"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Insira sua chave de recuperação"; "crash_detection_dialog_content" = "%1$@ fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Para permitir que o aplicativo use a câmera, conceda a permissão nas configurações do sistema."; "dialog_permission_generic" = "Por favor, conceda a permissão nas configurações do sistema."; "dialog_permission_location_description_ios" = "Permita o acesso em Configurações -> Localização."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Notificações silenciosas"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** Falha ao enviar - por favor, abra a sala"; -"notification_invitation_action_reject" = "Rejeitar"; "notification_invite_body" = "Convidou você para conversar"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Mencionou você: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Desidentar"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Adicionar anexo"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL inválida, por favor verifique se o protocolo (http/https) e o endereço correto estão presentes."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Alterar provedor da conta"; "screen_account_provider_form_hint" = "Endereço do servidor"; "screen_account_provider_form_notice" = "Insira um termo de pesquisa ou um endereço de domínio."; "screen_account_provider_form_subtitle" = "Procure uma empresa, comunidade ou servidor privado."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Você está prestes a criar uma conta em %@"; "screen_advanced_settings_developer_mode" = "Modo de desenvolvedor"; "screen_advanced_settings_developer_mode_description" = "Habilite para ter acesso a recursos e funcionalidades para desenvolvedores."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Desative o editor de rich text para digitar Markdown manualmente."; "screen_advanced_settings_send_read_receipts" = "Confirmações de leitura"; "screen_advanced_settings_send_read_receipts_description" = "Se desligado, suas confirmações de leitura não serão enviadas para ninguém. Você ainda receberá confirmações de leitura de outros usuários."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Ativar o backup"; "screen_chat_backup_key_backup_description" = "O backup garante que você não perca seu histórico de mensagens. %1$@."; "screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Alterar chave de recuperação"; -"screen_chat_backup_recovery_action_confirm" = "Insira a chave de recuperação"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Seu backup das conversas está atualmente fora de sincronia."; -"screen_chat_backup_recovery_action_setup" = "Configurar a recuperação"; "screen_chat_backup_recovery_action_setup_description" = "Tenha acesso às suas mensagens criptografadas se você perder todos os seus dispositivos ou for desconectado do %1$@ em qualquer lugar."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Mostrar resultados somente após o término da enquete"; "screen_create_poll_anonymous_headline" = "Ocultar votos"; "screen_create_poll_answer_hint" = "Opção %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Suas alterações não serão salvas"; "screen_create_poll_cancel_confirmation_title_ios" = "Cancelar enquete"; "screen_create_poll_question_desc" = "Pergunta ou tópico"; "screen_create_poll_question_hint" = "Sobre o que é a enquete?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Atualizando o perfil..."; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "Sem convites"; "screen_invites_invited_you" = "%1$@(%2$@) convidou você"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Bate-papos em grupo"; "screen_notification_settings_invite_for_me_label" = "Convites"; "screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; -"screen_notification_settings_mentions_section_title" = "Menções"; "screen_notification_settings_mode_all" = "Todos"; "screen_notification_settings_mode_mentions" = "Menções"; "screen_notification_settings_notification_section_title" = "Me notifique para"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,18 +638,16 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Obtenha uma nova chave de recuperação caso tenha perdido a existente. Depois de alterar sua chave de recuperação, a antiga não funcionará mais."; "screen_recovery_key_change_generate_key" = "Gere uma nova chave de recuperação"; -"screen_recovery_key_change_generate_key_description" = "Certifique-se de que você pode armazenar sua chave de recuperação em algum lugar seguro"; "screen_recovery_key_change_success" = "Chave de recuperação alterada"; "screen_recovery_key_change_title" = "Alterar chave de recuperação?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Certifique-se de que ninguém possa ver essa tela!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Chave de recuperação incorreta"; "screen_recovery_key_confirm_key_description" = "Se você tiver uma chave de segurança ou frase de segurança, isso também funcionará."; "screen_recovery_key_confirm_key_placeholder" = "Inserir..."; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Chave de recuperação confirmada"; -"screen_recovery_key_confirm_title" = "Insira sua chave de recuperação"; "screen_recovery_key_copied_to_clipboard" = "Chave de recuperação copiada"; "screen_recovery_key_generating_key" = "Gerando..."; "screen_recovery_key_save_action" = "Salvar chave de recuperação"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Editar moderadores"; "screen_room_change_role_unsaved_changes_description" = "Você tem alterações não salvas."; -"screen_room_change_role_unsaved_changes_title" = "Salvar alterações?"; "screen_room_details_add_topic_title" = "Adicionar tópico"; "screen_room_details_already_a_member" = "Já é membro"; "screen_room_details_already_invited" = "Já foi convidado"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Falha ao ativar o som desta sala. Tente novamente."; "screen_room_details_notification_mode_custom" = "Personalizado"; "screen_room_details_notification_mode_default" = "Padrão"; -"screen_room_details_notification_title" = "Notificações"; "screen_room_details_share_room_title" = "Compartilhar sala"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "Atualizando a sala..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Desbloquear"; "screen_room_member_details_unblock_alert_description" = "Você poderá ver todas as mensagens deles novamente."; "screen_room_member_details_unblock_user" = "Desbloquear usuário"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Banir"; "screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; "screen_room_member_list_ban_member_confirmation_title" = "Tem certeza de que quer banir este membro?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banindo %1$@"; "screen_room_member_list_manage_member_ban" = "Remover e banir membro"; "screen_room_member_list_manage_member_remove" = "Remover da sala"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remover e banir membro"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Somente remover membro"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remover membro e banir de entrar novamente no futuro?"; "screen_room_member_list_manage_member_unban_action" = "Desbanir"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Mostrar menos"; "screen_room_timeline_message_copied" = "Mensagem copiada"; "screen_room_timeline_no_permission_to_post" = "Você não tem permissão para postar nesta sala"; -"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_room_timeline_reactions_show_more" = "Mostrar mais"; "screen_room_timeline_read_marker_title" = "Novo"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Marcar como lido"; "screen_roomlist_mark_as_unread" = "Marcar como não lido"; "screen_roomlist_room_directory_button_title" = "Navegar por todas as salas"; -"screen_server_confirmation_change_server" = "Alterar provedor da conta"; "screen_server_confirmation_message_login_element_dot_io" = "Um servidor privado para funcionários do Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "A Matrix é uma rede aberta para comunicação segura e descentralizada."; "screen_server_confirmation_message_register" = "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Comparar números"; "screen_session_verification_complete_subtitle" = "Sua nova sessão está agora verificada. Ela tem acesso às suas mensagens criptografadas e outros usuários a verão como confiável."; "screen_session_verification_enter_recovery_key" = "Insira a chave de recuperação"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Prove que é você para acessar seu histórico de mensagens criptografadas."; "screen_session_verification_open_existing_session_title" = "Abrir uma sessão existente"; "screen_session_verification_positive_button_canceled" = "Repetir verificação"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Esperando para combinar"; "screen_session_verification_ready_subtitle" = "Compare um conjunto único de emojis."; "screen_session_verification_request_accepted_subtitle" = "Compare os emojis únicos, garantindo que apareçam na mesma ordem."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Eles não combinam"; "screen_session_verification_they_match" = "Eles combinam"; "screen_session_verification_waiting_to_accept_subtitle" = "Aceite a solicitação para iniciar o processo de verificação em sua outra sessão para continuar."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; "screen_signout_key_backup_disabled_title" = "Você desativou o backup"; "screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; -"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; "screen_signout_key_backup_ongoing_title" = "O backup das suas chaves ainda está em andamento"; "screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; "screen_signout_recovery_disabled_title" = "A recuperação não está configurada"; "screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; -"screen_signout_save_recovery_key_title" = "Você salvou sua chave de recuperação?"; "screen_start_chat_error_starting_chat" = "Ocorreu um erro ao tentar iniciar um chat"; "screen_view_location_title" = "Localização"; "screen_welcome_bullet_1" = "Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano."; @@ -919,7 +952,6 @@ "test_language_identifier" = "pt-br"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Execute testes"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "Enquete"; +"banner_set_up_recovery_submit" = "Configurar a recuperação"; "dialog_title_error" = "Erro"; "dialog_title_success" = "Sucesso"; "notification_fallback_content" = "Notificação"; "notification_invitation_action_join" = "Entrar"; +"notification_invitation_action_reject" = "Recusar"; "notification_room_action_mark_as_read" = "Marcar como lido"; "notification_room_action_quick_reply" = "Resposta rápida"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Todos"; +"screen_account_provider_change" = "Alterar provedor da conta"; "screen_account_provider_signin_subtitle" = "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."; "screen_account_provider_signup_subtitle" = "Aqui é onde suas conversas vão ficar — assim como você usa um provedor de e-mails para manter seus e-mails."; "screen_analytics_settings_help_us_improve" = "Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Você poderá ver todas as mensagens deles novamente."; "screen_blocked_users_unblock_alert_title" = "Desbloquear usuário"; "screen_bug_report_rash_logs_alert_title" = "%1$@ fechou inesperadamente na última vez que foi usado. Gostaria de compartilhar um relatório de falhas conosco?"; +"screen_chat_backup_recovery_action_confirm" = "Insira a chave de recuperação"; +"screen_chat_backup_recovery_action_setup" = "Configurar a recuperação"; +"screen_create_poll_cancel_confirmation_content_ios" = "Suas alterações não serão salvas"; "screen_create_room_add_people_title" = "Convidar pessoas"; "screen_create_room_room_name_label" = "Nome da sala"; "screen_create_room_title" = "Criar uma sala"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Editar enquete"; "screen_identity_use_another_device" = "Usar outro dispositivo"; "screen_login_subtitle" = "A Matrix é uma rede aberta para comunicação segura e descentralizada."; +"screen_notification_settings_mentions_section_title" = "Menções"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Tente novamente"; +"screen_recovery_key_change_generate_key_description" = "Certifique-se de que você pode armazenar sua chave de recuperação em algum lugar seguro"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Bloquear usuário"; +"screen_reset_encryption_password_placeholder" = "Inserir..."; "screen_room_attachment_source_camera_photo" = "Tirar foto"; "screen_room_change_permissions_everyone" = "Todos"; "screen_room_change_permissions_member_moderation" = "Moderação de membros"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administradores"; "screen_room_change_role_section_moderators" = "Moderadores"; "screen_room_change_role_section_users" = "Membros"; +"screen_room_change_role_unsaved_changes_title" = "Salvar alterações?"; "screen_room_details_invite_people_title" = "Convidar pessoas"; "screen_room_details_leave_conversation_title" = "Sair da conversa"; "screen_room_details_leave_room_title" = "Sair da sala"; +"screen_room_details_notification_title" = "Notificações"; "screen_room_details_roles_and_permissions" = "Cargos e permissões"; "screen_room_details_room_name_label" = "Nome da sala"; "screen_room_details_security_title" = "Segurança"; "screen_room_details_topic_title" = "Tópico"; "screen_room_error_failed_processing_media" = "Falha ao processar mídia para upload. Tente novamente."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remover e banir membro"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Somente menções e palavras-chave"; +"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_roomlist_filter_people" = "Pessoas"; +"screen_server_confirmation_change_server" = "Alterar provedor da conta"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Sair"; "screen_signout_confirmation_dialog_title" = "Sair"; +"screen_signout_key_backup_offline_title" = "O backup das suas chaves ainda está em andamento"; "screen_signout_preference_item" = "Sair"; +"screen_signout_save_recovery_key_title" = "Você salvou sua chave de recuperação?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/pt.lproj/Localizable.strings b/ElementX/Resources/Localizations/pt.lproj/Localizable.strings index b385910f11..b4a06c13f7 100644 --- a/ElementX/Resources/Localizations/pt.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pt.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausar"; "a11y_pin_field" = "Campo para PIN"; "a11y_play" = "Reproduzir"; -"a11y_poll" = "Sondagem"; "a11y_poll_end" = "Sondagem concluída"; "a11y_react_with" = "Reagir com %1$@"; "a11y_react_with_other_emojis" = "Reagir com outros emojis"; @@ -27,20 +26,21 @@ "action_back" = "Voltar"; "action_call" = "Chamar"; "action_cancel" = "Cancelar"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Cancelar por enquanto"; "action_choose_photo" = "Escolher foto"; "action_clear" = "Limpar"; "action_close" = "Fechar"; "action_complete_verification" = "Concluir verificação"; "action_confirm" = "Confirmar"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Confirmar palavra-passe"; "action_continue" = "Continuar"; "action_copy" = "Copiar"; "action_copy_link" = "Copiar ligação"; "action_copy_link_to_message" = "Copiar ligação da mensagem"; "action_create" = "Criar"; "action_create_a_room" = "Criar uma sala"; -"action_deactivate" = "Deactivate"; +"action_deactivate" = "Desativar"; +"action_deactivate_account" = "Desativar conta"; "action_decline" = "Rejeitar"; "action_delete_poll" = "Eliminar sondagem"; "action_disable" = "Desativar"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Esqueceu-se da senha?"; "action_forward" = "Reencaminhar"; "action_go_back" = "Voltar"; +"action_ignore" = "Ignorar"; "action_invite" = "Convidar"; "action_invite_friends" = "Convidar pessoas"; "action_invite_friends_to_app" = "Convidar amigos para %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Sair"; "action_leave_conversation" = "Sair da conversa"; "action_leave_room" = "Sair da sala"; +"action_load_more" = "Carrega mais"; "action_manage_account" = "Gerir conta"; "action_manage_devices" = "Gerir dispositivos"; "action_message" = "Enviar mensagem"; @@ -93,6 +95,7 @@ "action_send_message" = "Enviar mensagem"; "action_share" = "Partilhar"; "action_share_link" = "Partilhar ligação"; +"action_show" = "Mostrar"; "action_sign_in_again" = "Iniciar sessão novamente"; "action_signout" = "Terminar sessão"; "action_signout_anyway" = "Terminar mesmo assim"; @@ -105,17 +108,15 @@ "action_tap_for_options" = "Toca para ver as opções"; "action_try_again" = "Tentar novamente"; "action_unpin" = "Desafixar"; -"action_view_in_timeline" = "View in timeline"; +"action_view_in_timeline" = "Ver na cronologia"; "action_view_source" = "Ver fonte"; "action_yes" = "Sim"; -"action.load_more" = "Carrega mais"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "Sair & Atualizar"; +"banner_migrate_to_native_sliding_sync_description" = "O teu servidor suporta agora um protocolo novo e mais rápido. Termina a sessão e volta a iniciar sessão para atualizar agora. Se o fizeres agora, evitarás um fim de sessão forçado quando o protocolo antigo for removido mais tarde."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Seu homeserver não suporta mais o protocolo antigo. Termine sessão e volte a iniciar sessão para continuar a utilizar a aplicação."; +"banner_migrate_to_native_sliding_sync_title" = "Atualização disponível"; +"banner_set_up_recovery_content" = "Gere uma nova chave de recuperação que pode ser usada para restaurar seu histórico de mensagens criptografadas caso você perca o acesso aos seus dispositivos."; +"banner_set_up_recovery_title" = "Configurar a recuperação"; "common_about" = "Sobre"; "common_acceptable_use_policy" = "Política de utilização aceitável"; "common_advanced_settings" = "Configurações avançadas"; @@ -133,10 +134,12 @@ "common_dark" = "Escuro"; "common_decryption_error" = "Erro de decifragem"; "common_developer_options" = "Opções de programador"; +"common_device_id" = "ID do dispositivo"; "common_direct_chat" = "Conversa direta"; "common_edited_suffix" = "(editada)"; "common_editing" = "A editar"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encriptação"; "common_encryption_enabled" = "Cifragem ativada"; "common_enter_your_pin" = "Introduz o teu PIN"; "common_error" = "Erro"; @@ -147,6 +150,7 @@ "common_favourited" = "Favoritas"; "common_file" = "Ficheiro"; "common_forward_message" = "Reencaminhar mensagem"; +"common_frequently_used" = "Frequentemente utilizado"; "common_gif" = "GIF"; "common_image" = "Imagem"; "common_in_reply_to" = "Em resposta a %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderno"; "common_mute" = "Silenciar"; "common_no_results" = "Sem resultados"; +"common_no_room_name" = "Sala sem nome"; "common_offline" = "Desligado"; "common_optic_id_ios" = "Optic ID"; "common_or" = "ou"; @@ -169,7 +174,9 @@ "common_people" = "Pessoas"; "common_permalink" = "Ligação permanente"; "common_permission" = "Permissão"; -"common_please_wait" = "Por favor, aguarda…"; +"common_please_wait" = "Por favor, aguarde…"; +"common_poll_end_confirmation" = "Tens a certeza que queres concluir esta sondagem?"; +"common_poll_summary" = "Sondagem: %1$@"; "common_poll_total_votes" = "Total de votos: %1$@"; "common_poll_undisclosed_text" = "Os resultados serão apresentados após o fim da sondagem"; "common_privacy_policy" = "Política de privacidade"; @@ -200,6 +207,7 @@ "common_settings" = "Configurações"; "common_shared_location" = "Localização partilhada"; "common_signing_out" = "A terminar sessão"; +"common_something_went_wrong" = "Algo correu mal"; "common_starting_chat" = "A iniciar conversa…"; "common_sticker" = "Autocolante"; "common_success" = "Sucesso"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Sobre o que é esta sala?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Incapaz de decifrar"; +"common_unable_to_decrypt_no_access" = "Não tens acesso a esta mensagem"; "common_unable_to_invite_message" = "Não foi possível enviar convites a um ou mais utilizadores."; "common_unable_to_invite_title" = "Não foi possível enviar convite(s)"; "common_unlock" = "Desbloquear"; @@ -221,23 +230,30 @@ "common_username" = "Nome de utilizador"; "common_verification_cancelled" = "Verificação cancelada"; "common_verification_complete" = "Verificação concluída"; +"common_verification_failed" = "A verificação falhou"; +"common_verified" = "Verificado"; +"common_verify_device" = "Verificar o dispositivo"; +"common_verify_identity" = "Verifica a identidade"; "common_video" = "Vídeo"; "common_voice_message" = "Mensagem de voz"; "common_waiting" = "A aguardar…"; "common_waiting_for_decryption_key" = "À espera desta mensagem"; +"common.copied_to_clipboard" = "Copiado para a área de transferência"; "common.do_not_show_this_again" = "Não mostrar novamente"; "common.open_source_licenses" = "Licenças de código aberto"; -"common.pinned" = "Pinned"; +"common.pinned" = "Afixado"; "common.send_to" = "Enviar para"; -"common_no_room_name" = "Sala sem nome"; -"common_poll_end_confirmation" = "Tens a certeza que queres concluir esta sondagem?"; -"common_poll_summary" = "Sondagem: %1$@"; -"common_something_went_wrong" = "Algo correu mal"; -"common_unable_to_decrypt_no_access" = "Não tens acesso a esta mensagem"; -"common_verify_device" = "Verificar o dispositivo"; -"confirm_recovery_key_banner_message" = "A tua cópia de segurança das conversas está atualmente dessincronizada. Tens de inserir a tua chave de recuperação para manteres o acesso à cópia."; -"confirm_recovery_key_banner_title" = "Insere a tua chave de recuperação"; +"common.you" = "Você"; +"common_unable_to_decrypt_insecure_device" = "Enviado de um dispositivo inseguro"; +"common_unable_to_decrypt_verification_violation" = "A identidade verificada do remetente foi alterada"; +"confirm_recovery_key_banner_message" = "Confirma a tua chave de recuperação para manteres o acesso ao teu armazenamento de chaves e ao histórico de mensagens."; +"confirm_recovery_key_banner_primary_button_title" = "Introduz a tua chave de recuperação"; +"confirm_recovery_key_banner_secondary_button_title" = "Esqueceste-te da tua chave de recuperação?"; +"confirm_recovery_key_banner_title" = "O teu armazenamento de chaves não está sincronizado"; "crash_detection_dialog_content" = "A %1$@ teve uma falha da última vez que foi utilizada. Gostarias de partilhar um relatório de acidente connosco?"; +"crypto_identity_change_pin_violation" = "A identidade de %1$@ parece ter mudado. %2$@"; +"crypto_identity_change_pin_violation_new" = "A identidade de %1$@ (username: %2$@ ) aparenta ter mudado. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Para que a aplicação possa utilizar a câmara, concede a permissão nas configurações do sistema."; "dialog_permission_generic" = "Concede a permissão nas configurações do sistema."; "dialog_permission_location_description_ios" = "Concede a permissão em Definições -> Localização."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Caras e Pessoas"; "emoji_picker_category_places" = "Viagens e Lugares"; "emoji_picker_category_symbols" = "Símbolos"; -"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation."; +"error_account_creation_not_possible" = "Seu homeserver precisa ser atualizado para suportar o Matrix Authentication Service e a criação de conta."; "error_failed_creating_the_permalink" = "Falha ao criar ligação permanente"; "error_failed_loading_map" = "%1$@ não foi possível carregar o mapa. Por favor, tente novamente mais tarde."; "error_failed_loading_messages" = "Falha ao carregar mensagens"; @@ -267,10 +283,10 @@ "error_message_not_found" = "Mensagem não encontrada"; "error_no_compatible_app_found" = "Nenhuma aplicação encontrada capaz de continuar esta ação."; "error_some_messages_have_not_been_sent" = "Algumas mensagens não foram enviadas"; -"error_unknown" = "Ocorreu um erro, desculpa"; +"error_unknown" = "Desculpe, ocorreu um erro"; "event_shield_reason_authenticity_not_guaranteed" = "A autenticidade desta mensagem cifrada não pode ser garantida neste dispositivo."; -"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; -"event_shield_reason_sent_in_clear" = "Not encrypted."; +"event_shield_reason_previously_verified" = "Criptografado por um usuário verificado anteriormente."; +"event_shield_reason_sent_in_clear" = "Não cifrado."; "event_shield_reason_unknown_device" = "Cifragem com origem num dispositivo eliminado ou desconhecido."; "event_shield_reason_unsigned_device" = "Cifragem com origem num dispositivo não verificado pelo seu dono."; "event_shield_reason_unverified_identity" = "Cifragem com origem num utilizador não verificado."; @@ -290,16 +306,15 @@ "notification_channel_silent" = "Notificações silenciosas"; "notification_incoming_call" = "Chamada recebida"; "notification_inline_reply_failed" = "** Falha no envio - por favor abre a sala"; -"notification_invitation_action_reject" = "Rejeitar"; "notification_invite_body" = "Convidou-te para conversar"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ convidou-o para conversar"; "notification_mentioned_you_body" = "Mencionou-te: %1$@"; "notification_new_messages" = "Mensagens novas"; "notification_reaction_body" = "Reagiu com %1$@"; "notification_room_invite_body" = "Convidou-te a entrar na sala"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ convidou-o a juntar-se à sala"; "notification_sender_me" = "Eu"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@ mencionou ou respondeu"; "notification_test_push_notification_content" = "Estás a ver a notificação! Clica em mim!"; "notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,31 +344,46 @@ "rich_text_editor_unindent" = "Desindentar"; "rich_text_editor_url_placeholder" = "Ligação"; "rich_text_editor_a11y_add_attachment" = "Adicionar anexo"; +"rich_text_editor_composer_caption_placeholder" = "Legenda opcional..."; "screen_advanced_settings_element_call_base_url" = "URL base para Element Call personalizado"; "screen_advanced_settings_element_call_base_url_description" = "Define um URL base para a Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL inválido, certifica-te de que incluis o protocolo (http/https) e o endereço correto."; -"screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; -"screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; -"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; -"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; -"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; -"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; -"screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; -"screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; +"screen_create_room_room_address_section_footer" = "Para que esta sala seja visível no diretório público de salas, precisas de um endereço de sala."; +"screen_create_room_room_address_section_title" = "Endereço da sala"; +"screen_create_room_room_visibility_section_title" = "Visibilidade da sala"; +"screen_create_room_access_section_anyone_option_description" = "Qualquer pessoa pode entrar nesta sala"; +"screen_create_room_access_section_anyone_option_title" = "Qualquer pessoa"; +"screen_create_room_access_section_header" = "Acesso à sala"; +"screen_create_room_access_section_knocking_option_description" = "Qualquer pessoa pode pedir para entrar na sala, mas um administrador ou um moderador terá de aceitar o pedido"; +"screen_create_room_access_section_knocking_option_title" = "Pedir para participar"; +"screen_join_room_cancel_knock_action" = "Cancelar pedido"; +"screen_join_room_cancel_knock_alert_confirmation" = "Sim, cancelar"; +"screen_join_room_cancel_knock_alert_description" = "Tens a certeza de que queres cancelar o teu pedido de entrada nesta sala?"; +"screen_join_room_cancel_knock_alert_title" = "Cancela o pedido de adesão"; +"screen_join_room_knock_message_description" = "Mensagem (opcional)"; +"screen_join_room_knock_sent_description" = "Irá receber um convite para participar na sala se seu pedido for aceite."; +"screen_join_room_knock_sent_title" = "Pedido de adesão enviado"; +"screen_pinned_timeline_empty_state_description" = "Pressione uma mensagem e escolha \"%1$@\" para incluir aqui."; +"screen_pinned_timeline_empty_state_headline" = "Fixa mensagens importantes para que possam ser facilmente descobertas"; +"screen_reset_encryption_password_error" = "Um erro desconhecido aconteceu. Verifique se a senha da sua conta está correta e tente novamente."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Retirar verificação e enviar"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Você pode retirar sua verificação e enviar esta mensagem de qualquer maneira, ou você pode cancelar por enquanto e tentar novamente mais tarde depois de verificar novamente %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "A sua mensagem não foi enviada porque a identidade verificada de %1$@ foi alterada"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Enviar mensagem mesmo assim"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ está usando um ou mais dispositivos não verificados. Você pode enviar a mensagem de qualquer maneira, ou você pode cancelar por enquanto e tentar novamente mais tarde depois que %2$@ tiver verificado todos os seus dispositivos."; +"screen_resolve_send_failure_unsigned_device_title" = "A sua mensagem não foi enviada porque %1$@ não verificou todos os dispositivos"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Um ou mais dos seus dispositivos não são verificados. Você pode enviar a mensagem de qualquer maneira, ou você pode cancelar por enquanto e tentar novamente mais tarde depois de ter verificado todos os seus dispositivos."; +"screen_resolve_send_failure_you_unsigned_device_title" = "A sua mensagem não foi enviada porque não verificou um ou mais dos seus dispositivos"; "screen_room_mentions_at_room_subtitle" = "Notificar toda a sala"; "screen_room_pinned_banner_indicator" = "%1$@ de %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ mensagens afixadas"; -"screen_room_pinned_banner_loading_description" = "Loading message…"; +"screen_room_pinned_banner_loading_description" = "A carregar mensagem..."; "screen_room_pinned_banner_view_all_button_title" = "Ver todas"; -"screen_room_details_pinned_events_row_title" = "Pinned messages"; -"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; -"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Alterar operador de conta"; +"screen_room_details_pinned_events_row_title" = "Mensagens afixadas"; +"screen_roomlist_knock_event_sent_description" = "Pedido de adesão enviado"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Mensagem não enviada porque a identidade verificada de %1$@ foi alterada."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Mensagem não enviada porque %1$@ não verificou todos os dispositivos."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Mensagem não enviada porque não verificou um ou mais dos seus dispositivos."; "screen_account_provider_form_hint" = "Endereço do servidor"; "screen_account_provider_form_notice" = "Insira um termo para pesquisa ou um endereço."; "screen_account_provider_form_subtitle" = "Pesquisar por uma empresa, comunidade ou servidor privado."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Irás criar uma conta em %@"; "screen_advanced_settings_developer_mode" = "Modo de programador"; "screen_advanced_settings_developer_mode_description" = "Permite o acesso a funcionalidades para programadores."; +"screen_advanced_settings_media_compression_description" = "Carrega fotos e vídeos mais rapidamente e reduz a utilização de dados"; +"screen_advanced_settings_media_compression_title" = "Otimiza a qualidade da mídia"; "screen_advanced_settings_rich_text_editor_description" = "Desativa o editor de texto rico para poderes escrever Markdown manualmente."; "screen_advanced_settings_send_read_receipts" = "Recibos de leitura"; "screen_advanced_settings_send_read_receipts_description" = "Se desativada, os teus recibos de leitura não serão enviados a ninguém. Continuas a receber recibos de leitura de outros utilizadores."; @@ -372,7 +404,7 @@ "screen_analytics_prompt_help_us_improve" = "Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."; "screen_analytics_prompt_read_terms" = "Podes ler todos os nossos termos %1$@."; "screen_analytics_prompt_read_terms_content_link" = "aqui"; -"screen_analytics_prompt_settings" = "Podes desligar qualquer momento"; +"screen_analytics_prompt_settings" = "Pode desactivar a qualquer momento"; "screen_analytics_prompt_third_party_sharing" = "Não partilharemos os teus dados com terceiros"; "screen_analytics_prompt_title" = "Ajude a melhorar a %1$@"; "screen_analytics_settings_share_data" = "Partilhar dados de utilização"; @@ -428,14 +460,16 @@ "screen_change_server_title" = "Seleciona o teu servidor"; "screen_chat_backup_key_backup_action_disable" = "Desativar a cópia de segurança"; "screen_chat_backup_key_backup_action_enable" = "Ativar a cópia de segurança"; -"screen_chat_backup_key_backup_description" = "A cópia de segurança garante que não perdes o teu histórico de mensagens. %1$@."; -"screen_chat_backup_key_backup_title" = "Cópia de segurança"; +"screen_chat_backup_key_backup_description" = "Guarda a tua identidade criptográfica e as chaves de mensagens de forma segura no servidor. Isto permitir-te-á ver o teu histórico de mensagens em qualquer dispositivo novo. %1$@."; +"screen_chat_backup_key_backup_title" = "Armazenamento de chaves"; +"screen_chat_backup_key_storage_disabled_error" = "O armazenamento de chaves deve ser ativado para configurar a recuperação."; +"screen_chat_backup_key_storage_toggle_description" = "Carrega chaves a partir deste dispositivo"; +"screen_chat_backup_key_storage_toggle_title" = "Permite o armazenamento de chaves"; "screen_chat_backup_recovery_action_change" = "Alterar chave de recuperação"; -"screen_chat_backup_recovery_action_confirm" = "Inserir chave de recuperação"; -"screen_chat_backup_recovery_action_confirm_description" = "A tua cópia de segurança das conversas está atualmente dessincronizada."; -"screen_chat_backup_recovery_action_setup" = "Configurar recuperação"; +"screen_chat_backup_recovery_action_change_description" = "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação, caso tenhas perdido todos os teus dispositivos existentes."; +"screen_chat_backup_recovery_action_confirm_description" = "O teu armazenamento de chaves está atualmente dessincronizado."; "screen_chat_backup_recovery_action_setup_description" = "Obtém acesso às tuas mensagens cifradas mesmo se perderes todos os teus dispositivos ou se terminares todas as tuas sessões %1$@."; -"screen_create_account_title" = "Create account"; +"screen_create_account_title" = "Criar conta"; "screen_create_new_recovery_key_list_item_1" = "Abre a %1$@ num computador"; "screen_create_new_recovery_key_list_item_2" = "Iniciar sessão novamente"; "screen_create_new_recovery_key_list_item_3" = "Quando te for pedido para verificares o teu dispositivo, seleciona %1$@"; @@ -447,29 +481,28 @@ "screen_create_poll_anonymous_desc" = "Mostrar resultados só após o da sondagem"; "screen_create_poll_anonymous_headline" = "Ocultar votos"; "screen_create_poll_answer_hint" = "Opção %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "As tuas alterações não serão guardadas"; "screen_create_poll_cancel_confirmation_title_ios" = "Cancelar sondagem"; "screen_create_poll_question_desc" = "Pergunta ou tópico"; "screen_create_poll_question_hint" = "De que trata a sondagem?"; "screen_create_poll_title" = "Criar sondagem"; "screen_create_room_action_create_room" = "Nova sala"; "screen_create_room_error_creating_room" = "Ocorreu um erro ao criar a sala"; -"screen_create_room_private_option_description" = "As mensagens serão cifradas. Uma vez ativada, não é possível desativar a cifragem."; -"screen_create_room_private_option_title" = "Sala privada (entrada apenas por convite)"; -"screen_create_room_public_option_description" = "As mensagens não serão cifradas e qualquer um as poderá ler. É possível ativar a cifragem posteriormente."; -"screen_create_room_public_option_title" = "Sala pública (entrada livre)"; +"screen_create_room_private_option_description" = "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são encriptadas ponta a ponta."; +"screen_create_room_private_option_title" = "Sala privada"; +"screen_create_room_public_option_description" = "Qualquer um pode encontrar esta sala. \nPode alterar esta opção nas definições da sala."; +"screen_create_room_public_option_title" = "Sala pública"; "screen_create_room_topic_label" = "Descrição (opcional)"; -"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; -"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; -"screen_deactivate_account_delete_all_messages_notice" = "Warning: Future users may see incomplete conversations."; -"screen_deactivate_account_description" = "Deactivating your account is %1$@, it will:"; -"screen_deactivate_account_description_bold_part" = "irreversible"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; -"screen_deactivate_account_list_item_1_bold_part" = "Permanently disable"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; -"screen_deactivate_account_list_item_4" = "Your messages will still be visible to registered users but won’t be available to new or unregistered users if you choose to delete them."; -"screen_deactivate_account_title" = "Deactivate account"; +"screen_deactivate_account_confirmation_dialog_content" = "Confirme que pretende desativar a sua conta. Esta ação não pode ser desfeita."; +"screen_deactivate_account_delete_all_messages" = "Eliminar todas as minhas mensagens"; +"screen_deactivate_account_delete_all_messages_notice" = "Aviso: futuros usuários podem ver conversas incompletas."; +"screen_deactivate_account_description" = "A desativação da sua conta é %1$@, irá:"; +"screen_deactivate_account_description_bold_part" = "irreversível"; +"screen_deactivate_account_list_item_1" = "%1$@ sua conta (não pode voltar a iniciar sessão e o seu ID não pode ser reutilizado)."; +"screen_deactivate_account_list_item_1_bold_part" = "Desativar permanentemente"; +"screen_deactivate_account_list_item_2" = "Removê-lo de todas as salas de chat."; +"screen_deactivate_account_list_item_3" = "Exclua as informações da sua conta do nosso servidor de identidade."; +"screen_deactivate_account_list_item_4" = "Suas mensagens ainda estarão visíveis para usuários registrados, mas não estarão disponíveis para usuários novos ou não registrados se você optar por excluí-las."; +"screen_deactivate_account_title" = "Desativar conta"; "screen_edit_poll_delete_confirmation" = "Tens a certeza que queres apagar esta sondagem?"; "screen_edit_profile_display_name" = "Pseudónimo"; "screen_edit_profile_display_name_placeholder" = "O teu pseudónimo"; @@ -477,7 +510,7 @@ "screen_edit_profile_error_title" = "Não foi possível atualizar o perfil"; "screen_edit_profile_title" = "Editar perfil"; "screen_edit_profile_updating_details" = "A atualizar o perfil…"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; +"screen_encryption_reset_action_continue_reset" = "Continuar a reposição"; "screen_encryption_reset_bullet_1" = "Os detalhes da tua conta, contactos, preferências e lista de conversas serão mantidos."; "screen_encryption_reset_bullet_2" = "Perderás o acesso ao teu histórico de mensagens existente"; "screen_encryption_reset_bullet_3" = "Necessitarás de verificar todos os teus dispositivos e contactos novamente."; @@ -492,9 +525,9 @@ "screen_identity_confirmed_subtitle" = "Agora podes ler ou enviar mensagens de forma segura, e qualquer pessoa com quem converses também pode confiar neste dispositivo."; "screen_identity_confirmed_title" = "Dispositivo verificado"; "screen_identity_waiting_on_other_device" = "A aguardar por outros dispositivos…"; -"screen_invites_decline_chat_message" = "Tens a certeza que queres rejeitar o convite para %1$@?"; -"screen_invites_decline_chat_title" = "Rejeitar conite"; -"screen_invites_decline_direct_chat_message" = "Tens a certeza que queres rejeitar esta conversa privada com %1$@?"; +"screen_invites_decline_chat_message" = "Tens a certeza que queres rejeitar o convite para entra em %1$@?"; +"screen_invites_decline_chat_title" = "Rejeitar convite"; +"screen_invites_decline_direct_chat_message" = "Tem a certeza que queres rejeitar esta conversa privada com %1$@?"; "screen_invites_decline_direct_chat_title" = "Rejeitar conversa"; "screen_invites_empty_list" = "Sem convites"; "screen_invites_invited_you" = "%1$@ (%2$@) convidou-te"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "De grupo"; "screen_notification_settings_invite_for_me_label" = "Convites"; "screen_notification_settings_mentions_only_disclaimer" = "O teu servidor não suporta esta opção em salas cifradas, pelo que poderás não ser notificado em algumas salas."; -"screen_notification_settings_mentions_section_title" = "Menções"; "screen_notification_settings_mode_all" = "Tudo"; "screen_notification_settings_mode_mentions" = "Menções"; "screen_notification_settings_notification_section_title" = "Conversas"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Seleciona %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Ligar novo dispositivo”"; "screen_qr_code_login_initial_state_item_4" = "Lê o código QR com este dispositivo"; +"screen_qr_code_login_initial_state_subtitle" = "Disponível apenas se o seu fornecedor de conta o suportar."; "screen_qr_code_login_initial_state_title" = "Abre a %1$@ noutro dispositivo para obteres o código QR"; "screen_qr_code_login_invalid_scan_state_description" = "Lê o código QR apresentado no outro dispositivo."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Código QR inválido"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "O teu código de verificação"; "screen_recovery_key_change_description" = "Obtém uma nova chave de recuperação se tiveres perdido a atual. Depois de a alterares, a antiga deixará de funcionar."; "screen_recovery_key_change_generate_key" = "Gerar uma nova chave de recuperação"; -"screen_recovery_key_change_generate_key_description" = "Certifica-te de que podes guardar a tua chave de recuperação num local seguro"; "screen_recovery_key_change_success" = "Chave de recuperação alterada"; "screen_recovery_key_change_title" = "Alterar a chave de recuperação?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Criar nova chave de recuperação"; "screen_recovery_key_confirm_description" = "Certifica-te de que ninguém consegue ver esta página!"; -"screen_recovery_key_confirm_error_content" = "Por favor, tenta novamente para confirmar o acesso à tua cópia de segurança das conversas."; +"screen_recovery_key_confirm_error_content" = "Tenta novamente para confirmar o acesso ao teu armazenamento de chaves."; "screen_recovery_key_confirm_error_title" = "Chave de recuperação incorreta"; "screen_recovery_key_confirm_key_description" = "Também funciona se tiveres uma chave ou frase de segurança."; "screen_recovery_key_confirm_key_placeholder" = "Inserir..."; "screen_recovery_key_confirm_lost_recovery_key" = "Perdeste a tua chave?"; "screen_recovery_key_confirm_success" = "Chave de recuperação confirmada"; -"screen_recovery_key_confirm_title" = "Insere a tua chave de recuperação"; "screen_recovery_key_copied_to_clipboard" = "Chave de recuperação copiada"; "screen_recovery_key_generating_key" = "A gerar…"; "screen_recovery_key_save_action" = "Guardar chave"; -"screen_recovery_key_save_description" = "Anota a tua chave de recuperação num local seguro ou guarda-a num gestor de senhas."; +"screen_recovery_key_save_description" = "Anota esta chave de recuperação num local seguro, como um gestor de palavras-passe, uma nota encriptada ou um cofre físico."; "screen_recovery_key_save_key_description" = "Toca para copiar a chave de recuperação"; "screen_recovery_key_save_title" = "Guarda a tua chave de recuperação"; "screen_recovery_key_setup_confirmation_description" = "Não poderás aceder à tua nova chave de recuperação após este passo."; "screen_recovery_key_setup_confirmation_title" = "Guardaste a tua chave de recuperação?"; "screen_recovery_key_setup_description" = "A tua cópia de segurança das conversas está protegida por uma chave de recuperação. Se precisares de uma nova chave após a configuração, podes recriá-la selecionando \"Alterar chave de recuperação\"."; "screen_recovery_key_setup_generate_key" = "Gerar a tua chave de recuperação"; -"screen_recovery_key_setup_generate_key_description" = "Certifica-te de que podes guardar a tua chave de recuperação num local seguro"; +"screen_recovery_key_setup_generate_key_description" = "Não partilhes isto com ninguém!"; "screen_recovery_key_setup_success" = "Recuperação configurada com sucesso"; "screen_recovery_key_setup_title" = "Configurar recuperação"; "screen_report_content_block_user_hint" = "Ativar para esconder todas as atuais e futuras mensagens deste utilizador"; @@ -636,11 +667,10 @@ "screen_reset_encryption_confirmation_alert_action" = "Sim, repor agora"; "screen_reset_encryption_confirmation_alert_subtitle" = "Este processo é irreversível."; "screen_reset_encryption_confirmation_alert_title" = "Tens a certeza que pretendes repor a tua cifra?"; -"screen_reset_encryption_password_placeholder" = "Inserir…"; "screen_reset_encryption_password_subtitle" = "Confirma que pretendes realmente repor a tua cifra."; "screen_reset_encryption_password_title" = "Insere a tua palavra-passe para continuares"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_identity_confirmation_subtitle" = "Está prestes a aceder à sua conta %1$@ para repor a sua identidade. Depois, você será levado de volta ao aplicativo."; +"screen_reset_identity_confirmation_title" = "Não consegue confirmar? Aceda à sua conta para repor a sua identidade."; "screen_room_alias_resolver_resolve_alias_failure" = "Não foi possível encontrar esse endereço de sala"; "screen_room_attachment_source_camera" = "Câmara"; "screen_room_attachment_source_camera_video" = "Gravar vídeo"; @@ -659,7 +689,7 @@ "screen_room_change_permissions_room_name" = "Altera o nome da sala"; "screen_room_change_permissions_room_topic" = "Alterar a descrição da sala"; "screen_room_change_permissions_send_messages" = "Enviar mensagens"; -"screen_room_change_role_administrators_title" = "Editar administradores"; +"screen_room_change_role_administrators_title" = "Editar Administradores"; "screen_room_change_role_confirm_add_admin_description" = "Não poderás desfazer esta ação. Estás a promover o utilizador para ter o mesmo nível de poder que tu."; "screen_room_change_role_confirm_add_admin_title" = "Adicionar administrador?"; "screen_room_change_role_confirm_demote_self_action" = "Despromover"; @@ -667,9 +697,8 @@ "screen_room_change_role_confirm_demote_self_title" = "Despromover-te?"; "screen_room_change_role_invited_member_name" = "%1$@ (pendente)"; "screen_room_change_role_moderators_admin_section_footer" = "Os administradores têm automaticamente privilégios de moderador"; -"screen_room_change_role_moderators_title" = "Editar moderadores"; +"screen_room_change_role_moderators_title" = "Editar Moderadores"; "screen_room_change_role_unsaved_changes_description" = "Tens alterações por guardar."; -"screen_room_change_role_unsaved_changes_title" = "Guardar alterações?"; "screen_room_details_add_topic_title" = "Adicionar descrição"; "screen_room_details_already_a_member" = "Já é participante"; "screen_room_details_already_invited" = "Já foi convidado"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Não foi possível dessilenciar esta sala, por favor tenta novamente."; "screen_room_details_notification_mode_custom" = "Personalizado"; "screen_room_details_notification_mode_default" = "Predefinição"; -"screen_room_details_notification_title" = "Notificações"; "screen_room_details_share_room_title" = "Partilhar sala"; "screen_room_details_title" = "Informação da sala"; "screen_room_details_updating_room" = "A atualizar sala…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Desbloquear"; "screen_room_member_details_unblock_alert_description" = "Poderás voltar a ver todas as suas mensagens."; "screen_room_member_details_unblock_user" = "Desbloquear utilizador"; +"screen_room_member_details_verify_button_subtitle" = "Utiliza a aplicação Web para verificar este utilizador."; +"screen_room_member_details_verify_button_title" = "Verifique %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Banir"; "screen_room_member_list_ban_member_confirmation_description" = "Não poderão voltar a entrar nesta sala, mesmo se forem convidados."; "screen_room_member_list_ban_member_confirmation_title" = "Tens a certeza que queres banir este participante?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "A banir %1$@"; "screen_room_member_list_manage_member_ban" = "Remover e banir participante"; "screen_room_member_list_manage_member_remove" = "Remover da sala"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remover e banir"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Remover apenas"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remover participante e proibir de se juntar no futuro?"; "screen_room_member_list_manage_member_unban_action" = "Anular banimento"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Mostrar menos"; "screen_room_timeline_message_copied" = "Mensagem copiada"; "screen_room_timeline_no_permission_to_post" = "Não tens permissão para publicar nesta sala"; -"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_room_timeline_reactions_show_more" = "Mostrar mais"; "screen_room_timeline_read_marker_title" = "Novas"; "screen_room_title" = "Conversa"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Marcar como lida"; "screen_roomlist_mark_as_unread" = "Marcar como não lida"; "screen_roomlist_room_directory_button_title" = "Consultar lista completa de salas"; -"screen_server_confirmation_change_server" = "Alterar operador de conta"; "screen_server_confirmation_message_login_element_dot_io" = "Um servidor privado para funcionários da Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "A Matrix é uma rede aberta de comunicação descentralizada e segura."; "screen_server_confirmation_message_register" = "É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Comparar números"; "screen_session_verification_complete_subtitle" = "A tua nova sessão está agora verificada, pelo que tem acesso às tuas mensagens cifradas e os outros utilizadores vão vê-la como de confiança."; "screen_session_verification_enter_recovery_key" = "Insere a chave de recuperação"; +"screen_session_verification_failed_subtitle" = "O pedido expirou, o pedido foi recusado ou houve um erro de verificação."; "screen_session_verification_open_existing_session_subtitle" = "Prova que és tu para acederes ao teu histórico de mensagens cifradas."; "screen_session_verification_open_existing_session_title" = "Abrir sessão existente"; "screen_session_verification_positive_button_canceled" = "Repetir verificação"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "A aguardar correspondência"; "screen_session_verification_ready_subtitle" = "Compara um conjunto único de emojis."; "screen_session_verification_request_accepted_subtitle" = "Compara os emojis únicos, certificando-te de que aparecem pela mesma ordem."; +"screen_session_verification_request_details_timestamp" = "Sessão iniciada"; +"screen_session_verification_request_failure_title" = "A verificação falhou"; +"screen_session_verification_request_footer" = "Continue apenas se tiver iniciado esta verificação."; +"screen_session_verification_request_subtitle" = "Verifique o outro dispositivo para manter o histórico de mensagens seguro."; +"screen_session_verification_request_success_subtitle" = "Agora podes ler ou enviar mensagens de forma segura no teu outro dispositivo."; +"screen_session_verification_request_success_title" = "Dispositivo verificado"; +"screen_session_verification_request_title" = "Verificação solicitada"; "screen_session_verification_they_dont_match" = "Não correspondem"; "screen_session_verification_they_match" = "Correspondem"; "screen_session_verification_waiting_to_accept_subtitle" = "Para continuar, aceita o pedido de verificação na tua outra sessão."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas."; "screen_signout_key_backup_disabled_title" = "Desativaste a cópia de segurança"; "screen_signout_key_backup_offline_subtitle" = "As tuas chaves ainda estavam a ser guardadas quando ficaste desligado. Volta a ligar-te para que as tuas chaves possam ser guardadas antes de encerrares a sessão."; -"screen_signout_key_backup_offline_title" = "As tuas chaves ainda estão a ser guardadas"; "screen_signout_key_backup_ongoing_subtitle" = "Por favor, aguarda a conclusão desta operação antes de terminares a sessão."; "screen_signout_key_backup_ongoing_title" = "As tuas chaves ainda estão a ser guardadas"; "screen_signout_recovery_disabled_subtitle" = "Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas."; "screen_signout_recovery_disabled_title" = "Recuperação não configurada"; "screen_signout_save_recovery_key_subtitle" = "Estás prestes a terminar a tua última sessão. Se continuares, poderás perder o acesso às tuas mensagens cifradas."; -"screen_signout_save_recovery_key_title" = "Guardaste a tua chave de recuperação?"; "screen_start_chat_error_starting_chat" = "Ocorreu um erro ao tentar iniciar uma conversa"; "screen_view_location_title" = "Localização"; "screen_welcome_bullet_1" = "Chamadas, sondagens, pesquisa e mais funcionalidades vão ser adicionadas ao longo do ano."; @@ -919,7 +952,6 @@ "test_language_identifier" = "pt"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Resolução de problemas"; -"troubleshoot_notifications_entry_point_title" = "Corrigir notificações"; "troubleshoot_notifications_screen_action" = "Correr testes"; "troubleshoot_notifications_screen_action_again" = "Correr testes novamente"; "troubleshoot_notifications_screen_failure" = "Alguns testes falharam. Por favor, verifica os detalhes."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Certifica que os distribuidores UnifiedPush estão disponíveis."; "troubleshoot_notifications_test_unified_push_failure" = "Nenhum distribuidor encontrado."; "troubleshoot_notifications_test_unified_push_title" = "Verificar UnifiedPush"; +"a11y_poll" = "Sondagem"; +"banner_set_up_recovery_submit" = "Configurar recuperação"; "dialog_title_error" = "Erro"; "dialog_title_success" = "Sucesso"; "notification_fallback_content" = "Notificação"; "notification_invitation_action_join" = "Entrar"; +"notification_invitation_action_reject" = "Rejeitar"; "notification_room_action_mark_as_read" = "Marcar como lida"; "notification_room_action_quick_reply" = "Resposta rápida"; +"screen_pinned_timeline_screen_title_empty" = "Mensagens afixadas"; "screen_room_mentions_at_room_title" = "Toda a gente"; +"screen_account_provider_change" = "Alterar operador de conta"; "screen_account_provider_signin_subtitle" = "É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail."; "screen_account_provider_signup_subtitle" = "É aqui que as tuas conversas vão ficar — tal como num serviço de e-mail."; "screen_analytics_settings_help_us_improve" = "Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Poderás voltar a ver todas as suas mensagens."; "screen_blocked_users_unblock_alert_title" = "Desbloquear utilizador"; "screen_bug_report_rash_logs_alert_title" = "A %1$@ teve uma falha da última vez que foi utilizada. Gostarias de partilhar um relatório de acidente connosco?"; +"screen_chat_backup_recovery_action_confirm" = "Insere a chave de recuperação"; +"screen_chat_backup_recovery_action_setup" = "Configurar recuperação"; +"screen_create_poll_cancel_confirmation_content_ios" = "As tuas alterações não serão guardadas"; "screen_create_room_add_people_title" = "Convidar pessoas"; "screen_create_room_room_name_label" = "Nome da sala"; "screen_create_room_title" = "Criar uma sala"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Editar sondagem"; "screen_identity_use_another_device" = "Utilizar outro dispositivo"; "screen_login_subtitle" = "A Matrix é uma rede aberta de comunicação descentralizada e segura."; +"screen_notification_settings_mentions_section_title" = "Menções"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Tentar novamente"; +"screen_recovery_key_change_generate_key_description" = "Não partilhes isto com ninguém!"; +"screen_recovery_key_confirm_title" = "Introduz a tua chave de recuperação"; "screen_report_content_block_user" = "Bloquear utilizador"; +"screen_reset_encryption_password_placeholder" = "Inserir..."; "screen_room_attachment_source_camera_photo" = "Tirar foto"; "screen_room_change_permissions_everyone" = "Toda a gente"; "screen_room_change_permissions_member_moderation" = "Moderação de participantes"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administradores"; "screen_room_change_role_section_moderators" = "Moderadores"; "screen_room_change_role_section_users" = "Participantes"; +"screen_room_change_role_unsaved_changes_title" = "Guardar alterações?"; "screen_room_details_invite_people_title" = "Convidar pessoas"; "screen_room_details_leave_conversation_title" = "Sair da conversa"; "screen_room_details_leave_room_title" = "Sair da sala"; +"screen_room_details_notification_title" = "Notificações"; "screen_room_details_roles_and_permissions" = "Cargos e permissões"; "screen_room_details_room_name_label" = "Nome da sala"; "screen_room_details_security_title" = "Segurança"; "screen_room_details_topic_title" = "Descrição"; "screen_room_error_failed_processing_media" = "Falha ao processar multimédia para carregamento, por favor tente novamente."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remover e banir participante"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Menções ou palavras-chave"; +"screen_room_timeline_reactions_show_less" = "Mostrar menos"; "screen_roomlist_filter_people" = "Pessoas"; +"screen_server_confirmation_change_server" = "Alterar operador de conta"; +"screen_session_verification_request_failure_subtitle" = "O pedido expirou, o pedido foi recusado ou houve um erro de verificação."; "screen_signout_confirmation_dialog_submit" = "Terminar sessão"; "screen_signout_confirmation_dialog_title" = "Terminar sessão"; +"screen_signout_key_backup_offline_title" = "As tuas chaves ainda estão a ser guardadas"; "screen_signout_preference_item" = "Terminar sessão"; +"screen_signout_save_recovery_key_title" = "Guardaste a tua chave de recuperação?"; +"troubleshoot_notifications_entry_point_title" = "Corrigir notificações"; diff --git a/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict index be8835fae3..eb8934967a 100644 --- a/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict @@ -205,9 +205,9 @@ NSStringFormatValueTypeKey d one - %1$d Pinned message + %1$d Mensagem afixada other - %1$d Pinned messages + %1$d Mensagens afixadas screen_room_member_list_header_title diff --git a/ElementX/Resources/Localizations/ro.lproj/Localizable.strings b/ElementX/Resources/Localizations/ro.lproj/Localizable.strings index 595d4b3690..fdaff848fe 100644 --- a/ElementX/Resources/Localizations/ro.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ro.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pauză"; "a11y_pin_field" = "Câmp PIN"; "a11y_play" = "Redați"; -"a11y_poll" = "Sondaj"; "a11y_poll_end" = "Sondaj încheiat"; "a11y_react_with" = "Reacționați cu %1$@"; "a11y_react_with_other_emojis" = "Reacționați cu alte emoji-uri"; @@ -41,6 +40,7 @@ "action_create" = "Creați"; "action_create_a_room" = "Creați o cameră"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Refuzați"; "action_delete_poll" = "Ștergeți sondajul"; "action_disable" = "Dezactivați"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Ați uitat parola?"; "action_forward" = "Redirecționați"; "action_go_back" = "Înapoi"; +"action_ignore" = "Ignore"; "action_invite" = "Invitați"; "action_invite_friends" = "Invitați prieteni"; "action_invite_friends_to_app" = "Invitați prieteni în %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Părăsiți"; "action_leave_conversation" = "Părăsiți conversația"; "action_leave_room" = "Părăsiți camera"; +"action_load_more" = "Încărcați mai mult"; "action_manage_account" = "Administrare cont"; "action_manage_devices" = "Gestionare dispozitive"; "action_message" = "Mesaj"; @@ -93,6 +95,7 @@ "action_send_message" = "Trimiteți mesajul"; "action_share" = "Partajați"; "action_share_link" = "Partajați linkul"; +"action_show" = "Show"; "action_sign_in_again" = "Autentificați-vă din nou"; "action_signout" = "Deconectați-vă"; "action_signout_anyway" = "Deconectați-vă oricum"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Vedeți sursă"; "action_yes" = "Da"; -"action.load_more" = "Încărcați mai mult"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Despre"; "common_acceptable_use_policy" = "Politică de utilizare rezonabilă"; "common_advanced_settings" = "Setări avansate"; @@ -133,10 +134,12 @@ "common_dark" = "Întunecat"; "common_decryption_error" = "Eroare de decriptare"; "common_developer_options" = "Opțiuni programator"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Chat direct"; "common_edited_suffix" = "(editat)"; "common_editing" = "Editare"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Criptare activată"; "common_enter_your_pin" = "Introduceți codul PIN"; "common_error" = "Eroare"; @@ -147,6 +150,7 @@ "common_favourited" = "Favorită"; "common_file" = "Fişier"; "common_forward_message" = "Redirecționați mesajul"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Imagine"; "common_in_reply_to" = "Ca răspuns la %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modern"; "common_mute" = "Dezactivați sunetul"; "common_no_results" = "Niciun rezultat"; +"common_no_room_name" = "Fără nume de cameră"; "common_offline" = "Deconectat"; "common_optic_id_ios" = "Optic ID"; "common_or" = "sau"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalink"; "common_permission" = "Permisiune"; "common_please_wait" = "Va rugam asteptati…"; +"common_poll_end_confirmation" = "Sunteți sigur că doriți să încheiați acest sondaj?"; +"common_poll_summary" = "Sondajul %1$@"; "common_poll_total_votes" = "Total voturi: %1$@"; "common_poll_undisclosed_text" = "Rezultatele vor fi afișate după încheierea sondajului"; "common_privacy_policy" = "Politica de confidențialitate"; @@ -200,6 +207,7 @@ "common_settings" = "Setări"; "common_shared_location" = "Locație partajată"; "common_signing_out" = "Deconectare în curs"; +"common_something_went_wrong" = "Ceva nu a mers bine"; "common_starting_chat" = "Se începe conversația…"; "common_sticker" = "Autocolant"; "common_success" = "Succes"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Despre ce este vorba în această cameră?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Nu s-a putut decripta"; +"common_unable_to_decrypt_no_access" = "Nu aveți acces la acest mesaj"; "common_unable_to_invite_message" = "Nu am putut trimite invitații unuia sau mai multor utilizatori."; "common_unable_to_invite_title" = "Nu s-a putut trimite invitația (invitațiile)"; "common_unlock" = "Deblocare"; @@ -221,23 +230,30 @@ "common_username" = "Utilizator"; "common_verification_cancelled" = "Verificare anulată"; "common_verification_complete" = "Verificare completă"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verificați dispozitivul"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Mesaj vocal"; "common_waiting" = "Se aşteaptă…"; "common_waiting_for_decryption_key" = "Mesaj în așteptare"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Trimiteți către"; -"common_no_room_name" = "Fără nume de cameră"; -"common_poll_end_confirmation" = "Sunteți sigur că doriți să încheiați acest sondaj?"; -"common_poll_summary" = "Sondajul %1$@"; -"common_something_went_wrong" = "Ceva nu a mers bine"; -"common_unable_to_decrypt_no_access" = "Nu aveți acces la acest mesaj"; -"common_verify_device" = "Verificați dispozitivul"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Confirmați cheia de recuperare"; "crash_detection_dialog_content" = "%1$@ s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Pentru a permite aplicației să utilizeze camera, vă rugăm să acordați permisiunea în setările sistemului."; "dialog_permission_generic" = "Vă rugăm să acordați permisiunea în setările sistemului."; "dialog_permission_location_description_ios" = "Acordați accesul în Setări -> Locație."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Notificări silențioase"; "notification_incoming_call" = "Apel primit"; "notification_inline_reply_failed" = "** Trimiterea eșuată - vă rugăm să deschideți camera"; -"notification_invitation_action_reject" = "Respingeți"; "notification_invite_body" = "V-a invitat la o discuție"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "%1$@ v-a menționat"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Dez-identare"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Adăugați un atașament"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Adresa URL de bază Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Setați o adresă URL de bază personalizată pentru Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "URL invalid, vă rugăm să vă asigurați că includeți protocolul (http/https) și adresa corectă."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Schimbați furnizorul contului"; "screen_account_provider_form_hint" = "Adresa Homeserver-ului"; "screen_account_provider_form_notice" = "Introduceţi un termen de căutare sau o adresă de domeniu."; "screen_account_provider_form_subtitle" = "Căutați o companie, o comunitate sau un server privat."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Sunteți pe cale să creați un cont pe %@"; "screen_advanced_settings_developer_mode" = "Modul dezvoltator"; "screen_advanced_settings_developer_mode_description" = "Activați pentru a avea acces la funcționalități pentru dezvoltatori."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Dezactivați editorul avansat pentru a tasta manual Markdown."; "screen_advanced_settings_send_read_receipts" = "Chitanțe de citire"; "screen_advanced_settings_send_read_receipts_description" = "Dacă dezactivată, chitanțele dumneavoastră de citire nu vor fi trimise nimănui. Veți primi în continuare chitanțe de citire de la alți utilizatori."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Activați backupul"; "screen_chat_backup_key_backup_description" = "Backup vă asigură că nu pierdeți istoricul mesajelor. %1$@."; "screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Schimbați cheia de recuperare"; -"screen_chat_backup_recovery_action_confirm" = "Confirmați cheia de recuperare"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Backup-ul pentru chat nu este sincronizat în prezent."; -"screen_chat_backup_recovery_action_setup" = "Configurați recuperarea"; "screen_chat_backup_recovery_action_setup_description" = "Obțineți acces la mesajele dumneavoastră criptate dacă vă pierdeți toate dispozitivele sau sunteți deconectat de la %1$@ peste tot."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Deschideți %1$@ pe un dispozitiv desktop"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Afișați rezultatele numai după încheierea sondajului"; "screen_create_poll_anonymous_headline" = "Sondaj anonim"; "screen_create_poll_answer_hint" = "Opțiune %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Modificările dumneavoastră nu vor fi salvate"; "screen_create_poll_cancel_confirmation_title_ios" = "Renunțați la sondaj"; "screen_create_poll_question_desc" = "Întrebare sau subiect"; "screen_create_poll_question_hint" = "Despre ce este sondajul?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Se actualizează profilul..."; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Discuții de grup"; "screen_notification_settings_invite_for_me_label" = "Invitații"; "screen_notification_settings_mentions_only_disclaimer" = "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."; -"screen_notification_settings_mentions_section_title" = "Mențiuni"; "screen_notification_settings_mode_all" = "Toate"; "screen_notification_settings_mode_mentions" = "Mențiuni"; "screen_notification_settings_notification_section_title" = "Anunță-mă pentru"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Selectați %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "„Conectați un dispozitiv nou”"; "screen_qr_code_login_initial_state_item_4" = "Scanați codul QR cu acest dispozitiv"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Deschideți %1$@ pe un alt dispozitiv pentru a obține codul QR"; "screen_qr_code_login_invalid_scan_state_description" = "Utilizați codul QR afișat pe celălalt dispozitiv."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Cod QR greșit"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Codul dumneavoastră de verificare"; "screen_recovery_key_change_description" = "Obțineți o nouă cheie de recuperare dacă ați pierdut-o pe cea existentă. După schimbarea cheii de recuperare, cea veche nu va mai funcționa."; "screen_recovery_key_change_generate_key" = "Generați o nouă cheie de recuperare"; -"screen_recovery_key_change_generate_key_description" = "Asigurați-vă că puteți stoca cheia de recuperare undeva în siguranță"; "screen_recovery_key_change_success" = "Cheia de recuperare a fost schimbată"; "screen_recovery_key_change_title" = "Schimbați cheia de recuperare?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Creați o nouă cheie de recuperare"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Introduceți..."; "screen_recovery_key_confirm_lost_recovery_key" = "Ați pierdut cheia de recuperare?"; "screen_recovery_key_confirm_success" = "Cheia de recuperare confirmată"; -"screen_recovery_key_confirm_title" = "Confirmați cheia de recuperare"; "screen_recovery_key_copied_to_clipboard" = "Cheia de recuperare copiată"; "screen_recovery_key_generating_key" = "Se generează..."; "screen_recovery_key_save_action" = "Salvați cheia de recuperare"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Administratorii au automat privilegii de moderator"; "screen_room_change_role_moderators_title" = "Editați moderatorii"; "screen_room_change_role_unsaved_changes_description" = "Aveți modificări nesalvate."; -"screen_room_change_role_unsaved_changes_title" = "Salvați modificările?"; "screen_room_details_add_topic_title" = "Adăugare subiect"; "screen_room_details_already_a_member" = "Deja membru"; "screen_room_details_already_invited" = "Deja invitat"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Activarea notificarilor pentru această cameră a eșuat, încercați din nou."; "screen_room_details_notification_mode_custom" = "Personalizat"; "screen_room_details_notification_mode_default" = "Implicit"; -"screen_room_details_notification_title" = "Notificări"; "screen_room_details_share_room_title" = "Partajați camera"; "screen_room_details_title" = "Informatii camera"; "screen_room_details_updating_room" = "Se actualizează camera…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Deblocați"; "screen_room_member_details_unblock_alert_description" = "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."; "screen_room_member_details_unblock_user" = "Deblocați utilizatorul"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Interzicere"; "screen_room_member_list_ban_member_confirmation_description" = "Nu se vor putea alătura din nou acestei camere dacă sunt invitați."; "screen_room_member_list_ban_member_confirmation_title" = "Sunteți sigur că doriți să interziceți acest membru?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Se interzice %1$@"; "screen_room_member_list_manage_member_ban" = "Eliminați și interziceți membrul"; "screen_room_member_list_manage_member_remove" = "Înlăturați membrul"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Înlăturați și interziceți membrul"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Doar înlăturare"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Înlăturați membrul și interziceți-i să se alăture în viitor?"; "screen_room_member_list_manage_member_unban_action" = "Anulare excludere"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Afișați mai puțin"; "screen_room_timeline_message_copied" = "Mesaj copiat"; "screen_room_timeline_no_permission_to_post" = "Nu aveți permisiunea de a posta în această cameră"; -"screen_room_timeline_reactions_show_less" = "Afișați mai puțin"; "screen_room_timeline_reactions_show_more" = "Afișați mai mult"; "screen_room_timeline_read_marker_title" = "Nou"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Marcați ca citită"; "screen_roomlist_mark_as_unread" = "Marcați ca necitită"; "screen_roomlist_room_directory_button_title" = "Răsfoiți toate camerele"; -"screen_server_confirmation_change_server" = "Schimbați furnizorul contului"; "screen_server_confirmation_message_login_element_dot_io" = "Un server privat pentru angajații Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."; "screen_server_confirmation_message_register" = "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Comparați numerele"; "screen_session_verification_complete_subtitle" = "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar alți utilizatori vă vor vedea ca fiind de încredere."; "screen_session_verification_enter_recovery_key" = "Introduceți cheia de recuperare"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Demonstrați-vă identitatea pentru a accesa istoricul mesajelor criptate."; "screen_session_verification_open_existing_session_title" = "Deschideți o sesiune existentă"; "screen_session_verification_positive_button_canceled" = "Reîncercați verificarea"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Se așteaptă confirmarea"; "screen_session_verification_ready_subtitle" = "Comparați un set unic de emoji-uri."; "screen_session_verification_request_accepted_subtitle" = "Comparăți emoticoalene asigurându-vă că apar în aceeași ordine."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Nu se potrivesc"; "screen_session_verification_they_match" = "Se potrivesc"; "screen_session_verification_waiting_to_accept_subtitle" = "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate."; "screen_signout_key_backup_disabled_title" = "Ați dezactivat backup-ul"; "screen_signout_key_backup_offline_subtitle" = "Cheile dumneavoastră erau încă în curs de backup atunci când ați fost deconectat. Reconectați-vă pentru ca cheile dumneavoastră să poată fi salvate înainte de a vă deconecta."; -"screen_signout_key_backup_offline_title" = "Cheile dumneavoastră sunt încă în curs de backup"; "screen_signout_key_backup_ongoing_subtitle" = "Vă rugăm să așteptați până la finalizarea acestui proces înainte de a vă deconecta."; "screen_signout_key_backup_ongoing_title" = "Cheile dumneavoastră sunt încă în curs de backup"; "screen_signout_recovery_disabled_subtitle" = "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, veți pierde accesul la mesajele criptate."; "screen_signout_recovery_disabled_title" = "Recuperarea nu este configurată"; "screen_signout_save_recovery_key_subtitle" = "Sunteți pe cale să vă deconectați de la ultima sesiune. Dacă vă deconectați acum, este posibil să pierdeți accesul la mesajele criptate."; -"screen_signout_save_recovery_key_title" = "Ați salvat cheia de recuperare?"; "screen_start_chat_error_starting_chat" = "A apărut o eroare la încercarea începerii conversației"; "screen_view_location_title" = "Locație"; "screen_welcome_bullet_1" = "Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."; @@ -919,7 +952,6 @@ "test_language_identifier" = "ro"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Depanare"; -"troubleshoot_notifications_entry_point_title" = "Depanați notificările"; "troubleshoot_notifications_screen_action" = "Rulați testele"; "troubleshoot_notifications_screen_action_again" = "Rulați din nou testele"; "troubleshoot_notifications_screen_failure" = "Unele teste au eșuat. Vă rugăm să verificați detaliile."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Asigurați-vă că distribuitorii UnifiedPush sunt disponibili."; "troubleshoot_notifications_test_unified_push_failure" = "Nu au fost găsiți distribuitori push."; "troubleshoot_notifications_test_unified_push_title" = "Verificați UnifiedPush"; +"a11y_poll" = "Sondaj"; +"banner_set_up_recovery_submit" = "Configurați recuperarea"; "dialog_title_error" = "Eroare"; "dialog_title_success" = "Succes"; "notification_fallback_content" = "Notificare"; "notification_invitation_action_join" = "Alăturați-vă"; +"notification_invitation_action_reject" = "Respinge"; "notification_room_action_mark_as_read" = "Marcați ca citită"; "notification_room_action_quick_reply" = "Raspuns rapid"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Toți"; +"screen_account_provider_change" = "Schimbați furnizorul contului"; "screen_account_provider_signin_subtitle" = "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."; "screen_account_provider_signup_subtitle" = "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile."; "screen_analytics_settings_help_us_improve" = "Distribuiți date anonime de utilizare pentru a ne ajuta să identificăm probleme."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."; "screen_blocked_users_unblock_alert_title" = "Deblocați utilizatorul"; "screen_bug_report_rash_logs_alert_title" = "%1$@ s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?"; +"screen_chat_backup_recovery_action_confirm" = "Introduceți cheia de recuperare"; +"screen_chat_backup_recovery_action_setup" = "Configurați recuperarea"; +"screen_create_poll_cancel_confirmation_content_ios" = "Modificările dumneavoastră nu vor fi salvate"; "screen_create_room_add_people_title" = "Invitați prieteni"; "screen_create_room_room_name_label" = "Numele camerei"; "screen_create_room_title" = "Creați o cameră"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Editați sondajul"; "screen_identity_use_another_device" = "Utilizați un alt dispozitiv"; "screen_login_subtitle" = "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată."; +"screen_notification_settings_mentions_section_title" = "Mențiuni"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Încercați din nou"; +"screen_recovery_key_change_generate_key_description" = "Asigurați-vă că puteți stoca cheia de recuperare undeva în siguranță"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Blocați utilizatorul"; +"screen_reset_encryption_password_placeholder" = "Introduceți..."; "screen_room_attachment_source_camera_photo" = "Faceți o fotografie"; "screen_room_change_permissions_everyone" = "Toți"; "screen_room_change_permissions_member_moderation" = "Moderarea membrilor"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administratori"; "screen_room_change_role_section_moderators" = "Moderatori"; "screen_room_change_role_section_users" = "Membri"; +"screen_room_change_role_unsaved_changes_title" = "Salvați modificările?"; "screen_room_details_invite_people_title" = "Invitați prieteni"; "screen_room_details_leave_conversation_title" = "Părăsiți conversația"; "screen_room_details_leave_room_title" = "Părăsiți camera"; +"screen_room_details_notification_title" = "Notificări"; "screen_room_details_roles_and_permissions" = "Roluri și permisiuni"; "screen_room_details_room_name_label" = "Numele camerei"; "screen_room_details_security_title" = "Securitate"; "screen_room_details_topic_title" = "Subiect"; "screen_room_error_failed_processing_media" = "Procesarea datelor media a eșuat, vă rugăm să încercați din nou."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Eliminați și interziceți membrul"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Numai mențiuni și cuvinte cheie"; +"screen_room_timeline_reactions_show_less" = "Afișați mai puțin"; "screen_roomlist_filter_people" = "Persoane"; +"screen_server_confirmation_change_server" = "Schimbați furnizorul contului"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Deconectați-vă"; "screen_signout_confirmation_dialog_title" = "Deconectați-vă"; +"screen_signout_key_backup_offline_title" = "Cheile dumneavoastră sunt încă în curs de backup"; "screen_signout_preference_item" = "Deconectați-vă"; +"screen_signout_save_recovery_key_title" = "Ați salvat cheia de recuperare?"; +"troubleshoot_notifications_entry_point_title" = "Depanați notificările"; diff --git a/ElementX/Resources/Localizations/ru.lproj/Localizable.strings b/ElementX/Resources/Localizations/ru.lproj/Localizable.strings index 4c044fa74f..c4b1205b07 100644 --- a/ElementX/Resources/Localizations/ru.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ru.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Приостановить"; "a11y_pin_field" = "Поле PIN-кода"; "a11y_play" = "Воспроизвести"; -"a11y_poll" = "Опрос"; "a11y_poll_end" = "Опрос завершен"; "a11y_react_with" = "Реагировать вместе с %1$@"; "a11y_react_with_other_emojis" = "Реакция с помощью эмодзи"; @@ -31,7 +30,7 @@ "action_choose_photo" = "Выбрать фото"; "action_clear" = "Очистить"; "action_close" = "Закрыть"; -"action_complete_verification" = "Полная проверка"; +"action_complete_verification" = "Завершите подтверждение"; "action_confirm" = "Подтвердить"; "action_confirm_password" = "Подтвердите пароль"; "action_continue" = "Продолжить"; @@ -40,7 +39,8 @@ "action_copy_link_to_message" = "Скопировать ссылку в сообщение"; "action_create" = "Создать"; "action_create_a_room" = "Создать комнату"; -"action_deactivate" = "Деактивировать"; +"action_deactivate" = "Отключить"; +"action_deactivate_account" = "Отключить учётную запись"; "action_decline" = "Отклонить"; "action_delete_poll" = "Удалить опрос"; "action_disable" = "Отключить"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Забыли пароль?"; "action_forward" = "Переслать"; "action_go_back" = "Вернуться"; +"action_ignore" = "Игнорировать"; "action_invite" = "Пригласить"; "action_invite_friends" = "Пригласить в комнату"; "action_invite_friends_to_app" = "Пригласить в %1$@"; @@ -61,10 +62,11 @@ "action_invites_list" = "Приглашения"; "action_join" = "Присоединиться"; "action_learn_more" = "Подробнее"; -"action_leave" = "Выйти"; +"action_leave" = "Покинуть"; "action_leave_conversation" = "Покинуть беседу"; "action_leave_room" = "Покинуть комнату"; -"action_manage_account" = "Настройки аккаунта"; +"action_load_more" = "Загрузить еще"; +"action_manage_account" = "Настройки учетной записи"; "action_manage_devices" = "Управление устройствами"; "action_message" = "Сообщение"; "action_next" = "Далее"; @@ -93,6 +95,7 @@ "action_send_message" = "Отправить сообщение"; "action_share" = "Поделиться"; "action_share_link" = "Поделиться ссылкой"; +"action_show" = "Показать"; "action_sign_in_again" = "Повторите вход"; "action_signout" = "Выйти"; "action_signout_anyway" = "Все равно выйти"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Просмотр в хронологии"; "action_view_source" = "Показать источник"; "action_yes" = "Да"; -"action.load_more" = "Загрузить еще"; -"action_deactivate_account" = "Отключить учётную запись"; "banner_migrate_to_native_sliding_sync_action" = "Выйти и обновить"; -"banner_migrate_to_native_sliding_sync_description" = "Теперь ваш сервер поддерживает новый, более быстрый протокол. Выйдите из системы и снова войдите в систему для обновления прямо сейчас. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Ваш homeserver больше не поддерживает старый протокол. Пожалуйста, выйдите из системы и войдите снова, чтобы продолжить использование приложения."; +"banner_migrate_to_native_sliding_sync_description" = "Теперь ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Ваш домашний сервер больше не поддерживает старый протокол. Пожалуйста, выйдите и войдите в свою учётную запись снова, чтобы продолжить использование приложения."; "banner_migrate_to_native_sliding_sync_title" = "Доступно обновление"; -"banner.set_up_recovery.content" = "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."; -"banner.set_up_recovery.title" = "Настроить восстановление"; +"banner_set_up_recovery_content" = "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."; +"banner_set_up_recovery_title" = "Для защиты вашего аккаунта рекомендуется настроить восстановление"; "common_about" = "О приложении"; "common_acceptable_use_policy" = "Политика допустимого использования"; "common_advanced_settings" = "Дополнительные настройки"; @@ -130,29 +131,32 @@ "common_copyright" = "Авторское право"; "common_creating_room" = "Создание комнаты…"; "common_current_user_left_room" = "Покинул комнату"; -"common_dark" = "Темная"; +"common_dark" = "Тёмное"; "common_decryption_error" = "Ошибка расшифровки"; "common_developer_options" = "Для разработчика"; +"common_device_id" = "Идентификатор устройства"; "common_direct_chat" = "Личный чат"; "common_edited_suffix" = "(изменено)"; "common_editing" = "Редактирование"; "common_emote" = "%1$@%2$@"; +"common_encryption" = "Шифрование"; "common_encryption_enabled" = "Шифрование включено"; "common_enter_your_pin" = "Введите свой PIN-код"; "common_error" = "Ошибка"; -"common_everyone" = "Для всех"; +"common_everyone" = "Все"; "common_face_id_ios" = "Face ID"; "common_failed" = "Ошибка"; "common_favourite" = "Избранное"; "common_favourited" = "Избранное"; "common_file" = "Файл"; "common_forward_message" = "Переслать сообщение"; +"common_frequently_used" = "Часто используемые"; "common_gif" = "GIF"; "common_image" = "Изображения"; "common_in_reply_to" = "В ответ на %1$@"; "common_invite_unknown_profile" = "Идентификатор Matrix ID не найден, приглашение может быть не получено."; -"common_leaving_room" = "Покинуть комнату"; -"common_light" = "Светлая"; +"common_leaving_room" = "Покидание комнаты"; +"common_light" = "Светлое"; "common_link_copied_to_clipboard" = "Ссылка скопирована в буфер обмена"; "common_loading" = "Загрузка…"; "common_message" = "Сообщение"; @@ -160,22 +164,25 @@ "common_message_layout" = "Оформление сообщения"; "common_message_removed" = "Сообщение удалено"; "common_modern" = "Современный"; -"common_mute" = "Без звука"; +"common_mute" = "Выкл. звук"; "common_no_results" = "Ничего не найдено"; +"common_no_room_name" = "Название комнаты отсутствует"; "common_offline" = "Не в сети"; "common_optic_id_ios" = "Оптический идентификатор"; "common_or" = "или"; "common_password" = "Пароль"; -"common_people" = "Люди"; +"common_people" = "Пользователи"; "common_permalink" = "Постоянная ссылка"; "common_permission" = "Разрешение"; "common_please_wait" = "Подождите..."; +"common_poll_end_confirmation" = "Вы действительно хотите завершить данный опрос?"; +"common_poll_summary" = "Опрос: %1$@"; "common_poll_total_votes" = "Всего голосов: %1$@"; "common_poll_undisclosed_text" = "Результаты будут показаны после завершения опроса"; "common_privacy_policy" = "Политика конфиденциальности"; "common_reaction" = "Реакция"; "common_reactions" = "Реакции"; -"common_recovery_key" = "Ключ восстановления"; +"common_recovery_key" = "Ключ восстановления"; "common_refreshing" = "Обновление…"; "common_replying_to" = "Отвечает на %1$@"; "common_report_a_bug" = "Сообщить об ошибке"; @@ -184,11 +191,11 @@ "common_rich_text_editor" = "Редактор форматированного текста"; "common_room" = "Комната"; "common_room_name" = "Название комнаты"; -"common_room_name_placeholder" = "напр., название вашего проекта"; -"common_saved_changes" = "Сохраненные изменения"; +"common_room_name_placeholder" = "например, название вашего проекта"; +"common_saved_changes" = "Изменения сохранены"; "common_saving" = "Сохранение"; -"common_screen_lock" = "Блокировка экрана"; -"common_search_for_someone" = "Поиск человека"; +"common_screen_lock" = "Блокировка приложения"; +"common_search_for_someone" = "Найти кого-нибудь"; "common_search_results" = "Результаты поиска"; "common_security" = "Безопасность"; "common_seen_by" = "Просмотрено"; @@ -198,14 +205,15 @@ "common_server_not_supported" = "Сервер не поддерживается"; "common_server_url" = "Адрес сервера"; "common_settings" = "Настройки"; -"common_shared_location" = "Делится местонахождением"; +"common_shared_location" = "Поделился местоположением"; "common_signing_out" = "Выход…"; -"common_starting_chat" = "Начало чата…"; +"common_something_went_wrong" = "Что-то пошло не так"; +"common_starting_chat" = "Чат запускается…"; "common_sticker" = "Стикер"; "common_success" = "Успешно"; "common_suggestions" = "Предложения"; "common_syncing" = "Синхронизация"; -"common_system" = "Системная"; +"common_system" = "Системное"; "common_text" = "Текст"; "common_third_party_notices" = "Уведомление о третьей стороне"; "common_thread" = "Обсуждение"; @@ -213,31 +221,39 @@ "common_topic_placeholder" = "О чем эта комната?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Невозможно расшифровать"; +"common_unable_to_decrypt_no_access" = "Вы не имеете доступа к этому сообщению"; "common_unable_to_invite_message" = "Не удалось отправить приглашения одному или нескольким пользователям."; "common_unable_to_invite_title" = "Не удалось отправить приглашение(я)"; "common_unlock" = "Разблокировать"; -"common_unmute" = "Включить звук"; +"common_unmute" = "Вкл. звук"; "common_unsupported_event" = "Неподдерживаемое событие"; "common_username" = "Имя пользователя"; -"common_verification_cancelled" = "Проверка отменена"; -"common_verification_complete" = "Проверка завершена"; +"common_verification_cancelled" = "Подтверждение отменено"; +"common_verification_complete" = "Подтверждение завершено"; +"common_verification_failed" = "Сбой проверки"; +"common_verified" = "Проверено"; +"common_verify_device" = "Подтверждение устройства"; +"common_verify_identity" = "Подтвердить личность"; "common_video" = "Видео"; "common_voice_message" = "Голосовое сообщение"; "common_waiting" = "Ожидание…"; "common_waiting_for_decryption_key" = "Ожидание ключа расшифровки"; +"common.copied_to_clipboard" = "Скопировано в буфер обмена"; "common.do_not_show_this_again" = "Не показывать больше"; "common.open_source_licenses" = "Лицензии с открытым исходным кодом"; "common.pinned" = "Закрепленный"; "common.send_to" = "Отправить"; -"common_no_room_name" = "Нету названия комнаты"; -"common_poll_end_confirmation" = "Вы действительно хотите завершить данный опрос?"; -"common_poll_summary" = "Опрос: %1$@"; -"common_something_went_wrong" = "Что-то пошло не так"; -"common_unable_to_decrypt_no_access" = "Вы не имеете доступа к этому сообщению"; -"common_verify_device" = "Подтверждение устройства"; -"confirm_recovery_key_banner_message" = "В настоящее время резервная копия вашего чата не синхронизирована. Требуется подтвердить вашим ключом восстановления, чтобы сохранить доступ к резервной копии чата."; -"confirm_recovery_key_banner_title" = "Введите ключ восстановления"; +"common.you" = "Вы"; +"common_unable_to_decrypt_insecure_device" = "Отправлено с незащищенного устройства"; +"common_unable_to_decrypt_verification_violation" = "Подтвержденная личность отправителя изменилась"; +"confirm_recovery_key_banner_message" = "Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений."; +"confirm_recovery_key_banner_primary_button_title" = "Введите ключ восстановления"; +"confirm_recovery_key_banner_secondary_button_title" = "Забыли ключ восстановления?"; +"confirm_recovery_key_banner_title" = "Хранилище ключей не синхронизировано"; "crash_detection_dialog_content" = "При последнем использовании %1$@ произошел сбой. Хотите поделиться отчетом о сбое?"; +"crypto_identity_change_pin_violation" = "Судя по всему, идентификатор %1$@ изменился. %2$@"; +"crypto_identity_change_pin_violation_new" = "Пользователь %1$@ сменил имя пользователя на %2$@. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Чтобы приложение могло использовать камеру, предоставьте разрешение в системных настройках."; "dialog_permission_generic" = "Пожалуйста, предоставьте разрешение в системных настройках."; "dialog_permission_location_description_ios" = "Предоставьте доступ в меню «Настройки» -> «Местоположение»."; @@ -258,7 +274,7 @@ "emoji_picker_category_people" = "Улыбки и люди"; "emoji_picker_category_places" = "Путешествия и места"; "emoji_picker_category_symbols" = "Символы"; -"error_account_creation_not_possible" = "Ваш homeserver необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учетной записи."; +"error_account_creation_not_possible" = "Ваш домашний сервер необходимо обновить, чтобы он поддерживал Matrix Authentication Service и создание учётных записей."; "error_failed_creating_the_permalink" = "Не удалось создать постоянную ссылку"; "error_failed_loading_map" = "Не удалось загрузить карту %1$@. Пожалуйста, повторите попытку позже."; "error_failed_loading_messages" = "Не удалось загрузить сообщения"; @@ -274,13 +290,13 @@ "event_shield_reason_unknown_device" = "Зашифровано неизвестным или удаленным устройством."; "event_shield_reason_unsigned_device" = "Зашифровано устройством, не проверенным его владельцем."; "event_shield_reason_unverified_identity" = "Зашифровано непроверенным пользователем."; -"full_screen_intent_banner_message" = "Чтобы никогда не пропустить важный звонок, измените настройки, чтобы разрешить полноэкранные уведомления, когда ваш телефон заблокирован."; +"full_screen_intent_banner_message" = "Чтобы больше не пропускать важные звонки, разрешите приложению показывать полноэкранные уведомления на заблокированном экране телефона."; "full_screen_intent_banner_title" = "Улучшите качество звонков"; "invite_friends_rich_title" = "🔐️ Присоединяйтесь ко мне в %1$@"; "invite_friends_text" = "Привет, поговори со мной по %1$@: %2$@"; -"leave_conversation_alert_subtitle" = "Вы уверены, что хотите покинуть беседу?"; +"leave_conversation_alert_subtitle" = "Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."; "leave_room_alert_empty_subtitle" = "Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."; -"leave_room_alert_private_subtitle" = "Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."; +"leave_room_alert_private_subtitle" = "Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения."; "leave_room_alert_subtitle" = "Вы уверены, что хотите покинуть комнату?"; "login_initial_device_name_ios" = "%1$@ iOS"; "notification_channel_call" = "Позвонить"; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Бесшумные уведомления"; "notification_incoming_call" = "Входящий вызов"; "notification_inline_reply_failed" = "** Не удалось отправить - пожалуйста, откройте комнату"; -"notification_invitation_action_reject" = "Отклонить"; "notification_invite_body" = "Пригласил вас в чат"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ пригласил вас в чат"; "notification_mentioned_you_body" = "Упомянул вас: %1$@"; "notification_new_messages" = "Новые сообщения"; "notification_reaction_body" = "Отреагировал на %1$@"; "notification_room_invite_body" = "Пригласил вас в комнату"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ пригласил вас присоединиться к комнате"; "notification_sender_me" = "Я"; "notification_sender_mention_reply" = "%1$@ упомянул или ответил"; "notification_test_push_notification_content" = "Вы просматриваете уведомление! Нажмите на меня!"; @@ -329,15 +344,30 @@ "rich_text_editor_unindent" = "Без отступа"; "rich_text_editor_url_placeholder" = "Ссылка"; "rich_text_editor_a11y_add_attachment" = "Прикрепить файл"; +"rich_text_editor_composer_caption_placeholder" = "Необязательный заголовок..."; "screen_advanced_settings_element_call_base_url" = "Базовый URL сервера звонков Element"; "screen_advanced_settings_element_call_base_url_description" = "Задайте свой сервер Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес."; +"screen_create_room_room_address_section_footer" = "Чтобы эта комната была видна в каталоге общедоступных, вам необходим ее адрес"; +"screen_create_room_room_address_section_title" = "Адрес комнаты"; +"screen_create_room_room_visibility_section_title" = "Видимость комнаты"; +"screen_create_room_access_section_anyone_option_description" = "Любой желающий может присоединиться к этой комнате"; +"screen_create_room_access_section_anyone_option_title" = "Любой"; +"screen_create_room_access_section_header" = "Доступ в комнату"; +"screen_create_room_access_section_knocking_option_description" = "Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."; +"screen_create_room_access_section_knocking_option_title" = "Попросить присоединиться"; +"screen_join_room_cancel_knock_action" = "Отменить запрос"; +"screen_join_room_cancel_knock_alert_confirmation" = "Да, отменить"; +"screen_join_room_cancel_knock_alert_description" = "Вы действительно хотите отменить заявку на вступление в эту комнату?"; +"screen_join_room_cancel_knock_alert_title" = "Отменить запрос на присоединение"; +"screen_join_room_knock_message_description" = "Сообщение (опционально)"; +"screen_join_room_knock_sent_description" = "Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."; +"screen_join_room_knock_sent_title" = "Запрос на присоединение отправлен"; "screen_pinned_timeline_empty_state_description" = "Нажмите на сообщение и выберите “%1$@”, чтобы добавить его сюда."; "screen_pinned_timeline_empty_state_headline" = "Закрепите важные сообщения, чтобы их можно было легко найти"; -"screen_pinned_timeline_screen_title_empty" = "Закрепленные сообщения"; "screen_reset_encryption_password_error" = "Произошла неизвестная ошибка. Проверьте правильность пароля учетной записи и повторите попытку."; -"screen_resolve_send_failure_changed_identity_primary_button_title" = "Отозвать верификацию и отправить"; -"screen_resolve_send_failure_changed_identity_subtitle" = "Вы можете отозвать свою верификацию и отправить это сообщение в любом случае или вы можете отменить ее сейчас и повторить попытку позже после повторной верификации %1$@."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Отозвать статус и отправить"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Вы можете либо отозвать свой статус подтверждения и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$@."; "screen_resolve_send_failure_changed_identity_title" = "Ваше сообщение не было отправлено, потому что изменилась подтвержденная личность %1$@"; "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Отправь сообщение в любом случае"; "screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ использует одно или несколько непроверенных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже %2$@, проверив все устройства пользователя."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Загрузка сообщения..."; "screen_room_pinned_banner_view_all_button_title" = "Посмотреть все"; "screen_room_details_pinned_events_row_title" = "Закрепленные сообщения"; +"screen_roomlist_knock_event_sent_description" = "Запрос на присоединение отправлен"; "screen_timeline_item_menu_send_failure_changed_identity" = "Сообщение не отправлено, потому что верифицированная личность %1$@ изменилась."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Сообщение не отправлено, потому что %1$@ не проверил одно или несколько устройств."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств."; -"screen_account_provider_change" = "Переключить аккаунт"; "screen_account_provider_form_hint" = "Адрес домашнего сервера"; "screen_account_provider_form_notice" = "Введите поисковый запрос или адрес домена."; "screen_account_provider_form_subtitle" = "Поиск компании, сообщества или частного сервера."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Вы собираетесь создать учетную запись на %@"; "screen_advanced_settings_developer_mode" = "Режим разработчика"; "screen_advanced_settings_developer_mode_description" = "Предоставьте разработчикам доступ к функциям и функциональным возможностям."; +"screen_advanced_settings_media_compression_description" = "Загружайте фотографии и видео быстрее и сокращайте потребление трафика"; +"screen_advanced_settings_media_compression_title" = "Оптимизировать качество мультимедиа"; "screen_advanced_settings_rich_text_editor_description" = "Отключить редактор форматированного текста и включить Markdown."; "screen_advanced_settings_send_read_receipts" = "Уведомления о прочтении"; "screen_advanced_settings_send_read_receipts_description" = "Если этот параметр выключен, ваш статус о прочтении не будет отображаться. Вы по-прежнему будете видеть статус о прочтении от других пользователей."; @@ -369,18 +401,18 @@ "screen_advanced_settings_share_presence_description" = "Если выключено, вы не сможете отправлять, получать уведомления о прочтении и наборе текста"; "screen_advanced_settings_view_source_description" = "Включить опцию просмотра источника сообщения в ленте."; "screen_analytics_prompt_data_usage" = "Мы не будем записывать или профилировать какие-либо персональные данные"; -"screen_analytics_prompt_help_us_improve" = "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."; +"screen_analytics_prompt_help_us_improve" = "Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."; "screen_analytics_prompt_read_terms" = "Вы можете ознакомиться со всеми нашими условиями %1$@."; "screen_analytics_prompt_read_terms_content_link" = "здесь"; "screen_analytics_prompt_settings" = "Вы можете отключить эту функцию в любое время"; "screen_analytics_prompt_third_party_sharing" = "Мы не будем передавать ваши данные третьим лицам"; "screen_analytics_prompt_title" = "Помогите улучшить %1$@"; -"screen_analytics_settings_share_data" = "Делитесь данными аналитики"; +"screen_analytics_settings_share_data" = "Отправлять аналитические данные"; "screen_app_lock_biometric_authentication" = "биометрическая идентификация"; "screen_app_lock_biometric_unlock" = "биометрическая разблокировка"; "screen_app_lock_biometric_unlock_reason_ios" = "Для доступа к приложению необходима аутентификация"; "screen_app_lock_forgot_pin" = "Забыли PIN-код?"; -"screen_app_lock_settings_change_pin" = "Измените PIN-код"; +"screen_app_lock_settings_change_pin" = "Изменить PIN-код"; "screen_app_lock_settings_enable_biometric_unlock" = "Разрешить биометрическую разблокировку"; "screen_app_lock_settings_enable_face_id_ios" = "Разрешить Face ID"; "screen_app_lock_settings_enable_optic_id_ios" = "Разрешить Optic ID"; @@ -399,7 +431,7 @@ "screen_app_lock_setup_pin_mismatch_dialog_content" = "Повторите PIN-код"; "screen_app_lock_setup_pin_mismatch_dialog_title" = "PIN-коды не совпадают"; "screen_app_lock_signout_alert_message" = "Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"; -"screen_app_lock_signout_alert_title" = "Вы выходите из системы"; +"screen_app_lock_signout_alert_title" = "Выполняется выход из системы"; "screen_blocked_users_empty" = "У вас нет заблокированных пользователей"; "screen_blocked_users_unblocking" = "Разблокировка…"; "screen_bug_report_attach_screenshot" = "Приложить снимок экрана"; @@ -426,48 +458,49 @@ "screen_change_server_form_notice" = "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$@"; "screen_change_server_subtitle" = "Какой адрес у вашего сервера?"; "screen_change_server_title" = "Выберите свой сервер"; -"screen_chat_backup_key_backup_action_disable" = "Отключить резервное копирование"; +"screen_chat_backup_key_backup_action_disable" = "Удалить хранилище ключей"; "screen_chat_backup_key_backup_action_enable" = "Включить резервное копирование"; -"screen_chat_backup_key_backup_description" = "Резервное копирование гарантирует, что вы не потеряете историю сообщений. %1$@."; -"screen_chat_backup_key_backup_title" = "Резервное копирование"; -"screen_chat_backup_recovery_action_change" = "Изменить ключ восстановления"; -"screen_chat_backup_recovery_action_confirm" = "Ввести ключ восстановления"; -"screen_chat_backup_recovery_action_confirm_description" = "Резервная копия чата в настоящее время не синхронизирована."; -"screen_chat_backup_recovery_action_setup" = "Настроить восстановление"; +"screen_chat_backup_key_backup_description" = "Сохраните вашу криптографическую идентификацию и ключи сообщений в безопасности на сервере. Это позволит вам просматривать историю сообщений на любых новых устройствах.%1$@ ."; +"screen_chat_backup_key_backup_title" = "Хранилище ключей"; +"screen_chat_backup_key_storage_disabled_error" = "Для настройки восстановления необходимо включить хранилище ключей."; +"screen_chat_backup_key_storage_toggle_description" = "Загрузить ключи с этого устройства"; +"screen_chat_backup_key_storage_toggle_title" = "Разрешить хранение ключей"; +"screen_chat_backup_recovery_action_change" = "Изменить ключ восстановления"; +"screen_chat_backup_recovery_action_change_description" = "Если вы потеряли все существующие устройства, то сможете восстановить свою криптографическую идентификацию и историю сообщений с помощью ключа восстановления"; +"screen_chat_backup_recovery_action_confirm_description" = "В настоящее время резервная копия ваших чатов не синхронизирована."; "screen_chat_backup_recovery_action_setup_description" = "Получите доступ к зашифрованным сообщениям, если вы потеряете все свои устройства или выйдете из системы %1$@ отовсюду."; "screen_create_account_title" = "Создать учетную запись"; -"screen_create_new_recovery_key_list_item_1" = "Откройте %1$@ на настольном устройстве"; +"screen_create_new_recovery_key_list_item_1" = "Откройте %1$@ на компьютере"; "screen_create_new_recovery_key_list_item_2" = "Войдите в свой аккаунт еще раз"; "screen_create_new_recovery_key_list_item_3" = "Когда вас попросят подтвердить устройство, выберите %1$@"; "screen_create_new_recovery_key_list_item_3_reset_all" = "“Сбросить все”"; "screen_create_new_recovery_key_list_item_4" = "Следуйте инструкциям, чтобы создать новый ключ восстановления"; -"screen_create_new_recovery_key_list_item_5" = "Сохраните новый ключ восстановления в менеджере паролей или зашифрованной заметке"; +"screen_create_new_recovery_key_list_item_5" = "Сохраните новый ключ восстановления в менеджере паролей или зашифрованной заметке"; "screen_create_new_recovery_key_title" = "Сбросьте шифрование вашей учетной записи с помощью другого устройства."; "screen_create_poll_add_option_btn" = "Добавить вариант"; "screen_create_poll_anonymous_desc" = "Показывать результаты только после окончания опроса"; -"screen_create_poll_anonymous_headline" = "Анонимный опрос"; -"screen_create_poll_answer_hint" = "Настройка %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Вы действительно хотите отменить этот опрос"; +"screen_create_poll_anonymous_headline" = "Скрыть голоса"; +"screen_create_poll_answer_hint" = "Вариант %1$d"; "screen_create_poll_cancel_confirmation_title_ios" = "Отменить опрос"; "screen_create_poll_question_desc" = "Вопрос или тема"; -"screen_create_poll_question_hint" = "Тема опроса?"; +"screen_create_poll_question_hint" = "О чём будет опрос?"; "screen_create_poll_title" = "Создать опрос"; -"screen_create_room_action_create_room" = "Новая комната"; +"screen_create_room_action_create_room" = "Создать новую комнату"; "screen_create_room_error_creating_room" = "Произошла ошибка при создании комнаты"; -"screen_create_room_private_option_description" = "Сообщения в этой комнате зашифрованы. Отключить шифрование позже будет невозможно."; -"screen_create_room_private_option_title" = "Приватная комната (только по приглашению)"; -"screen_create_room_public_option_description" = "Сообщения не зашифрованы, каждый может их прочитать. Вы можете включить шифрование позже."; -"screen_create_room_public_option_title" = "Публичная комната (любой)"; +"screen_create_room_private_option_description" = "Доступ в эту комнату имеют только приглашенные пользователи. Все сообщения защищены сквозным шифрованием."; +"screen_create_room_private_option_title" = "Частная комната"; +"screen_create_room_public_option_description" = "Любой желающий может найти эту комнату.\nВы можете изменить это в любое время в настройках комнаты."; +"screen_create_room_public_option_title" = "Общедоступная комната"; "screen_create_room_topic_label" = "Тема (необязательно)"; -"screen_deactivate_account_confirmation_dialog_content" = "Подтвердите, что вы хотите деактивировать свою учетную запись. Это действие не может быть отменено."; +"screen_deactivate_account_confirmation_dialog_content" = "Вы уверены, что хотите отключить свою учётную запись? Данное действие не может быть отменено."; "screen_deactivate_account_delete_all_messages" = "Удалить все мои сообщения"; "screen_deactivate_account_delete_all_messages_notice" = "Предупреждение: будущие пользователи могут увидеть незавершенные разговоры."; -"screen_deactivate_account_description" = "Деактивация вашей учетной записи %1$@ означает следующее:"; -"screen_deactivate_account_description_bold_part" = "необратимый"; -"screen_deactivate_account_list_item_1" = "%1$@ вашей учетной записи (вы не можете войти в систему снова, и ваш ID не может быть использован повторно)."; +"screen_deactivate_account_description" = "Отключение вашей учетной записи %1$@ и означает следующее:"; +"screen_deactivate_account_description_bold_part" = "необратимо"; +"screen_deactivate_account_list_item_1" = "Ваша учётная запись будет %1$@ (вы не сможете войти в неё снова, и ваш ID не может быть использован повторно)."; "screen_deactivate_account_list_item_1_bold_part" = "Отключить навсегда"; -"screen_deactivate_account_list_item_2" = "Удалите вас из всех чатов."; -"screen_deactivate_account_list_item_3" = "Удалите данные своей учетной записи с нашего сервера идентификации."; +"screen_deactivate_account_list_item_2" = "Вы будете удалены из всех чатов."; +"screen_deactivate_account_list_item_3" = "Данные вашей учётной записи будут удалены с нашего сервера идентификации."; "screen_deactivate_account_list_item_4" = "Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."; "screen_deactivate_account_title" = "Отключить учётную запись"; "screen_edit_poll_delete_confirmation" = "Вы уверены, что хотите удалить этот опрос?"; @@ -484,10 +517,10 @@ "screen_encryption_reset_footer" = "Сбрасывайте данные только в том случае, если у вас нет доступа к другому устройству, на котором выполнен вход, и вы потеряли ключ восстановления."; "screen_encryption_reset_title" = "Сбросьте ключи подтверждения, если вы не можете подтвердить свою личность другим способом."; "screen_identity_confirmation_cannot_confirm" = "Не можете подтвердить?"; -"screen_identity_confirmation_create_new_recovery_key" = "Создайте новый ключ восстановления"; +"screen_identity_confirmation_create_new_recovery_key" = "Создайте новый ключ восстановления"; "screen_identity_confirmation_subtitle" = "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями."; "screen_identity_confirmation_title" = "Подтвердите, что это вы"; -"screen_identity_confirmation_use_another_device" = "Используйте другое устройство"; +"screen_identity_confirmation_use_another_device" = "Использовать другое устройство"; "screen_identity_confirmation_use_recovery_key" = "Используйте recovery key"; "screen_identity_confirmed_subtitle" = "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству."; "screen_identity_confirmed_title" = "Устройство проверено"; @@ -509,16 +542,16 @@ "screen_key_backup_disable_confirmation_action_turn_off" = "Выключить"; "screen_key_backup_disable_confirmation_description" = "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств."; "screen_key_backup_disable_confirmation_title" = "Вы действительно хотите отключить резервное копирование?"; -"screen_key_backup_disable_description" = "Отключение резервного копирования удалит текущую резервную копию ключа шифрования и отключит другие функции безопасности. В этом случае вы выполните следующие действия:"; +"screen_key_backup_disable_description" = "Удаление хранилища ключей приведёт к удалению вашей криптографической идентификации и ключей сообщений с сервера, а также отключению следующих функций безопасности:"; "screen_key_backup_disable_description_point_1" = "Нет зашифрованной истории сообщений на новых устройствах"; "screen_key_backup_disable_description_point_2" = "Вы потеряете доступ к зашифрованным сообщениям, если выйдете из %1$@ везде"; -"screen_key_backup_disable_title" = "Вы действительно хотите отключить резервное копирование?"; -"screen_login_error_deactivated_account" = "Данная учетная запись была деактивирована."; +"screen_key_backup_disable_title" = "Вы уверены, что хотите отключить хранение ключей и удалить их?"; +"screen_login_error_deactivated_account" = "Данная учётная запись была отключена."; "screen_login_error_invalid_credentials" = "Неверное имя пользователя и/или пароль"; "screen_login_error_invalid_user_id" = "Это не корректный идентификатор пользователя. Ожидаемый формат: '@user:homeserver.org'"; "screen_login_error_refresh_tokens" = "Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля."; "screen_login_error_unsupported_authentication" = "Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."; -"screen_login_form_header" = "Введите сведения о себе"; +"screen_login_form_header" = "Введите свои данные"; "screen_login_title" = "Рады видеть вас снова!"; "screen_login_title_with_homeserver" = "Войти в %1$@"; "screen_media_picker_error_failed_selection" = "Не удалось выбрать носитель, попробуйте еще раз."; @@ -527,47 +560,46 @@ "screen_migration_message" = "Это одноразовый процесс, спасибо, что подождали."; "screen_migration_title" = "Настройка учетной записи."; "screen_notification_optin_subtitle" = "Вы можете изменить настройки позже."; -"screen_notification_optin_title" = "Разрешите уведомления и никогда не пропустите сообщение"; +"screen_notification_optin_title" = "Разрешите отправку уведомлений и ни одно сообщение не будет пропущено"; "screen_notification_settings_additional_settings_section_title" = "Дополнительные параметры"; "screen_notification_settings_calls_label" = "Аудио и видео звонки"; "screen_notification_settings_configuration_mismatch" = "Несоответствие конфигурации"; "screen_notification_settings_configuration_mismatch_description" = "Мы упростили настройки уведомлений, чтобы упростить поиск опций. Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. \n\nЕсли вы продолжите, некоторые настройки могут быть изменены."; -"screen_notification_settings_direct_chats" = "Прямые чаты"; +"screen_notification_settings_direct_chats" = "В личных чатах"; "screen_notification_settings_edit_custom_settings_section_title" = "Персональные настройки для каждого чата"; -"screen_notification_settings_edit_failed_updating_default_mode" = "При обновлении настроек уведомления произошла ошибка."; -"screen_notification_settings_edit_mode_all_messages" = "Все сообщения"; +"screen_notification_settings_edit_failed_updating_default_mode" = "Произошла ошибка при обновлении настройки уведомления."; +"screen_notification_settings_edit_mode_all_messages" = "О всех сообщениях"; "screen_notification_settings_edit_mode_mentions_and_keywords" = "Только упоминания и ключевые слова"; "screen_notification_settings_edit_screen_direct_section_header" = "Уведомлять меня в личных чатах"; "screen_notification_settings_edit_screen_group_section_header" = "Уведомлять меня в групповых чатах"; "screen_notification_settings_enable_notifications" = "Включить уведомления на данном устройстве"; "screen_notification_settings_failed_fixing_configuration" = "Конфигурация не была исправлена, попробуйте еще раз."; -"screen_notification_settings_group_chats" = "Групповые чаты"; +"screen_notification_settings_group_chats" = "В групповых чатах"; "screen_notification_settings_invite_for_me_label" = "Приглашения"; "screen_notification_settings_mentions_only_disclaimer" = "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."; -"screen_notification_settings_mentions_section_title" = "Упоминания"; "screen_notification_settings_mode_all" = "Все"; "screen_notification_settings_mode_mentions" = "Упоминания"; -"screen_notification_settings_notification_section_title" = "Уведомить меня"; -"screen_notification_settings_room_mention_label" = "Уведомить меня в @room"; +"screen_notification_settings_notification_section_title" = "Уведомлять меня"; +"screen_notification_settings_room_mention_label" = "Уведомлять меня при упоминании @room"; "screen_notification_settings_system_notifications_action_required" = "Чтобы получать уведомления, измените свой %1$@."; "screen_notification_settings_system_notifications_action_required_content_link" = "настройки системы"; "screen_notification_settings_system_notifications_turned_off" = "Системные уведомления выключены"; "screen_notification_settings_title" = "Уведомления"; -"screen_onboarding_sign_in_manually" = "Вход в систему вручную"; -"screen_onboarding_sign_in_with_qr_code" = "Войти с помощью QR-кода"; +"screen_onboarding_sign_in_manually" = "Войти вручную"; +"screen_onboarding_sign_in_with_qr_code" = "Войти QR-кодом"; "screen_onboarding_sign_up" = "Создать учетную запись"; -"screen_onboarding_welcome_message" = "Добро пожаловать в самый быстрый %1$@. Сверхзаряженность на скорость и простоту."; -"screen_onboarding_welcome_subtitle" = "Добро пожаловать в %1$@. Сверхзаряжен для скорости и простоты."; -"screen_onboarding_welcome_title" = "Будьте в своем element"; +"screen_onboarding_welcome_message" = "Добро пожаловать в самый быстрый клиент %1$@. Ориентирован на скорость и простоту."; +"screen_onboarding_welcome_subtitle" = "Добро пожаловать в %1$@. Ориентирован на скорость и простоту."; +"screen_onboarding_welcome_title" = "Чувствуйте себя как дома с Element"; "screen_polls_history_empty_ongoing" = "Не найдено текущих опросов."; -"screen_polls_history_empty_past" = "Не найдено предыдущих опросов."; +"screen_polls_history_empty_past" = "Не найдено прошлых опросов."; "screen_polls_history_filter_ongoing" = "Текущие"; "screen_polls_history_filter_past" = "Прошлые"; "screen_polls_history_title" = "Опросы"; "screen_qr_code_login_connecting_subtitle" = "Установление безопасного соединения"; "screen_qr_code_login_connection_note_secure_state_description" = "Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."; "screen_qr_code_login_connection_note_secure_state_list_header" = "Что теперь?"; -"screen_qr_code_login_connection_note_secure_state_list_item_1" = "Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"; +"screen_qr_code_login_connection_note_secure_state_list_item_1" = "Попробуйте снова войти в систему с помощью QR-кода, если это была проблема с соединением"; "screen_qr_code_login_connection_note_secure_state_list_item_2" = "Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"; "screen_qr_code_login_connection_note_secure_state_list_item_3" = "Если это не помогло, войдите вручную"; "screen_qr_code_login_connection_note_secure_state_title" = "Соединение не защищено"; @@ -586,11 +618,12 @@ "screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Поставщик учетной записи не поддерживает %1$@."; "screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ не поддерживается"; "screen_qr_code_login_initial_state_button_title" = "Готово к сканированию"; -"screen_qr_code_login_initial_state_item_1" = "Откройте %1$@ на настольном устройстве"; +"screen_qr_code_login_initial_state_item_1" = "Откройте %1$@ на компьютере"; "screen_qr_code_login_initial_state_item_2" = "Нажмите на свое изображение"; -"screen_qr_code_login_initial_state_item_3" = "Выбрать %1$@"; +"screen_qr_code_login_initial_state_item_3" = "Выберите %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "\"Привязать новое устройство\""; "screen_qr_code_login_initial_state_item_4" = "Отсканируйте QR-код с помощью этого устройства"; +"screen_qr_code_login_initial_state_subtitle" = "Доступно только в том случае, если ваш поставщик учетной записи поддерживает это."; "screen_qr_code_login_initial_state_title" = "Откройте %1$@ на другом устройстве, чтобы получить QR-код"; "screen_qr_code_login_invalid_scan_state_description" = "Используйте QR-код, показанный на другом устройстве."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Неверный QR-код"; @@ -604,30 +637,28 @@ "screen_qr_code_login_verify_code_subtitle" = "Поставщик учетной записи может запросить следующий код для подтверждения входа."; "screen_qr_code_login_verify_code_title" = "Ваш код подтверждения"; "screen_recovery_key_change_description" = "Получите новый ключ восстановления, если вы потеряли существующий. После смены ключа восстановления старый ключ больше не будет работать."; -"screen_recovery_key_change_generate_key" = "Создать новый ключ восстановления"; -"screen_recovery_key_change_generate_key_description" = "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте"; -"screen_recovery_key_change_success" = "Ключ восстановления изменен"; +"screen_recovery_key_change_generate_key" = "Создать новый ключ восстановления"; +"screen_recovery_key_change_success" = "Ключ восстановления изменен"; "screen_recovery_key_change_title" = "Изменить ключ восстановления?"; -"screen_recovery_key_confirm_create_new_recovery_key" = "Создать новый ключ восстановления"; +"screen_recovery_key_confirm_create_new_recovery_key" = "Создать новый ключ восстановления"; "screen_recovery_key_confirm_description" = "Убедитесь, что никто не видит этот экран!"; "screen_recovery_key_confirm_error_content" = "Пожалуйста, попробуйте еще раз, чтобы подтвердить доступ к резервной копии чата."; -"screen_recovery_key_confirm_error_title" = "Неверный ключ восстановления"; +"screen_recovery_key_confirm_error_title" = "Неверный ключ восстановления"; "screen_recovery_key_confirm_key_description" = "Если у вас есть пароль для восстановления или секретный пароль/ключ, это тоже сработает."; "screen_recovery_key_confirm_key_placeholder" = "Вход…"; "screen_recovery_key_confirm_lost_recovery_key" = "Потеряли ключ восстановления?"; -"screen_recovery_key_confirm_success" = "Ключ восстановления подтвержден"; -"screen_recovery_key_confirm_title" = "Подтвердите ключ восстановления"; -"screen_recovery_key_copied_to_clipboard" = "Ключ восстановления скопирован"; +"screen_recovery_key_confirm_success" = "Ключ восстановления подтвержден"; +"screen_recovery_key_copied_to_clipboard" = "Ключ восстановления скопирован"; "screen_recovery_key_generating_key" = "Генерация…"; -"screen_recovery_key_save_action" = "Сохранить ключ восстановления"; -"screen_recovery_key_save_description" = "Запишите ключ восстановления в безопасном месте или сохраните его в менеджере паролей."; +"screen_recovery_key_save_action" = "Сохранить ключ восстановления"; +"screen_recovery_key_save_description" = "Запишите данный ключ восстановления в безопасном месте, например в диспетчере паролей, зашифрованной заметке или физическом сейфе."; "screen_recovery_key_save_key_description" = "Нажмите, чтобы скопировать ключ восстановления"; -"screen_recovery_key_save_title" = "Сохраните ключ восстановления"; +"screen_recovery_key_save_title" = "Сохраните ключ восстановления"; "screen_recovery_key_setup_confirmation_description" = "После этого шага вы не сможете получить доступ к новому ключу восстановления."; "screen_recovery_key_setup_confirmation_title" = "Вы сохранили ключ восстановления?"; -"screen_recovery_key_setup_description" = "Резервная копия чата защищена ключом восстановления. Если после настройки вам понадобится новый ключ восстановления, вы можете создать его заново, выбрав «Изменить ключ восстановления»."; -"screen_recovery_key_setup_generate_key" = "Создайте ключ восстановления"; -"screen_recovery_key_setup_generate_key_description" = "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте"; +"screen_recovery_key_setup_description" = "Ваше хранилище ключей защищено ключом восстановления. Если после настройки вам понадобится новый ключ восстановления, вы можете его пересоздать, выбрав «Изменить ключ восстановления»."; +"screen_recovery_key_setup_generate_key" = "Создайте ключ восстановления"; +"screen_recovery_key_setup_generate_key_description" = "Не сообщайте эту информацию никому!"; "screen_recovery_key_setup_success" = "Настройка восстановления выполнена успешно"; "screen_recovery_key_setup_title" = "Настроить восстановление"; "screen_report_content_block_user_hint" = "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Да, сбросить сейчас"; "screen_reset_encryption_confirmation_alert_subtitle" = "Этот процесс необратим."; "screen_reset_encryption_confirmation_alert_title" = "Вы действительно хотите сбросить шифрование?"; -"screen_reset_encryption_password_placeholder" = "Ввод..."; "screen_reset_encryption_password_subtitle" = "Подтвердите, что вы хотите сбросить шифрование."; "screen_reset_encryption_password_title" = "Введите пароль своей учетной записи, чтобы продолжить"; "screen_reset_identity_confirmation_subtitle" = "Вы собираетесь перейти в свою учетную запись %1$@, чтобы сбросить идентификацию. После этого вы вернетесь в приложение."; @@ -649,16 +679,16 @@ "screen_room_attachment_source_location" = "Местоположение"; "screen_room_attachment_source_poll" = "Опрос"; "screen_room_attachment_text_formatting" = "Форматирование текста"; -"screen_room_change_permissions_administrators" = "Только для администраторов"; -"screen_room_change_permissions_ban_people" = "Заблокировать людей"; -"screen_room_change_permissions_delete_messages" = "Удалить сообщения"; -"screen_room_change_permissions_invite_people" = "Пригласить людей"; +"screen_room_change_permissions_administrators" = "Только администраторы"; +"screen_room_change_permissions_ban_people" = "Блокировать людей могут"; +"screen_room_change_permissions_delete_messages" = "Удалять сообщения могут"; +"screen_room_change_permissions_invite_people" = "Приглашать людей могут"; "screen_room_change_permissions_moderators" = "Администраторы и модераторы"; -"screen_room_change_permissions_remove_people" = "Удалить людей"; -"screen_room_change_permissions_room_avatar" = "Изменить изображение комнаты"; -"screen_room_change_permissions_room_name" = "Изменить название комнаты"; -"screen_room_change_permissions_room_topic" = "Сменить тему комнаты"; -"screen_room_change_permissions_send_messages" = "Отправить сообщение"; +"screen_room_change_permissions_remove_people" = "Удалять людей могут"; +"screen_room_change_permissions_room_avatar" = "Менять изображение комнаты могут"; +"screen_room_change_permissions_room_name" = "Менять название комнаты могут"; +"screen_room_change_permissions_room_topic" = "Менять тему комнаты могут"; +"screen_room_change_permissions_send_messages" = "Отправлять сообщения могут"; "screen_room_change_role_administrators_title" = "Редактировать роль администраторов"; "screen_room_change_role_confirm_add_admin_description" = "Вы не сможете отменить это действие. Вы устанавливаете уровень пользователю соответствующий вашему."; "screen_room_change_role_confirm_add_admin_title" = "Добавить администратора?"; @@ -669,24 +699,22 @@ "screen_room_change_role_moderators_admin_section_footer" = "Администраторы автоматически получают права модератора"; "screen_room_change_role_moderators_title" = "Редактировать роль модераторов"; "screen_room_change_role_unsaved_changes_description" = "У вас есть несохраненные изменения."; -"screen_room_change_role_unsaved_changes_title" = "Сохранить изменения?"; "screen_room_details_add_topic_title" = "Добавить тему"; "screen_room_details_already_a_member" = "Уже зарегистрирован"; "screen_room_details_already_invited" = "Уже приглашены"; "screen_room_details_badge_encrypted" = "Зашифровано"; -"screen_room_details_badge_not_encrypted" = "Не зашифровано"; -"screen_room_details_badge_public" = "Общественная комната"; +"screen_room_details_badge_not_encrypted" = "Шифрования нет"; +"screen_room_details_badge_public" = "Общедоступная комната"; "screen_room_details_edit_room_title" = "Редактировать комнату"; "screen_room_details_edition_error" = "Произошла неизвестная ошибка и информацию не удалось изменить."; "screen_room_details_edition_error_title" = "Не удалось обновить комнату"; "screen_room_details_encryption_enabled_subtitle" = "Сообщения зашифрованы. Только у вас и у получателей есть уникальные ключи для их разблокировки."; "screen_room_details_encryption_enabled_title" = "Шифрование сообщений включено"; -"screen_room_details_error_loading_notification_settings" = "При загрузке настроек уведомлений произошла ошибка."; +"screen_room_details_error_loading_notification_settings" = "Произошла ошибка при загрузке настроек уведомлений."; "screen_room_details_error_muting" = "Не удалось отключить звук в этой комнате, попробуйте еще раз."; "screen_room_details_error_unmuting" = "Не удалось включить звук в эту комнату, попробуйте еще раз."; "screen_room_details_notification_mode_custom" = "Пользовательский"; "screen_room_details_notification_mode_default" = "По умолчанию"; -"screen_room_details_notification_title" = "Уведомления"; "screen_room_details_share_room_title" = "Поделиться комнатой"; "screen_room_details_title" = "Информация о комнате"; "screen_room_details_updating_room" = "Обновление комнаты…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Разблокировать"; "screen_room_member_details_unblock_alert_description" = "Вы снова сможете увидеть все сообщения."; "screen_room_member_details_unblock_user" = "Разблокировать пользователя"; +"screen_room_member_details_verify_button_subtitle" = "Используйте веб-приложение для проверки этого пользователя."; +"screen_room_member_details_verify_button_title" = "Верифицировать %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Заблокировать"; "screen_room_member_list_ban_member_confirmation_description" = "Они не смогут снова присоединиться к этой комнате, если их пригласят."; "screen_room_member_list_ban_member_confirmation_title" = "Вы уверены, что хотите заблокировать этого участника?"; @@ -711,14 +741,13 @@ "screen_room_member_list_banning_user" = "Блокировка %1$@"; "screen_room_member_list_manage_member_ban" = "Удалить и заблокировать участника"; "screen_room_member_list_manage_member_remove" = "Удалить участника из комнаты"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Удалить и запретить участника"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Только удалить участника"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Удалить участника и запретить присоединяться в будущем?"; "screen_room_member_list_manage_member_unban_action" = "Разблокировать"; "screen_room_member_list_manage_member_unban_message" = "Они снова смогут присоединиться в эту комнату если их пригласят."; "screen_room_member_list_manage_member_unban_title" = "Разбанить пользователя?"; "screen_room_member_list_manage_member_user_info" = "Посмотреть профиль"; -"screen_room_member_list_mode_banned" = "Заблокирован"; +"screen_room_member_list_mode_banned" = "Заблокированные"; "screen_room_member_list_mode_members" = "Участники"; "screen_room_member_list_pending_header_title" = "В ожидании"; "screen_room_member_list_removing_user" = "Удаление %1$@…"; @@ -728,7 +757,7 @@ "screen_room_member_list_unbanning_user" = "Разблокировка %1$@"; "screen_room_notification_settings_allow_custom" = "Разрешить пользовательские настройки"; "screen_room_notification_settings_allow_custom_footnote" = "Включение этого параметра отменяет настройки по умолчанию"; -"screen_room_notification_settings_custom_settings_title" = "Уведомить меня в этом чате"; +"screen_room_notification_settings_custom_settings_title" = "Уведомлять меня в этом чате"; "screen_room_notification_settings_default_setting_footnote" = "Вы можете изменить его в своем %1$@."; "screen_room_notification_settings_default_setting_footnote_content_link" = "основные настройки"; "screen_room_notification_settings_default_setting_title" = "Настройка по умолчанию"; @@ -737,20 +766,20 @@ "screen_room_notification_settings_error_restoring_default" = "Не удалось восстановить режим по умолчанию, попробуйте еще раз."; "screen_room_notification_settings_error_setting_mode" = "Не удалось настроить режим, попробуйте еще раз."; "screen_room_notification_settings_mentions_only_disclaimer" = "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате."; -"screen_room_notification_settings_mode_all_messages" = "Все сообщения"; -"screen_room_notification_settings_room_custom_settings_title" = "В этой комнате уведомить меня о"; +"screen_room_notification_settings_mode_all_messages" = "О всех сообщениях"; +"screen_room_notification_settings_room_custom_settings_title" = "В этой комнате уведомлять меня"; "screen_room_retry_send_menu_send_again_action" = "Отправить снова"; "screen_room_retry_send_menu_title" = "Не удалось отправить ваше сообщение"; "screen_room_roles_and_permissions_admins" = "Администраторы"; -"screen_room_roles_and_permissions_change_my_role" = "Измените мою роль"; -"screen_room_roles_and_permissions_change_role_demote_to_member" = "Понижение до участника"; +"screen_room_roles_and_permissions_change_my_role" = "Изменить мою роль"; +"screen_room_roles_and_permissions_change_role_demote_to_member" = "Понизить до участника"; "screen_room_roles_and_permissions_change_role_demote_to_moderator" = "Понизить до модератора"; "screen_room_roles_and_permissions_member_moderation" = "Модерация участников"; "screen_room_roles_and_permissions_messages_and_content" = "Сообщения и содержание"; "screen_room_roles_and_permissions_moderators" = "Модераторы"; "screen_room_roles_and_permissions_permissions_header" = "Разрешения"; "screen_room_roles_and_permissions_reset" = "Сбросить разрешения"; -"screen_room_roles_and_permissions_reset_confirm_description" = "Как только вы сбросите разрешения, вы потеряете текущие настройки."; +"screen_room_roles_and_permissions_reset_confirm_description" = "Как только вы сбросите разрешения, все текущие настройки будут утеряны."; "screen_room_roles_and_permissions_reset_confirm_title" = "Сбросить разрешения?"; "screen_room_roles_and_permissions_roles_header" = "Роли"; "screen_room_roles_and_permissions_room_details" = "Информация о комнате"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Показать меньше"; "screen_room_timeline_message_copied" = "Сообщение скопировано"; "screen_room_timeline_no_permission_to_post" = "У вас нет разрешения публиковать сообщения в этой комнате"; -"screen_room_timeline_reactions_show_less" = "Показать меньше"; "screen_room_timeline_reactions_show_more" = "Показать больше"; "screen_room_timeline_read_marker_title" = "Новый"; "screen_room_title" = "Чат"; @@ -776,7 +804,7 @@ "screen_roomlist_filter_favourites" = "Избранное"; "screen_roomlist_filter_favourites_empty_state_subtitle" = "Добавить чат в избранное можно в настройках чата.\nНа данный момент вы можете убрать фильтры, чтобы увидеть другие ваши чаты."; "screen_roomlist_filter_favourites_empty_state_title" = "У вас пока нет избранных чатов"; -"screen_roomlist_filter_invites" = "Приглашает"; +"screen_roomlist_filter_invites" = "Приглашения"; "screen_roomlist_filter_invites_empty_state_title" = "У вас нет отложенных приглашений."; "screen_roomlist_filter_low_priority" = "Низкий приоритет"; "screen_roomlist_filter_mixed_empty_state_subtitle" = "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты."; @@ -785,15 +813,14 @@ "screen_roomlist_filter_rooms" = "Комнаты"; "screen_roomlist_filter_rooms_empty_state_title" = "Вас пока нет ни в одной комнате"; "screen_roomlist_filter_unreads" = "Непрочитанные"; -"screen_roomlist_filter_unreads_empty_state_title" = "Поздравляю!\nУ вас нет непрочитанных сообщений!"; +"screen_roomlist_filter_unreads_empty_state_title" = "Поздравляем!\nУ вас нет непрочитанных сообщений!"; "screen_roomlist_main_space_title" = "Все чаты"; "screen_roomlist_mark_as_read" = "Пометить как прочитанное"; -"screen_roomlist_mark_as_unread" = "Пометить как непрочитанное"; +"screen_roomlist_mark_as_unread" = "Отметить как непрочитанное"; "screen_roomlist_room_directory_button_title" = "Просмотреть все комнаты"; -"screen_server_confirmation_change_server" = "Сменить учетную запись"; "screen_server_confirmation_message_login_element_dot_io" = "Частный сервер для сотрудников Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix — это открытая сеть для безопасной децентрализованной связи."; -"screen_server_confirmation_message_register" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; +"screen_server_confirmation_message_register" = "Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."; "screen_server_confirmation_title_login" = "Вы собираетесь войти в %1$@"; "screen_server_confirmation_title_register" = "Вы собираетесь создать учетную запись на %1$@"; "screen_session_verification_cancelled_subtitle" = "Похоже, что-то не так. Время ожидания запроса либо истекло, либо запрос был отклонен."; @@ -802,17 +829,25 @@ "screen_session_verification_compare_numbers_subtitle" = "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе."; "screen_session_verification_compare_numbers_title" = "Сравните числа"; "screen_session_verification_complete_subtitle" = "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное."; -"screen_session_verification_enter_recovery_key" = "Введите ключ восстановления"; +"screen_session_verification_enter_recovery_key" = "Введите ключ восстановления"; +"screen_session_verification_failed_subtitle" = "Время ожидания подтверждения истекло, запрос был отклонён, или при подтверждении произошло несоответствие."; "screen_session_verification_open_existing_session_subtitle" = "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы."; "screen_session_verification_open_existing_session_title" = "Открыть существующий сеанс"; -"screen_session_verification_positive_button_canceled" = "Повторить проверку"; +"screen_session_verification_positive_button_canceled" = "Повторить подтверждение"; "screen_session_verification_positive_button_initial" = "Я готов"; -"screen_session_verification_positive_button_verifying_ongoing" = "Ожидание соответствия"; +"screen_session_verification_positive_button_verifying_ongoing" = "Ожидание соответствия…"; "screen_session_verification_ready_subtitle" = "Сравните уникальный набор эмодзи."; "screen_session_verification_request_accepted_subtitle" = "Сравните уникальные смайлики, убедившись, что они расположены в том же порядке."; +"screen_session_verification_request_details_timestamp" = "Вход выполнен"; +"screen_session_verification_request_failure_title" = "Сбой проверки"; +"screen_session_verification_request_footer" = "Продолжайте только если вы ожидали данное подтверждение."; +"screen_session_verification_request_subtitle" = "Чтобы сохранить историю сообщений в безопасности, проверьте другое устройство."; +"screen_session_verification_request_success_subtitle" = "Теперь вы можете безопасно читать или отправлять сообщения на другом устройстве."; +"screen_session_verification_request_success_title" = "Устройство проверено"; +"screen_session_verification_request_title" = "Запрошено подтверждение"; "screen_session_verification_they_dont_match" = "Они не совпадают"; "screen_session_verification_they_match" = "Они совпадают"; -"screen_session_verification_waiting_to_accept_subtitle" = "Для продолжения работы примите запрос на запуск процесса проверки в другом сеансе."; +"screen_session_verification_waiting_to_accept_subtitle" = "Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе."; "screen_session_verification_waiting_to_accept_title" = "Ожидание принятия запроса"; "screen_share_location_title" = "Поделиться местоположением"; "screen_share_my_location_action" = "Поделиться моим местоположением"; @@ -830,14 +865,12 @@ "screen_signout_key_backup_disabled_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."; "screen_signout_key_backup_disabled_title" = "Вы отключили резервное копирование"; "screen_signout_key_backup_offline_subtitle" = "Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей."; -"screen_signout_key_backup_offline_title" = "Резервное копирование ключей все еще продолжается"; "screen_signout_key_backup_ongoing_subtitle" = "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы."; "screen_signout_key_backup_ongoing_title" = "Резервное копирование ключей все еще продолжается"; "screen_signout_recovery_disabled_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."; "screen_signout_recovery_disabled_title" = "Восстановление не настроено"; "screen_signout_save_recovery_key_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям."; -"screen_signout_save_recovery_key_title" = "Вы сохранили свой ключ восстановления?"; -"screen_start_chat_error_starting_chat" = "Произошла ошибка при попытке открытия комнаты"; +"screen_start_chat_error_starting_chat" = "Произошла ошибка при запуске чата"; "screen_view_location_title" = "Местоположение"; "screen_welcome_bullet_1" = "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."; "screen_welcome_bullet_2" = "История сообщений для зашифрованных комнат в этом обновлении будет недоступна."; @@ -845,16 +878,16 @@ "screen_welcome_button" = "Поехали!"; "screen_welcome_subtitle" = "Вот что вам необходимо знать:"; "screen_welcome_title" = "Добро пожаловать в %1$@!"; -"session_verification_banner_message" = "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите верификацию с другим устройством."; +"session_verification_banner_message" = "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям пройдите подтверждение с другим устройством."; "session_verification_banner_title" = "Подтвердите, что это вы"; "settings_rageshake" = "Встряхните"; "settings_rageshake_detection_threshold" = "Порог обнаружения"; "settings_version_number" = "Версия: %1$@ (%2$@)"; "state_event_avatar_changed_too" = "(изображение тоже было изменено)"; -"state_event_avatar_url_changed" = "%1$@ сменили свое изображение"; +"state_event_avatar_url_changed" = "%1$@ сменил своё изображение"; "state_event_avatar_url_changed_by_you" = "Вы сменили изображение профиля"; -"state_event_demoted_to_member" = "%1$@ был понижен в должности до участника"; -"state_event_demoted_to_moderator" = "%1$@ был понижен в должности до модератора"; +"state_event_demoted_to_member" = "%1$@ был понижен до участника"; +"state_event_demoted_to_moderator" = "%1$@ был понижен до модератора"; "state_event_display_name_changed_from" = "%1$@ изменил свое отображаемое имя с %2$@ на %3$@"; "state_event_display_name_changed_from_by_you" = "Вы изменили свое отображаемое имя с %1$@ на %2$@"; "state_event_display_name_removed" = "%1$@ удалил свое отображаемое имя (оно было %2$@)"; @@ -888,7 +921,7 @@ "state_event_room_knock_retracted" = "%1$@ больше не заинтересован в присоединении"; "state_event_room_knock_retracted_by_you" = "Вы отменили запрос на присоединение"; "state_event_room_leave" = "%1$@ покинул комнату"; -"state_event_room_leave_by_you" = "Вы вышли из комнаты"; +"state_event_room_leave_by_you" = "Вы покинули комнату"; "state_event_room_name_changed" = "%1$@ изменил название комнаты на: %2$@"; "state_event_room_name_changed_by_you" = "Вы изменили название комнаты на: %1$@"; "state_event_room_name_removed" = "%1$@ удалил название комнаты"; @@ -919,7 +952,6 @@ "test_language_identifier" = "ru"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Устранение неполадок"; -"troubleshoot_notifications_entry_point_title" = "Уведомления об устранении неполадок"; "troubleshoot_notifications_screen_action" = "Выполнение тестов"; "troubleshoot_notifications_screen_action_again" = "Повторное выполнение тестов"; "troubleshoot_notifications_screen_failure" = "Некоторые тесты провалились. Пожалуйста, проверьте детали."; @@ -961,22 +993,30 @@ "troubleshoot_notifications_test_unified_push_description" = "Убедитесь, что дистрибьюторы UnifiedPush доступны."; "troubleshoot_notifications_test_unified_push_failure" = "Поставщиков push-уведомлений не найдено."; "troubleshoot_notifications_test_unified_push_title" = "Проверка UnifiedPush"; +"a11y_poll" = "Опрос"; +"banner_set_up_recovery_submit" = "Настроить восстановление"; "dialog_title_error" = "Ошибка"; "dialog_title_success" = "Успешно"; "notification_fallback_content" = "Уведомление"; "notification_invitation_action_join" = "Присоединиться"; +"notification_invitation_action_reject" = "Отклонить"; "notification_room_action_mark_as_read" = "Пометить как прочитанное"; "notification_room_action_quick_reply" = "Быстрый ответ"; -"screen_room_mentions_at_room_title" = "Для всех"; -"screen_account_provider_signin_subtitle" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; -"screen_account_provider_signup_subtitle" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; -"screen_analytics_settings_help_us_improve" = "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."; +"screen_pinned_timeline_screen_title_empty" = "Закрепленные сообщения"; +"screen_room_mentions_at_room_title" = "Все"; +"screen_account_provider_change" = "Сменить поставщика учетной записи"; +"screen_account_provider_signin_subtitle" = "Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."; +"screen_account_provider_signup_subtitle" = "Здесь будут храниться ваши разговоры — точно так же, как если бы вы использовали почтового провайдера для хранения своих писем."; +"screen_analytics_settings_help_us_improve" = "Предоставьте разработчикам анонимные данные об использовании, чтобы помочь им выявлять проблемы эффективнее."; "screen_analytics_settings_read_terms" = "Вы можете ознакомиться со всеми нашими условиями %1$@."; "screen_analytics_settings_read_terms_content_link" = "здесь"; "screen_blocked_users_unblock_alert_action" = "Разблокировать"; "screen_blocked_users_unblock_alert_description" = "Вы снова сможете увидеть все сообщения."; "screen_blocked_users_unblock_alert_title" = "Разблокировать пользователя"; "screen_bug_report_rash_logs_alert_title" = "При последнем использовании %1$@ произошел сбой. Хотите поделиться отчетом о сбое?"; +"screen_chat_backup_recovery_action_confirm" = "Введите ключ восстановления"; +"screen_chat_backup_recovery_action_setup" = "Настроить восстановление"; +"screen_create_poll_cancel_confirmation_content_ios" = "Ваши изменения не будут сохранены"; "screen_create_room_add_people_title" = "Пригласить в комнату"; "screen_create_room_room_name_label" = "Название комнаты"; "screen_create_room_title" = "Создать комнату"; @@ -988,28 +1028,41 @@ "screen_dm_details_unblock_user" = "Разблокировать пользователя"; "screen_edit_poll_delete_confirmation_title" = "Удалить опрос"; "screen_edit_poll_title" = "Редактировать опрос"; -"screen_identity_use_another_device" = "Используйте другое устройство"; +"screen_identity_use_another_device" = "Использовать другое устройство"; "screen_login_subtitle" = "Matrix — это открытая сеть для безопасной децентрализованной связи."; +"screen_notification_settings_mentions_section_title" = "Упоминания"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Повторить попытку"; +"screen_recovery_key_change_generate_key_description" = "Не сообщайте эту информацию никому!"; +"screen_recovery_key_confirm_title" = "Введите ключ восстановления"; "screen_report_content_block_user" = "Заблокировать пользователя"; +"screen_reset_encryption_password_placeholder" = "Вход…"; "screen_room_attachment_source_camera_photo" = "Сделать фото"; -"screen_room_change_permissions_everyone" = "Для всех"; +"screen_room_change_permissions_everyone" = "Все"; "screen_room_change_permissions_member_moderation" = "Модерация участников"; "screen_room_change_permissions_messages_and_content" = "Сообщения и содержание"; "screen_room_change_permissions_room_details" = "Информация о комнате"; "screen_room_change_role_section_administrators" = "Администраторы"; "screen_room_change_role_section_moderators" = "Модераторы"; "screen_room_change_role_section_users" = "Участники"; +"screen_room_change_role_unsaved_changes_title" = "Сохранить изменения?"; "screen_room_details_invite_people_title" = "Пригласить в комнату"; "screen_room_details_leave_conversation_title" = "Покинуть беседу"; "screen_room_details_leave_room_title" = "Покинуть комнату"; +"screen_room_details_notification_title" = "Уведомления"; "screen_room_details_roles_and_permissions" = "Роли и разрешения"; "screen_room_details_room_name_label" = "Название комнаты"; "screen_room_details_security_title" = "Безопасность"; "screen_room_details_topic_title" = "Тема"; "screen_room_error_failed_processing_media" = "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Удалить и заблокировать участника"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Только упоминания и ключевые слова"; -"screen_roomlist_filter_people" = "Люди"; +"screen_room_timeline_reactions_show_less" = "Показать меньше"; +"screen_roomlist_filter_people" = "Пользователи"; +"screen_server_confirmation_change_server" = "Сменить поставщика учетной записи"; +"screen_session_verification_request_failure_subtitle" = "Время ожидания подтверждения истекло, запрос был отклонён, или при подтверждении произошло несоответствие."; "screen_signout_confirmation_dialog_submit" = "Выйти"; "screen_signout_confirmation_dialog_title" = "Выйти"; +"screen_signout_key_backup_offline_title" = "Резервное копирование ключей все еще продолжается"; "screen_signout_preference_item" = "Выйти"; +"screen_signout_save_recovery_key_title" = "Вы сохранили ключ восстановления?"; +"troubleshoot_notifications_entry_point_title" = "Уведомления об устранении неполадок"; diff --git a/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict index 2a5f2f2586..71a17080b1 100644 --- a/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict @@ -193,11 +193,11 @@ NSStringFormatValueTypeKey d one - Вы попытались разблокировать %1$d раз + У вас осталась %1$d попытка на разблокировку few - Вы попытались разблокировать %1$d раз + У вас остались %1$d попытки на разблокировку many - Вы попытались разблокировать много раз + У вас осталось %1$d попыток на разблокировку screen_app_lock_subtitle_wrong_pin @@ -211,11 +211,11 @@ NSStringFormatValueTypeKey d one - Неверный PIN-код. У вас остался %1$d шанс + Неверный PIN-код. У вас осталась %1$d попытка few - Неверный PIN-код. У вас остался %1$d шансов + Неверный PIN-код. У вас остались %1$d попытки many - Неверный PIN-код. У вас остался %1$d шанса + Неверный PIN-код. У вас осталось %1$d попыток screen_pinned_timeline_screen_title @@ -229,11 +229,11 @@ NSStringFormatValueTypeKey d one - %1$d Закрепленное сообщение + %1$d закреплённое сообщение few - %1$d Закрепленных сообщений + %1$d закреплённых сообщения many - %1$d Закрепленных сообщений + %1$d закреплённых сообщений screen_room_member_list_header_title diff --git a/ElementX/Resources/Localizations/sk.lproj/Localizable.strings b/ElementX/Resources/Localizations/sk.lproj/Localizable.strings index dba24dba2a..1008f9615c 100644 --- a/ElementX/Resources/Localizations/sk.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/sk.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pozastaviť"; "a11y_pin_field" = "Pole PIN"; "a11y_play" = "Prehrať"; -"a11y_poll" = "Anketa"; "a11y_poll_end" = "Ukončená anketa"; "a11y_react_with" = "Reagovať s %1$@"; "a11y_react_with_other_emojis" = "Reagovať s inými emotikonmi"; @@ -41,6 +40,7 @@ "action_create" = "Vytvoriť"; "action_create_a_room" = "Vytvoriť miestnosť"; "action_deactivate" = "Deaktivovať"; +"action_deactivate_account" = "Deaktivovať účet"; "action_decline" = "Odmietnuť"; "action_delete_poll" = "Odstrániť anketu"; "action_disable" = "Vypnúť"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Zabudnuté heslo?"; "action_forward" = "Preposlať"; "action_go_back" = "Ísť späť"; +"action_ignore" = "Ignorovať"; "action_invite" = "Pozvať"; "action_invite_friends" = "Pozvať ľudí"; "action_invite_friends_to_app" = "Pozvať ľudí do %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Opustiť"; "action_leave_conversation" = "Opustiť konverzáciu"; "action_leave_room" = "Opustiť miestnosť"; +"action_load_more" = "Načítať viac"; "action_manage_account" = "Spravovať účet"; "action_manage_devices" = "Spravovať zariadenia"; "action_message" = "Poslať správu"; @@ -93,6 +95,7 @@ "action_send_message" = "Odoslať správu"; "action_share" = "Zdieľať"; "action_share_link" = "Zdieľať odkaz"; +"action_show" = "Zobraziť"; "action_sign_in_again" = "Prihláste sa znova"; "action_signout" = "Odhlásiť sa"; "action_signout_anyway" = "Napriek tomu sa odhlásiť"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Zobraziť na časovej osi"; "action_view_source" = "Zobraziť zdroj"; "action_yes" = "Áno"; -"action.load_more" = "Načítať viac"; -"action_deactivate_account" = "Deaktivovať účet"; "banner_migrate_to_native_sliding_sync_action" = "Odhlásiť sa a aktualizovať"; "banner_migrate_to_native_sliding_sync_description" = "Váš server teraz podporuje nový, rýchlejší protokol. Odhláste sa a prihláste sa znova, aby ste mohli aktualizovať. Ak to urobíte teraz, pomôže vám vyhnúť sa nútenému odhláseniu, keď sa starý protokol neskôr odstráni."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Váš domovský server už nepodporuje starý protokol. Ak chcete pokračovať v používaní aplikácie, odhláste sa a znova sa prihláste."; "banner_migrate_to_native_sliding_sync_title" = "Aktualizácia je k dispozícii"; -"banner.set_up_recovery.content" = "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam."; -"banner.set_up_recovery.title" = "Nastaviť obnovenie"; +"banner_set_up_recovery_content" = "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam."; +"banner_set_up_recovery_title" = "Nastaviť obnovenie"; "common_about" = "O aplikácii"; "common_acceptable_use_policy" = "Zásady prijateľného používania"; "common_advanced_settings" = "Pokročilé nastavenia"; @@ -133,10 +134,12 @@ "common_dark" = "Tmavý"; "common_decryption_error" = "Chyba dešifrovania"; "common_developer_options" = "Možnosti pre vývojárov"; +"common_device_id" = "ID zariadenia"; "common_direct_chat" = "Priama konverzácia"; "common_edited_suffix" = "(upravené)"; "common_editing" = "Upravuje sa"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Šifrovanie"; "common_encryption_enabled" = "Šifrovanie zapnuté"; "common_enter_your_pin" = "Zadajte svoj PIN"; "common_error" = "Chyba"; @@ -147,6 +150,7 @@ "common_favourited" = "Obľúbené"; "common_file" = "Súbor"; "common_forward_message" = "Preposlať správu"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Obrázok"; "common_in_reply_to" = "V odpovedi na %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Moderné"; "common_mute" = "Stlmiť"; "common_no_results" = "Žiadne výsledky"; +"common_no_room_name" = "Žiadny názov miestnosti"; "common_offline" = "Offline"; "common_optic_id_ios" = "Optic ID"; "common_or" = "alebo"; @@ -170,6 +175,8 @@ "common_permalink" = "Trvalý odkaz"; "common_permission" = "Povolenie"; "common_please_wait" = "Prosím, počkajte..."; +"common_poll_end_confirmation" = "Ste si istí, že chcete ukončiť túto anketu?"; +"common_poll_summary" = "Anketa: %1$@"; "common_poll_total_votes" = "Celkový počet hlasov: %1$@"; "common_poll_undisclosed_text" = "Výsledky sa zobrazia po ukončení ankety"; "common_privacy_policy" = "Zásady ochrany osobných údajov"; @@ -200,6 +207,7 @@ "common_settings" = "Nastavenia"; "common_shared_location" = "Zdieľaná poloha"; "common_signing_out" = "Odhlasovanie"; +"common_something_went_wrong" = "Niečo sa pokazilo"; "common_starting_chat" = "Spustenie konverzácie..."; "common_sticker" = "Nálepka"; "common_success" = "Úspech"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "O čom je táto miestnosť?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Nie je možné dešifrovať"; +"common_unable_to_decrypt_no_access" = "Nemáte prístup k tejto správe"; "common_unable_to_invite_message" = "Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom."; "common_unable_to_invite_title" = "Nie je možné odoslať pozvánku/ky"; "common_unlock" = "Odomknúť"; @@ -221,23 +230,30 @@ "common_username" = "Používateľské meno"; "common_verification_cancelled" = "Overovanie zrušené"; "common_verification_complete" = "Overovanie je dokončené"; +"common_verification_failed" = "Overenie zlyhalo"; +"common_verified" = "Overené"; +"common_verify_device" = "Overiť zariadenie"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Hlasová správa"; "common_waiting" = "Čaká sa…"; "common_waiting_for_decryption_key" = "Čaká sa na dešifrovací kľúč"; +"common.copied_to_clipboard" = "Skopírované do schránky"; "common.do_not_show_this_again" = "Nezobrazovať toto znova"; "common.open_source_licenses" = "Licencie s otvoreným zdrojom"; "common.pinned" = "Pripnuté"; "common.send_to" = "Odoslať"; -"common_no_room_name" = "Žiadny názov miestnosti"; -"common_poll_end_confirmation" = "Ste si istí, že chcete ukončiť túto anketu?"; -"common_poll_summary" = "Anketa: %1$@"; -"common_something_went_wrong" = "Niečo sa pokazilo"; -"common_unable_to_decrypt_no_access" = "Nemáte prístup k tejto správe"; -"common_verify_device" = "Overiť zariadenie"; +"common.you" = "Vy"; +"common_unable_to_decrypt_insecure_device" = "Odoslané z nezabezpečeného zariadenia"; +"common_unable_to_decrypt_verification_violation" = "Overená totožnosť odosielateľa sa zmenila"; "confirm_recovery_key_banner_message" = "Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Potvrďte svoj kľúč na obnovenie"; "crash_detection_dialog_content" = "%1$@ zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?"; +"crypto_identity_change_pin_violation" = "Zdá sa, že totožnosť používateľa %1$@ sa zmenila.%2$@"; +"crypto_identity_change_pin_violation_new" = "Zdá sa, že identita %2$@ používateľa %1$@ sa zmenila. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Aby aplikácia mohla používať fotoaparát, udeľte povolenie v systémových nastaveniach."; "dialog_permission_generic" = "Udeľte prosím povolenie v systémových nastaveniach."; "dialog_permission_location_description_ios" = "Udeľte prístup v časti Nastavenia -> Poloha."; @@ -290,14 +306,13 @@ "notification_channel_silent" = "Tiché oznámenia"; "notification_incoming_call" = "Prichádzajúci hovor"; "notification_inline_reply_failed" = "** Nepodarilo sa odoslať - prosím otvorte miestnosť"; -"notification_invitation_action_reject" = "Zamietnuť"; "notification_invite_body" = "Vás pozval/a na konverzáciu"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ vás pozval/a na rozhovor"; "notification_mentioned_you_body" = "Spomenul/a vás: %1$@"; "notification_new_messages" = "Nové správy"; "notification_reaction_body" = "Reagoval/a s %1$@"; "notification_room_invite_body" = "Vás pozval do miestnosti"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ vás pozval/a, aby ste sa pripojili k miestnosti"; "notification_sender_me" = "Ja"; "notification_sender_mention_reply" = "%1$@ spomenul/a alebo odpovedal/a"; "notification_test_push_notification_content" = "Prezeráte si oznámenie! Kliknite na mňa!"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Zrušiť odsadenie"; "rich_text_editor_url_placeholder" = "Odkaz"; "rich_text_editor_a11y_add_attachment" = "Pridať prílohu"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Vlastná Element Call základná URL adresa"; "screen_advanced_settings_element_call_base_url_description" = "Nastaviť vlastnú základnú URL adresu pre Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Neplatná adresa URL, uistite sa, že ste uviedli protokol (http/https) a správnu adresu."; +"screen_create_room_room_address_section_footer" = "Aby bola táto miestnosť viditeľná v adresári verejných miestností, budete potrebovať adresu miestnosti."; +"screen_create_room_room_address_section_title" = "Adresa miestnosti"; +"screen_create_room_room_visibility_section_title" = "Viditeľnosť miestnosti"; +"screen_create_room_access_section_anyone_option_description" = "Do tejto miestnosti sa môže pripojiť ktokoľvek"; +"screen_create_room_access_section_anyone_option_title" = "Ktokoľvek"; +"screen_create_room_access_section_header" = "Prístup do miestnosti"; +"screen_create_room_access_section_knocking_option_description" = "Ktokoľvek môže požiadať o pripojenie sa k miestnosti, ale administrátor alebo moderátor bude musieť žiadosť schváliť"; +"screen_create_room_access_section_knocking_option_title" = "Požiadať o pripojenie"; +"screen_join_room_cancel_knock_action" = "Zrušiť žiadosť"; +"screen_join_room_cancel_knock_alert_confirmation" = "Áno, zrušiť"; +"screen_join_room_cancel_knock_alert_description" = "Ste si istí, že chcete zrušiť svoju žiadosť o vstup do tejto miestnosti?"; +"screen_join_room_cancel_knock_alert_title" = "Zrušiť žiadosť o pripojenie"; +"screen_join_room_knock_message_description" = "Správa (voliteľné)"; +"screen_join_room_knock_sent_description" = "Ak bude vaša žiadosť prijatá, dostanete pozvánku na vstup do miestnosti."; +"screen_join_room_knock_sent_title" = "Žiadosť o pripojenie bola odoslaná"; "screen_pinned_timeline_empty_state_description" = "Stlačte správu a vyberte možnosť „%1$@“, ktorú chcete zahrnúť sem."; "screen_pinned_timeline_empty_state_headline" = "Pripnite dôležité správy, aby sa dali ľahko nájsť"; -"screen_pinned_timeline_screen_title_empty" = "Pripnuté správy"; "screen_reset_encryption_password_error" = "Nastala neznáma chyba. Skontrolujte, či je heslo vášho účtu správne a skúste to znova."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Odvolať overenie a odoslať"; "screen_resolve_send_failure_changed_identity_subtitle" = "Svoje overenie môžete odvolať a odoslať túto správu aj tak, alebo ju môžete zatiaľ zrušiť a po opätovnom overení to skúsiť znova %1$@ ."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Načítava sa správa..."; "screen_room_pinned_banner_view_all_button_title" = "Zobraziť všetko"; "screen_room_details_pinned_events_row_title" = "Pripnuté správy"; +"screen_roomlist_knock_event_sent_description" = "Žiadosť o vstup odoslaná"; "screen_timeline_item_menu_send_failure_changed_identity" = "Správa nebola odoslaná, pretože sa zmenila overená totožnosť používateľa %1$@."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Správa nebola odoslaná, pretože %1$@ neoveril/a všetky zariadenia."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení."; -"screen_account_provider_change" = "Zmeniť poskytovateľa účtu"; "screen_account_provider_form_hint" = "Adresa domovského servera"; "screen_account_provider_form_notice" = "Zadajte hľadaný výraz alebo adresu domény."; "screen_account_provider_form_subtitle" = "Vyhľadať spoločnosť, komunitu alebo súkromný server."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Chystáte sa vytvoriť účet na %@"; "screen_advanced_settings_developer_mode" = "Vývojársky režim"; "screen_advanced_settings_developer_mode_description" = "Umožniť prístup k možnostiam a funkciám pre vývojárov."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Vypnite rozšírený textový editor na ručné písanie Markdown."; "screen_advanced_settings_send_read_receipts" = "Potvrdenia o prečítaní"; "screen_advanced_settings_send_read_receipts_description" = "Ak je táto funkcia vypnutá, vaše potvrdenia o prečítaní sa nebudú nikomu odosielať. Stále budete dostávať potvrdenia o prečítaní od ostatných používateľov."; @@ -428,12 +460,14 @@ "screen_change_server_title" = "Vyberte svoj server"; "screen_chat_backup_key_backup_action_disable" = "Vypnúť zálohovanie"; "screen_chat_backup_key_backup_action_enable" = "Zapnúť zálohovanie"; -"screen_chat_backup_key_backup_description" = "Zálohovanie zaisťuje, že nestratíte históriu správ. %1$@."; -"screen_chat_backup_key_backup_title" = "Zálohovanie"; +"screen_chat_backup_key_backup_description" = "Uložte svoju kryptografickú identitu a kľúče správ bezpečne na server. To vám umožní zobraziť históriu správ na všetkých nových zariadeniach. %1$@."; +"screen_chat_backup_key_backup_title" = "Úložisko kľúčov"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Nahrať kľúče z tohto zariadenia"; +"screen_chat_backup_key_storage_toggle_title" = "Povoliť úložisko kľúčov"; "screen_chat_backup_recovery_action_change" = "Zmeniť kľúč na obnovenie"; -"screen_chat_backup_recovery_action_confirm" = "Potvrdiť kľúč na obnovenie"; +"screen_chat_backup_recovery_action_change_description" = "Obnovte svoju kryptografickú totožnosť a históriu správ pomocou kľúča na obnovenie, ak ste stratili všetky svoje existujúce zariadenia."; "screen_chat_backup_recovery_action_confirm_description" = "Vaša záloha konverzácie nie je momentálne synchronizovaná."; -"screen_chat_backup_recovery_action_setup" = "Nastaviť obnovovanie"; "screen_chat_backup_recovery_action_setup_description" = "Získajte prístup k vašim šifrovaným správam aj keď stratíte všetky svoje zariadenia alebo sa odhlásite zo všetkých %1$@ zariadení."; "screen_create_account_title" = "Vytvoriť účet"; "screen_create_new_recovery_key_list_item_1" = "Otvoriť %1$@ v stolnom počítači"; @@ -447,27 +481,26 @@ "screen_create_poll_anonymous_desc" = "Zobraziť výsledky až po skončení ankety"; "screen_create_poll_anonymous_headline" = "Anonymná anketa"; "screen_create_poll_answer_hint" = "Možnosť %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Vaše zmeny nebudú uložené"; "screen_create_poll_cancel_confirmation_title_ios" = "Zrušiť anketu"; "screen_create_poll_question_desc" = "Otázka alebo téma"; "screen_create_poll_question_hint" = "O čom je anketa?"; "screen_create_poll_title" = "Vytvoriť anketu"; "screen_create_room_action_create_room" = "Nová miestnosť"; "screen_create_room_error_creating_room" = "Pri vytváraní miestnosti došlo k chybe"; -"screen_create_room_private_option_description" = "Správy v tejto miestnosti sú šifrované. Šifrovanie už potom nie je možné vypnúť."; -"screen_create_room_private_option_title" = "Súkromná miestnosť (len pre pozvaných)"; -"screen_create_room_public_option_description" = "Správy nie sú šifrované a môže si ich prečítať ktokoľvek. Šifrovanie môžete zapnúť neskôr."; -"screen_create_room_public_option_title" = "Verejná miestnosť (ktokoľvek)"; +"screen_create_room_private_option_description" = "Do tejto miestnosti majú prístup iba pozvaní ľudia. Všetky správy sú end-to-end šifrované."; +"screen_create_room_private_option_title" = "Súkromná miestnosť"; +"screen_create_room_public_option_description" = "Túto miestnosť môže nájsť ktokoľvek.\nMôžete to kedykoľvek zmeniť v nastaveniach miestnosti."; +"screen_create_room_public_option_title" = "Verejná miestnosť"; "screen_create_room_topic_label" = "Téma (voliteľné)"; "screen_deactivate_account_confirmation_dialog_content" = "Prosím potvrďte, že chcete deaktivovať svoj účet. Túto akciu nie je možné vrátiť späť."; "screen_deactivate_account_delete_all_messages" = "Vymazať všetky moje správy"; "screen_deactivate_account_delete_all_messages_notice" = "Upozornenie: Budúcim používateľom sa môžu zobraziť neúplné konverzácie."; "screen_deactivate_account_description" = "Deaktivácia vášho účtu znamená %1$@, že:"; "screen_deactivate_account_description_bold_part" = "nezvratný"; -"screen_deactivate_account_list_item_1" = "%1$@ your account (you can't log back in, and your ID can't be reused)."; +"screen_deactivate_account_list_item_1" = "%1$@ váš účet (nebudete sa môcť znova prihlásiť a vaše ID nebude možné znova použiť)."; "screen_deactivate_account_list_item_1_bold_part" = "Natrvalo zakázať"; -"screen_deactivate_account_list_item_2" = "Remove you from all chat rooms."; -"screen_deactivate_account_list_item_3" = "Delete your account information from our identity server."; +"screen_deactivate_account_list_item_2" = "Odstrániť vás zo všetkých miestností."; +"screen_deactivate_account_list_item_3" = "Odstrániť informácie o vašom účte z nášho servera totožností."; "screen_deactivate_account_list_item_4" = "Vaše správy budú stále viditeľné pre registrovaných používateľov, ale nebudú dostupné pre nových alebo neregistrovaných používateľov, ak sa ich rozhodnete odstrániť."; "screen_deactivate_account_title" = "Deaktivovať účet"; "screen_edit_poll_delete_confirmation" = "Ste si istý, že chcete odstrániť túto anketu?"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Skupinové rozhovory"; "screen_notification_settings_invite_for_me_label" = "Pozvánky"; "screen_notification_settings_mentions_only_disclaimer" = "Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."; -"screen_notification_settings_mentions_section_title" = "Zmienky"; "screen_notification_settings_mode_all" = "Všetky"; "screen_notification_settings_mode_mentions" = "Zmienky"; "screen_notification_settings_notification_section_title" = "Upozorniť ma na"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Vyberte %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "„Prepojiť nové zariadenie“"; "screen_qr_code_login_initial_state_item_4" = "Naskenujte QR kód pomocou tohto zariadenia"; +"screen_qr_code_login_initial_state_subtitle" = "Dostupné iba v prípade, že to podporuje váš poskytovateľ účtu."; "screen_qr_code_login_initial_state_title" = "Ak chcete získať QR kód, otvorte %1$@ na inom zariadení"; "screen_qr_code_login_invalid_scan_state_description" = "Použite QR kód zobrazený na druhom zariadení."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Nesprávny QR kód"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Váš overovací kód"; "screen_recovery_key_change_description" = "Získajte nový kľúč na obnovenie, ak ste stratili svoj existujúci. Po zmene kľúča na obnovenie už starý kľúč nebude fungovať."; "screen_recovery_key_change_generate_key" = "Vygenerovať nový kľúč na obnovenie"; -"screen_recovery_key_change_generate_key_description" = "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí"; "screen_recovery_key_change_success" = "Kľúč na obnovenie bol zmenený"; "screen_recovery_key_change_title" = "Zmeniť kľúč na obnovenie?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Vytvoriť nový kľúč na obnovenie"; @@ -616,18 +648,17 @@ "screen_recovery_key_confirm_key_placeholder" = "Zadať..."; "screen_recovery_key_confirm_lost_recovery_key" = "Stratili ste kľúč na obnovenie?"; "screen_recovery_key_confirm_success" = "Kľúč na obnovu potvrdený"; -"screen_recovery_key_confirm_title" = "Zadajte kľúč na obnovenie"; "screen_recovery_key_copied_to_clipboard" = "Skopírovaný kľúč na obnovenie"; "screen_recovery_key_generating_key" = "Generovanie..."; "screen_recovery_key_save_action" = "Uložiť kľúč na obnovenie"; -"screen_recovery_key_save_description" = "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel."; +"screen_recovery_key_save_description" = "Zapíšte si tento kľúč na obnovenie na bezpečné miesto, napríklad do správcu hesiel, šifrovanej poznámky alebo fyzického trezoru."; "screen_recovery_key_save_key_description" = "Ťuknutím skopírujte kľúč na obnovenie"; "screen_recovery_key_save_title" = "Uložte svoj kľúč na obnovenie"; "screen_recovery_key_setup_confirmation_description" = "Po tomto kroku nebudete mať prístup k novému kľúču na obnovenie."; "screen_recovery_key_setup_confirmation_title" = "Uložili ste kľúč na obnovenie?"; "screen_recovery_key_setup_description" = "Vaša záloha konverzácie je chránená kľúčom na obnovenie. Ak potrebujete nový kľúč na obnovenie, po nastavení si ho môžete znova vytvoriť výberom položky „Zmeniť kľúč na obnovenie“."; "screen_recovery_key_setup_generate_key" = "Vygenerujte si váš kľúč na obnovenie"; -"screen_recovery_key_setup_generate_key_description" = "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí"; +"screen_recovery_key_setup_generate_key_description" = "Nezdieľajte to s nikým!"; "screen_recovery_key_setup_success" = "Úspešné nastavenie obnovy"; "screen_recovery_key_setup_title" = "Nastaviť obnovenie"; "screen_report_content_block_user_hint" = "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Áno, znovu nastaviť teraz"; "screen_reset_encryption_confirmation_alert_subtitle" = "Tento proces je nezvratný."; "screen_reset_encryption_confirmation_alert_title" = "Naozaj chcete obnoviť svoje šifrovanie?"; -"screen_reset_encryption_password_placeholder" = "Zadajte..."; "screen_reset_encryption_password_subtitle" = "Potvrďte, že chcete obnoviť svoje šifrovanie."; "screen_reset_encryption_password_title" = "Ak chcete pokračovať, zadajte heslo účtu"; "screen_reset_identity_confirmation_subtitle" = "Chystáte sa prejsť na svoj %1$@ účet, aby ste obnovili svoju identitu. Potom budete vrátení späť do aplikácie."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Správcovia majú automaticky oprávnenia moderátora"; "screen_room_change_role_moderators_title" = "Upraviť moderátorov"; "screen_room_change_role_unsaved_changes_description" = "Máte neuložené zmeny."; -"screen_room_change_role_unsaved_changes_title" = "Uložiť zmeny?"; "screen_room_details_add_topic_title" = "Pridať tému"; "screen_room_details_already_a_member" = "Už ste členom"; "screen_room_details_already_invited" = "Už ste pozvaní"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Nepodarilo sa zrušiť stlmenie tejto miestnosti, skúste to prosím znova."; "screen_room_details_notification_mode_custom" = "Vlastné"; "screen_room_details_notification_mode_default" = "Predvolené"; -"screen_room_details_notification_title" = "Oznámenia"; "screen_room_details_share_room_title" = "Zdieľať miestnosť"; "screen_room_details_title" = "Informácie o miestnosti"; "screen_room_details_updating_room" = "Aktualizácia miestnosti..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Odblokovať"; "screen_room_member_details_unblock_alert_description" = "Všetky správy od nich budete môcť opäť vidieť."; "screen_room_member_details_unblock_user" = "Odblokovať používateľa"; +"screen_room_member_details_verify_button_subtitle" = "Použite webovú aplikáciu na overenie tohto používateľa."; +"screen_room_member_details_verify_button_title" = "Overiť %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Zakázať"; "screen_room_member_list_ban_member_confirmation_description" = "Nebudú sa môcť pripojiť k tejto miestnosti znova ani ak budú pozvaní."; "screen_room_member_list_ban_member_confirmation_title" = "Ste si istý, že chcete zakázať tohto člena?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Zakazuje sa %1$@"; "screen_room_member_list_manage_member_ban" = "Odstrániť a zakázať člena"; "screen_room_member_list_manage_member_remove" = "Odstrániť z miestnosti"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Odstrániť a zakázať člena"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Iba odstrániť člena"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Odstrániť člena a zakázať vstup v budúcnosti?"; "screen_room_member_list_manage_member_unban_action" = "Zrušiť zákaz"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Zobraziť menej"; "screen_room_timeline_message_copied" = "Správa skopírovaná"; "screen_room_timeline_no_permission_to_post" = "Nemáte povolenie uverejňovať príspevky v tejto miestnosti"; -"screen_room_timeline_reactions_show_less" = "Zobraziť menej"; "screen_room_timeline_reactions_show_more" = "Zobraziť viac"; "screen_room_timeline_read_marker_title" = "Nové"; "screen_room_title" = "Konverzácia"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Označiť ako prečítané"; "screen_roomlist_mark_as_unread" = "Označiť ako neprečítané"; "screen_roomlist_room_directory_button_title" = "Prehliadať všetky miestnosti"; -"screen_server_confirmation_change_server" = "Zmeniť poskytovateľa účtu"; "screen_server_confirmation_message_login_element_dot_io" = "Súkromný server pre zamestnancov spoločnosti Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."; "screen_server_confirmation_message_register" = "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Porovnať čísla"; "screen_session_verification_complete_subtitle" = "Vaša nová relácia je teraz overená. Má prístup k vašim zašifrovaným správam a ostatní používatelia ju budú vidieť ako dôveryhodnú."; "screen_session_verification_enter_recovery_key" = "Zadajte kľúč na obnovenie"; +"screen_session_verification_failed_subtitle" = "Buď žiadosť vypršala, žiadosť bola zamietnutá, alebo došlo k nesúladu overovania."; "screen_session_verification_open_existing_session_subtitle" = "Dokážte, že ste to vy, aby ste získali prístup k histórii vašich zašifrovaných správ."; "screen_session_verification_open_existing_session_title" = "Otvoriť existujúcu reláciu"; "screen_session_verification_positive_button_canceled" = "Zopakovať overenie"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Čaká sa na zhodu"; "screen_session_verification_ready_subtitle" = "Porovnajte jedinečnú sadu emotikonov."; "screen_session_verification_request_accepted_subtitle" = "Porovnajte jedinečné emotikony a uistite sa, že sú zobrazené v rovnakom poradí."; +"screen_session_verification_request_details_timestamp" = "Prihlásený"; +"screen_session_verification_request_failure_title" = "Overenie zlyhalo"; +"screen_session_verification_request_footer" = "Pokračujte iba vtedy, ak ste toto overenie začali."; +"screen_session_verification_request_subtitle" = "Overte druhé zariadenie, aby bola vaša história správ zabezpečená."; +"screen_session_verification_request_success_subtitle" = "Teraz môžete bezpečne čítať alebo odosielať správy na svojom druhom zariadení."; +"screen_session_verification_request_success_title" = "Zariadenie overené"; +"screen_session_verification_request_title" = "Vyžadované overenie"; "screen_session_verification_they_dont_match" = "Nezhodujú sa"; "screen_session_verification_they_match" = "Zhodujú sa"; "screen_session_verification_waiting_to_accept_subtitle" = "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."; "screen_signout_key_backup_disabled_title" = "Vypli ste zálohovanie"; "screen_signout_key_backup_offline_subtitle" = "Keď ste sa odpojili od internetu, vaše kľúče sa ešte stále zálohovali. Pripojte sa znova k internetu, aby sa vaše kľúče mohli zálohovať pred odhlásením."; -"screen_signout_key_backup_offline_title" = "Vaše kľúče sa ešte stále zálohujú"; "screen_signout_key_backup_ongoing_subtitle" = "Pred odhlásením počkajte, kým sa to dokončí."; "screen_signout_key_backup_ongoing_title" = "Vaše kľúče sa ešte stále zálohujú"; "screen_signout_recovery_disabled_subtitle" = "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."; "screen_signout_recovery_disabled_title" = "Obnovenie nie je nastavené"; "screen_signout_save_recovery_key_subtitle" = "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam."; -"screen_signout_save_recovery_key_title" = "Uložili ste si kľúč na obnovenie?"; "screen_start_chat_error_starting_chat" = "Pri pokuse o spustenie konverzácie sa vyskytla chyba"; "screen_view_location_title" = "Poloha"; "screen_welcome_bullet_1" = "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."; @@ -919,7 +952,6 @@ "test_language_identifier" = "sk"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Riešenie problémov"; -"troubleshoot_notifications_entry_point_title" = "Oznámenia riešení problémov"; "troubleshoot_notifications_screen_action" = "Spustiť testy"; "troubleshoot_notifications_screen_action_again" = "Spustiť testy znova"; "troubleshoot_notifications_screen_failure" = "Niektoré testy zlyhali. Skontrolujte prosím podrobnosti."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Uistite sa, že sú dostupní distribútori UnifiedPush."; "troubleshoot_notifications_test_unified_push_failure" = "Nenašli sa žiadni distribútori push."; "troubleshoot_notifications_test_unified_push_title" = "Skontrolovať UnifiedPush"; +"a11y_poll" = "Anketa"; +"banner_set_up_recovery_submit" = "Nastaviť obnovenie"; "dialog_title_error" = "Chyba"; "dialog_title_success" = "Úspech"; "notification_fallback_content" = "Oznámenie"; "notification_invitation_action_join" = "Pripojiť sa"; +"notification_invitation_action_reject" = "Odmietnuť"; "notification_room_action_mark_as_read" = "Označiť ako prečítané"; "notification_room_action_quick_reply" = "Rýchla odpoveď"; +"screen_pinned_timeline_screen_title_empty" = "Pripnuté správy"; "screen_room_mentions_at_room_title" = "Všetci"; +"screen_account_provider_change" = "Zmeniť poskytovateľa účtu"; "screen_account_provider_signin_subtitle" = "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."; "screen_account_provider_signup_subtitle" = "Tu budú žiť vaše konverzácie - podobne ako používate poskytovateľa e-mailových služieb na uchovávanie e-mailov."; "screen_analytics_settings_help_us_improve" = "Zdieľajte anonymné údaje o používaní, aby sme mohli identifikovať problémy."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Všetky správy od nich budete môcť opäť vidieť."; "screen_blocked_users_unblock_alert_title" = "Odblokovať používateľa"; "screen_bug_report_rash_logs_alert_title" = "%1$@ zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?"; +"screen_chat_backup_recovery_action_confirm" = "Zadajte kľúč na obnovenie"; +"screen_chat_backup_recovery_action_setup" = "Nastaviť obnovenie"; +"screen_create_poll_cancel_confirmation_content_ios" = "Vaše zmeny nebudú uložené"; "screen_create_room_add_people_title" = "Pozvať ľudí"; "screen_create_room_room_name_label" = "Názov miestnosti"; "screen_create_room_title" = "Vytvoriť miestnosť"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Upraviť anketu"; "screen_identity_use_another_device" = "Použite iné zariadenie"; "screen_login_subtitle" = "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu."; +"screen_notification_settings_mentions_section_title" = "Zmienky"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Skúste to znova"; +"screen_recovery_key_change_generate_key_description" = "Nezdieľajte to s nikým!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Zablokovať používateľa"; +"screen_reset_encryption_password_placeholder" = "Zadať..."; "screen_room_attachment_source_camera_photo" = "Urobiť fotku"; "screen_room_change_permissions_everyone" = "Všetci"; "screen_room_change_permissions_member_moderation" = "Moderovanie členov"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Správcovia"; "screen_room_change_role_section_moderators" = "Moderátori"; "screen_room_change_role_section_users" = "Členovia"; +"screen_room_change_role_unsaved_changes_title" = "Uložiť zmeny?"; "screen_room_details_invite_people_title" = "Pozvať ľudí"; "screen_room_details_leave_conversation_title" = "Opustiť konverzáciu"; "screen_room_details_leave_room_title" = "Opustiť miestnosť"; +"screen_room_details_notification_title" = "Oznámenia"; "screen_room_details_roles_and_permissions" = "Roly a povolenia"; "screen_room_details_room_name_label" = "Názov miestnosti"; "screen_room_details_security_title" = "Bezpečnosť"; "screen_room_details_topic_title" = "Téma"; "screen_room_error_failed_processing_media" = "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Odstrániť a zakázať člena"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Iba zmienky a kľúčové slová"; +"screen_room_timeline_reactions_show_less" = "Zobraziť menej"; "screen_roomlist_filter_people" = "Ľudia"; +"screen_server_confirmation_change_server" = "Zmeniť poskytovateľa účtu"; +"screen_session_verification_request_failure_subtitle" = "Buď žiadosť vypršala, žiadosť bola zamietnutá, alebo došlo k nesúladu overovania."; "screen_signout_confirmation_dialog_submit" = "Odhlásiť sa"; "screen_signout_confirmation_dialog_title" = "Odhlásiť sa"; +"screen_signout_key_backup_offline_title" = "Vaše kľúče sa ešte stále zálohujú"; "screen_signout_preference_item" = "Odhlásiť sa"; +"screen_signout_save_recovery_key_title" = "Uložili ste kľúč na obnovenie?"; +"troubleshoot_notifications_entry_point_title" = "Oznámenia riešení problémov"; diff --git a/ElementX/Resources/Localizations/sv.lproj/Localizable.strings b/ElementX/Resources/Localizations/sv.lproj/Localizable.strings index db7a12b982..579f9bff91 100644 --- a/ElementX/Resources/Localizations/sv.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/sv.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pausa"; "a11y_pin_field" = "PIN-fält"; "a11y_play" = "Spela upp"; -"a11y_poll" = "Omröstning"; "a11y_poll_end" = "Avslutade omröstning"; "a11y_react_with" = "Reagera med %1$@"; "a11y_react_with_other_emojis" = "Reagera med andra emojier"; @@ -41,6 +40,7 @@ "action_create" = "Skapa"; "action_create_a_room" = "Skapa ett rum"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Neka"; "action_delete_poll" = "Radera omröstning"; "action_disable" = "Inaktivera"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Glömt lösenordet?"; "action_forward" = "Vidarebefordra"; "action_go_back" = "Gå tillbaka"; +"action_ignore" = "Ignore"; "action_invite" = "Bjud in"; "action_invite_friends" = "Bjud in personer"; "action_invite_friends_to_app" = "Bjud in personer till %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Lämna"; "action_leave_conversation" = "Lämna konversation"; "action_leave_room" = "Lämna rum"; +"action_load_more" = "Ladda mer"; "action_manage_account" = "Hantera konto"; "action_manage_devices" = "Hantera enheter"; "action_message" = "Meddela"; @@ -93,6 +95,7 @@ "action_send_message" = "Skicka meddelande"; "action_share" = "Dela"; "action_share_link" = "Dela länk"; +"action_show" = "Show"; "action_sign_in_again" = "Logga in igen"; "action_signout" = "Logga ut"; "action_signout_anyway" = "Logga ut ändå"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "Visa i tidslinjen"; "action_view_source" = "Visa källkod"; "action_yes" = "Ja"; -"action.load_more" = "Ladda mer"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; +"banner_migrate_to_native_sliding_sync_description" = "Din server stöder nu ett nytt, snabbare protokoll. Logga ut och logga in igen för att uppgradera nu. Om du gör detta nu hjälper du dig att undvika en tvingad utloggning när det gamla protokollet tas bort senare."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter."; -"banner.set_up_recovery.title" = "Ställ in återställning"; +"banner_migrate_to_native_sliding_sync_title" = "Uppgradering tillgänglig"; +"banner_set_up_recovery_content" = "Skapa en ny återställningsnyckel som kan användas för att återställa din krypterade meddelandehistorik om du förlorar åtkomst till dina enheter."; +"banner_set_up_recovery_title" = "Ställ in återställning"; "common_about" = "Om"; "common_acceptable_use_policy" = "Policy för godtagbar användning"; "common_advanced_settings" = "Avancerade inställningar"; @@ -133,10 +134,12 @@ "common_dark" = "Mörkt"; "common_decryption_error" = "Avkrypteringsfel"; "common_developer_options" = "Utvecklaralternativ"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Direktchatt"; "common_edited_suffix" = "(redigerad)"; "common_editing" = "Redigerar"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Kryptering aktiverad"; "common_enter_your_pin" = "Ange din PIN-kod"; "common_error" = "Fel"; @@ -147,6 +150,7 @@ "common_favourited" = "Favoriter"; "common_file" = "Fil"; "common_forward_message" = "Vidarebefordra meddelande"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Bild"; "common_in_reply_to" = "Som svar på %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Modernt"; "common_mute" = "Tysta"; "common_no_results" = "Inga resultat"; +"common_no_room_name" = "Inget rumsnamn"; "common_offline" = "Frånkopplad"; "common_optic_id_ios" = "Optic ID"; "common_or" = "eller"; @@ -170,6 +175,8 @@ "common_permalink" = "Permalänk"; "common_permission" = "Behörighet"; "common_please_wait" = "Vänligen vänta …"; +"common_poll_end_confirmation" = "Är du säker på att du vill avsluta den här omröstningen?"; +"common_poll_summary" = "Omröstning: %1$@"; "common_poll_total_votes" = "Totalt antal röster: %1$@"; "common_poll_undisclosed_text" = "Resultaten visas efter att omröstningen har avslutats"; "common_privacy_policy" = "Integritetspolicy"; @@ -200,6 +207,7 @@ "common_settings" = "Inställningar"; "common_shared_location" = "Delade plats"; "common_signing_out" = "Loggar ut"; +"common_something_went_wrong" = "Något gick fel"; "common_starting_chat" = "Startar chatt …"; "common_sticker" = "Dekal"; "common_success" = "Lyckades"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Vad handlar det här rummet om?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Kan inte avkryptera"; +"common_unable_to_decrypt_no_access" = "Du har inte tillgång till det här meddelandet"; "common_unable_to_invite_message" = "Inbjudan kunde inte skickas till en eller flera användare."; "common_unable_to_invite_title" = "Kunde inte skicka inbjudningar"; "common_unlock" = "Lås upp"; @@ -221,23 +230,30 @@ "common_username" = "Användarnamn"; "common_verification_cancelled" = "Verifiering avbruten"; "common_verification_complete" = "Verifieringen slutförd"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verifiera enheten"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Röstmeddelande"; "common_waiting" = "Väntar …"; "common_waiting_for_decryption_key" = "Väntar på detta meddelande"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Visa inte detta igen"; "common.open_source_licenses" = "Licenser för öppen källkod"; "common.pinned" = "Fäst"; "common.send_to" = "Skicka till"; -"common_no_room_name" = "Inget rumsnamn"; -"common_poll_end_confirmation" = "Är du säker på att du vill avsluta den här omröstningen?"; -"common_poll_summary" = "Omröstning: %1$@"; -"common_something_went_wrong" = "Något gick fel"; -"common_unable_to_decrypt_no_access" = "Du har inte tillgång till det här meddelandet"; -"common_verify_device" = "Verifiera enheten"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Din chattsäkerhetskopia är för närvarande inte synkroniserad. Du måste ange din återställningsnyckel för att behålla åtkomsten till din chattsäkerhetskopia."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Ange din återställningsnyckel"; "crash_detection_dialog_content" = "%1$@ kraschade senast den användes. Vill du dela en kraschrapport med oss?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "För att låta programmet använda kameran, vänligen ge behörigheten i systeminställningarna."; "dialog_permission_generic" = "Vänligen ge behörigheten i systeminställningarna."; "dialog_permission_location_description_ios" = "Ge åtkomst i Inställningar -> Plats."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Tysta aviseringar"; "notification_incoming_call" = "Inkommande samtal"; "notification_inline_reply_failed" = "** Misslyckades att skicka - vänligen öppna rummet"; -"notification_invitation_action_reject" = "Avvisa"; "notification_invite_body" = "Bjöd in dig att chatta"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Nämnde dig: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Ta bort indrag"; "rich_text_editor_url_placeholder" = "Länk"; "rich_text_editor_a11y_add_attachment" = "Lägg till bilaga"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Anpassad bas-URL för Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Ange en anpassad bas-URL för Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Ogiltig URL, se till att du inkluderar protokollet (http/https) och rätt adress."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Tryck på ett meddelande och välj ”%1$@” för att inkludera det här."; "screen_pinned_timeline_empty_state_headline" = "Fäst viktiga meddelanden så att de lätt kan upptäckas"; -"screen_pinned_timeline_screen_title_empty" = "Fästa meddelanden"; "screen_reset_encryption_password_error" = "Ett okänt fel inträffade. Kontrollera att ditt kontolösenord är korrekt och försök igen."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Laddar meddelande …"; "screen_room_pinned_banner_view_all_button_title" = "Visa alla"; "screen_room_details_pinned_events_row_title" = "Fästa meddelanden"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Byt kontoleverantör"; "screen_account_provider_form_hint" = "Hemserveradress"; "screen_account_provider_form_notice" = "Ange ett sökord eller en domänadress."; "screen_account_provider_form_subtitle" = "Sök efter ett företag, en gemenskap eller en privat server."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Du är på väg att skapa ett konto på %@"; "screen_advanced_settings_developer_mode" = "Utvecklarläge"; "screen_advanced_settings_developer_mode_description" = "Aktivera för att ha tillgång till funktionalitet för utvecklare."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Inaktivera rik-text-redigeraren för att skriva Markdown manuellt."; "screen_advanced_settings_send_read_receipts" = "Läskvitton"; "screen_advanced_settings_send_read_receipts_description" = "Om det är avstängt kommer dina läskvitton inte att skickas till någon. Du kommer fortfarande att få läskvitton från andra användare."; @@ -429,11 +461,13 @@ "screen_chat_backup_key_backup_action_disable" = "Stäng av säkerhetskopiering"; "screen_chat_backup_key_backup_action_enable" = "Slå på säkerhetskopiering"; "screen_chat_backup_key_backup_description" = "Säkerhetskopior ser till att du inte blir av med din meddelandehistorik. %1$@."; -"screen_chat_backup_key_backup_title" = "Säkerhetskopia"; +"screen_chat_backup_key_backup_title" = "Nyckellagring"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Byt återställningsnyckel"; -"screen_chat_backup_recovery_action_confirm" = "Ange återställningsnyckel"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Din chattsäkerhetskopia är för närvarande osynkroniserad."; -"screen_chat_backup_recovery_action_setup" = "Ställ in återställning"; "screen_chat_backup_recovery_action_setup_description" = "Få tillgång till dina krypterade meddelanden om du tappar bort alla dina enheter eller blir utloggad ur %1$@ överallt."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Öppna %1$@ på en skrivbordsenhet"; @@ -447,17 +481,16 @@ "screen_create_poll_anonymous_desc" = "Visa resultat först efter att omröstningen avslutats"; "screen_create_poll_anonymous_headline" = "Dölj röster"; "screen_create_poll_answer_hint" = "Alternativ %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Dina ändringar sparas inte"; "screen_create_poll_cancel_confirmation_title_ios" = "Avbryt omröstning"; "screen_create_poll_question_desc" = "Fråga eller ämne"; "screen_create_poll_question_hint" = "Vad handlar omröstningen om?"; "screen_create_poll_title" = "Skapa omröstning"; "screen_create_room_action_create_room" = "Nytt rum"; "screen_create_room_error_creating_room" = "Ett fel uppstod när rummet skapades"; -"screen_create_room_private_option_description" = "Meddelanden i det här rummet är krypterade. Kryptering kan inte inaktiveras efteråt."; -"screen_create_room_private_option_title" = "Privat rum (endast inbjudan)"; -"screen_create_room_public_option_description" = "Meddelanden är inte krypterade och vem som helst kan läsa dem. Du kan aktivera kryptering vid ett senare tillfälle."; -"screen_create_room_public_option_title" = "Offentligt rum (vem som helst)"; +"screen_create_room_private_option_description" = "Endast inbjudna personer har tillgång till detta rum. Alla meddelanden är totalsträckskrypterade."; +"screen_create_room_private_option_title" = "Privat rum"; +"screen_create_room_public_option_description" = "Vem som helst kan hitta det här rummet.\nDu kan ändra detta när som helst i rumsinställningarna."; +"screen_create_room_public_option_title" = "Offentligt rum"; "screen_create_room_topic_label" = "Ämne (valfritt)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; "screen_deactivate_account_delete_all_messages" = "Delete all my messages"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Gruppchattar"; "screen_notification_settings_invite_for_me_label" = "Inbjudningar"; "screen_notification_settings_mentions_only_disclaimer" = "Din hemserver stöder inte det här alternativet i krypterade rum, du kanske inte aviseras i vissa rum."; -"screen_notification_settings_mentions_section_title" = "Omnämnanden"; "screen_notification_settings_mode_all" = "Alla"; "screen_notification_settings_mode_mentions" = "Omnämnanden"; "screen_notification_settings_notification_section_title" = "Meddela mig för"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Välj %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "”Länka ny enhet”"; "screen_qr_code_login_initial_state_item_4" = "Skanna QR-koden med den här enheten"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Öppna %1$@ på en annan enhet för att få QR-koden"; "screen_qr_code_login_invalid_scan_state_description" = "Använd QR-koden som visas på den andra enheten."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Fel QR-kod"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Din verifieringskod"; "screen_recovery_key_change_description" = "Få en ny återställningsnyckel om du har tappat bort din befintliga. När du har bytt din återställningsnyckel fungerar din gamla inte längre."; "screen_recovery_key_change_generate_key" = "Generera en ny återställningsnyckel"; -"screen_recovery_key_change_generate_key_description" = "Se till att du kan lagra din återställningsnyckel någonstans säkert"; "screen_recovery_key_change_success" = "Återställningsnyckel ändrad"; "screen_recovery_key_change_title" = "Byt återställningsnyckel?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Skapa ny återställningsnyckel"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Ange …"; "screen_recovery_key_confirm_lost_recovery_key" = "Blivit av med din återställningsnyckel?"; "screen_recovery_key_confirm_success" = "Återställningsnyckel bekräftad"; -"screen_recovery_key_confirm_title" = "Ange din återställningsnyckel"; "screen_recovery_key_copied_to_clipboard" = "Kopierade återställningsnyckel"; "screen_recovery_key_generating_key" = "Genererar …"; "screen_recovery_key_save_action" = "Spara återställningsnyckeln"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Ja, återställ nu"; "screen_reset_encryption_confirmation_alert_subtitle" = "Denna process är irreversibel."; "screen_reset_encryption_confirmation_alert_title" = "Är du säker på att du vill återställa din kryptering?"; -"screen_reset_encryption_password_placeholder" = "Ange …"; "screen_reset_encryption_password_subtitle" = "Bekräfta att du vill återställa din kryptering."; "screen_reset_encryption_password_title" = "Ange ditt kontolösenord för att fortsätta"; "screen_reset_identity_confirmation_subtitle" = "Du är på väg att gå till ditt %1$@-konto för att återställa din identitet. Därefter kommer du att tas tillbaka till appen."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Administratörer har automatiskt moderatorbehörighet"; "screen_room_change_role_moderators_title" = "Redigera moderatorer"; "screen_room_change_role_unsaved_changes_description" = "Du har osparade ändringar."; -"screen_room_change_role_unsaved_changes_title" = "Spara ändringar?"; "screen_room_details_add_topic_title" = "Lägg till ämne"; "screen_room_details_already_a_member" = "Redan medlem"; "screen_room_details_already_invited" = "Redan inbjuden"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Misslyckades att avtysta det här rummet, vänligen pröva igen."; "screen_room_details_notification_mode_custom" = "Anpassad"; "screen_room_details_notification_mode_default" = "Förval"; -"screen_room_details_notification_title" = "Aviseringar"; "screen_room_details_share_room_title" = "Dela rum"; "screen_room_details_title" = "Rumsinfo"; "screen_room_details_updating_room" = "Uppdaterar rummet …"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Avblockera"; "screen_room_member_details_unblock_alert_description" = "Du kommer att kunna se alla meddelanden från dem igen."; "screen_room_member_details_unblock_user" = "Avblockera användare"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Banna"; "screen_room_member_list_ban_member_confirmation_description" = "Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in."; "screen_room_member_list_ban_member_confirmation_title" = "Är du säker på att du vill banna den här medlemmen?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Bannar %1$@"; "screen_room_member_list_manage_member_ban" = "Ta bort och banna medlem"; "screen_room_member_list_manage_member_remove" = "Ta bort från rummet"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ta bort och banna medlem"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Ta bara bort medlem"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Ta bort medlem och banna från att gå med i framtiden?"; "screen_room_member_list_manage_member_unban_action" = "Avbanna"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Visa mindre"; "screen_room_timeline_message_copied" = "Meddelande kopierat"; "screen_room_timeline_no_permission_to_post" = "Du är inte behörig att göra inlägg i det här rummet"; -"screen_room_timeline_reactions_show_less" = "Visa mindre"; "screen_room_timeline_reactions_show_more" = "Visa mer"; "screen_room_timeline_read_marker_title" = "Nytt"; "screen_room_title" = "Chatt"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Markera som läst"; "screen_roomlist_mark_as_unread" = "Markera som oläst"; "screen_roomlist_room_directory_button_title" = "Bläddra bland alla rum"; -"screen_server_confirmation_change_server" = "Byt kontoleverantör"; "screen_server_confirmation_message_login_element_dot_io" = "En privat server för Element-anställda."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."; "screen_server_confirmation_message_register" = "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Jämför siffror"; "screen_session_verification_complete_subtitle" = "Din nya session är nu verifierad. Den har tillgång till dina krypterade meddelanden, och andra användare kommer att se den som betrodd."; "screen_session_verification_enter_recovery_key" = "Ange återställningsnyckel"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Bevisa att det är du för att komma åt din krypterade meddelandehistorik."; "screen_session_verification_open_existing_session_title" = "Öppna en befintlig session"; "screen_session_verification_positive_button_canceled" = "Försök att verifiera igen"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Väntar på att matcha"; "screen_session_verification_ready_subtitle" = "Jämför en unik uppsättning emojis."; "screen_session_verification_request_accepted_subtitle" = "Jämför de unika emojierna och se till att de visas i samma ordning."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "De matchar inte"; "screen_session_verification_they_match" = "De matchar"; "screen_session_verification_waiting_to_accept_subtitle" = "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden."; "screen_signout_key_backup_disabled_title" = "Du har stängt av säkerhetskopiering"; "screen_signout_key_backup_offline_subtitle" = "Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut."; -"screen_signout_key_backup_offline_title" = "Dina nycklar säkerhetskopieras fortfarande"; "screen_signout_key_backup_ongoing_subtitle" = "Vänta tills detta är klart innan du loggar ut."; "screen_signout_key_backup_ongoing_title" = "Dina nycklar säkerhetskopieras fortfarande"; "screen_signout_recovery_disabled_subtitle" = "Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden."; "screen_signout_recovery_disabled_title" = "Återställning inte inställd"; "screen_signout_save_recovery_key_subtitle" = "Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden."; -"screen_signout_save_recovery_key_title" = "Har du sparat din återställningsnyckel?"; "screen_start_chat_error_starting_chat" = "Ett fel uppstod när du försökte starta en chatt"; "screen_view_location_title" = "Plats"; "screen_welcome_bullet_1" = "Samtal, omröstningar, sökning och mer kommer att läggas till senare i år."; @@ -919,7 +952,6 @@ "test_language_identifier" = "sv"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Felsök"; -"troubleshoot_notifications_entry_point_title" = "Felsök aviseringar"; "troubleshoot_notifications_screen_action" = "Kör tester"; "troubleshoot_notifications_screen_action_again" = "Kör tester igen"; "troubleshoot_notifications_screen_failure" = "Vissa tester misslyckades. Kontrollera detaljerna."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Se till att UnifiedPush-distributörer är tillgängliga."; "troubleshoot_notifications_test_unified_push_failure" = "Inga push-distributörer hittades."; "troubleshoot_notifications_test_unified_push_title" = "Kontrollera UnifiedPush"; +"a11y_poll" = "Omröstning"; +"banner_set_up_recovery_submit" = "Ställ in återställning"; "dialog_title_error" = "Fel"; "dialog_title_success" = "Lyckades"; "notification_fallback_content" = "notis"; "notification_invitation_action_join" = "Gå med"; +"notification_invitation_action_reject" = "Avvisa"; "notification_room_action_mark_as_read" = "Markera som läst"; "notification_room_action_quick_reply" = "Snabbsvar"; +"screen_pinned_timeline_screen_title_empty" = "Fästa meddelanden"; "screen_room_mentions_at_room_title" = "Alla"; +"screen_account_provider_change" = "Byt kontoleverantör"; "screen_account_provider_signin_subtitle" = "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev."; "screen_account_provider_signup_subtitle" = "Det är här dina konversationer kommer att sparas - precis som du skulle använda en e-postleverantör för att spara dina e-brev."; "screen_analytics_settings_help_us_improve" = "Dela anonyma användningsdata för att hjälpa oss att identifiera problem."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Du kommer att kunna se alla meddelanden från dem igen."; "screen_blocked_users_unblock_alert_title" = "Avblockera användare"; "screen_bug_report_rash_logs_alert_title" = "%1$@ kraschade senast den användes. Vill du dela en kraschrapport med oss?"; +"screen_chat_backup_recovery_action_confirm" = "Ange återställningsnyckel"; +"screen_chat_backup_recovery_action_setup" = "Ställ in återställning"; +"screen_create_poll_cancel_confirmation_content_ios" = "Dina ändringar kommer inte att sparas"; "screen_create_room_add_people_title" = "Bjud in personer"; "screen_create_room_room_name_label" = "Rumsnamn"; "screen_create_room_title" = "Skapa ett rum"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Redigera omröstning"; "screen_identity_use_another_device" = "Använd en annan enhet"; "screen_login_subtitle" = "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation."; +"screen_notification_settings_mentions_section_title" = "Omnämnanden"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Försök igen"; +"screen_recovery_key_change_generate_key_description" = "Se till att du kan lagra din återställningsnyckel någonstans säkert"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Blockera användare"; +"screen_reset_encryption_password_placeholder" = "Ange …"; "screen_room_attachment_source_camera_photo" = "Ta ett foto"; "screen_room_change_permissions_everyone" = "Alla"; "screen_room_change_permissions_member_moderation" = "Medlemsmoderering"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Administratörer"; "screen_room_change_role_section_moderators" = "Moderatorer"; "screen_room_change_role_section_users" = "Medlemmar"; +"screen_room_change_role_unsaved_changes_title" = "Spara ändringar?"; "screen_room_details_invite_people_title" = "Bjud in personer"; "screen_room_details_leave_conversation_title" = "Lämna konversation"; "screen_room_details_leave_room_title" = "Lämna rum"; +"screen_room_details_notification_title" = "Aviseringar"; "screen_room_details_roles_and_permissions" = "Roller och behörigheter"; "screen_room_details_room_name_label" = "Rumsnamn"; "screen_room_details_security_title" = "Säkerhet"; "screen_room_details_topic_title" = "Ämne"; "screen_room_error_failed_processing_media" = "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ta bort och banna medlem"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Endast omnämnanden och nyckelord"; +"screen_room_timeline_reactions_show_less" = "Visa mindre"; "screen_roomlist_filter_people" = "Personer"; +"screen_server_confirmation_change_server" = "Byt kontoleverantör"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Logga ut"; "screen_signout_confirmation_dialog_title" = "Logga ut"; +"screen_signout_key_backup_offline_title" = "Dina nycklar säkerhetskopieras fortfarande"; "screen_signout_preference_item" = "Logga ut"; +"screen_signout_save_recovery_key_title" = "Har du sparat din återställningsnyckel?"; +"troubleshoot_notifications_entry_point_title" = "Felsök aviseringar"; diff --git a/ElementX/Resources/Localizations/uk.lproj/Localizable.strings b/ElementX/Resources/Localizations/uk.lproj/Localizable.strings index d0dad527bb..6e5b02cfdd 100644 --- a/ElementX/Resources/Localizations/uk.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/uk.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Пауза"; "a11y_pin_field" = "Поле PIN-коду"; "a11y_play" = "Відтворити"; -"a11y_poll" = "Опитування"; "a11y_poll_end" = "Опитування завершено"; "a11y_react_with" = "Реагувати з%1$@"; "a11y_react_with_other_emojis" = "Відреагувати іншими смайликами"; @@ -41,6 +40,7 @@ "action_create" = "Створити"; "action_create_a_room" = "Створити кімнату"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Відхилити"; "action_delete_poll" = "Видалити опитування"; "action_disable" = "Вимкнути"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Забули пароль?"; "action_forward" = "Переслати"; "action_go_back" = "Повернутися"; +"action_ignore" = "Ignore"; "action_invite" = "Запросити"; "action_invite_friends" = "Запросити людей"; "action_invite_friends_to_app" = "Запросити людей до %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Вийти"; "action_leave_conversation" = "Залишити розмову"; "action_leave_room" = "Вийти з кімнати"; +"action_load_more" = "Завантажити ще"; "action_manage_account" = "Керування обліковим записом"; "action_manage_devices" = "Керування пристроями"; "action_message" = "Написати"; @@ -93,6 +95,7 @@ "action_send_message" = "Надіслати повідомлення"; "action_share" = "Поділитися"; "action_share_link" = "Поширити посилання"; +"action_show" = "Show"; "action_sign_in_again" = "Увійдіть знову"; "action_signout" = "Вийти"; "action_signout_anyway" = "Все одно вийти"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Переглянути джерело"; "action_yes" = "Так"; -"action.load_more" = "Завантажити ще"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Відомості"; "common_acceptable_use_policy" = "Політика прийнятного використання"; "common_advanced_settings" = "Додаткові налаштування"; @@ -133,10 +134,12 @@ "common_dark" = "Темна"; "common_decryption_error" = "Помилка розшифровки"; "common_developer_options" = "Налаштування розробника"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Особистий чат"; "common_edited_suffix" = "(відредаговано)"; "common_editing" = "Редагування"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Шифрування ввімкнено"; "common_enter_your_pin" = "Введіть свій PIN-код"; "common_error" = "Помилка"; @@ -147,6 +150,7 @@ "common_favourited" = "Вибране"; "common_file" = "Файл"; "common_forward_message" = "Переслати повідомлення"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Зображення"; "common_in_reply_to" = "У відповідь на %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "Модерн"; "common_mute" = "Вимкнути звук"; "common_no_results" = "Немає результатів"; +"common_no_room_name" = "Немає назви кімнати"; "common_offline" = "Не в мережі"; "common_optic_id_ios" = "Optic ID"; "common_or" = "або"; @@ -170,6 +175,8 @@ "common_permalink" = "Постійне посилання"; "common_permission" = "Дозвіл"; "common_please_wait" = "Будь ласка, зачекайте…"; +"common_poll_end_confirmation" = "Ви впевнені, що хочете закінчити це опитування?"; +"common_poll_summary" = "Опитування: %1$@"; "common_poll_total_votes" = "Всього голосів: %1$@"; "common_poll_undisclosed_text" = "Результати будуть показані після завершення опитування"; "common_privacy_policy" = "Політика конфіденційності"; @@ -200,6 +207,7 @@ "common_settings" = "Налаштування"; "common_shared_location" = "Поширене розташування"; "common_signing_out" = "Вихід"; +"common_something_went_wrong" = "Щось пішло не так"; "common_starting_chat" = "Початок чату..."; "common_sticker" = "Наліпка"; "common_success" = "Успіх"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Про що ця кімната?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Неможливо розшифрувати"; +"common_unable_to_decrypt_no_access" = "Ви не маєте доступу до цього повідомлення"; "common_unable_to_invite_message" = "Не вдалося надіслати запрошення одному чи кільком користувачам."; "common_unable_to_invite_title" = "Не вдалося надіслати запрошення"; "common_unlock" = "Розблокувати"; @@ -221,23 +230,30 @@ "common_username" = "Ім'я користувача"; "common_verification_cancelled" = "Верифікацію скасовано"; "common_verification_complete" = "Верифікацію завершено"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Перевірте пристрій"; +"common_verify_identity" = "Verify identity"; "common_video" = "Відео"; "common_voice_message" = "Голосове повідомлення"; "common_waiting" = "Очікування..."; "common_waiting_for_decryption_key" = "Чекаємо на це повідомлення"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Не показувати це знову"; "common.open_source_licenses" = "Ліцензії відкритого коду"; "common.pinned" = "Pinned"; "common.send_to" = "Надіслати до"; -"common_no_room_name" = "Немає назви кімнати"; -"common_poll_end_confirmation" = "Ви впевнені, що хочете закінчити це опитування?"; -"common_poll_summary" = "Опитування: %1$@"; -"common_something_went_wrong" = "Щось пішло не так"; -"common_unable_to_decrypt_no_access" = "Ви не маєте доступу до цього повідомлення"; -"common_verify_device" = "Перевірте пристрій"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "Ваша резервна копія чату наразі не синхронізована. Вам потрібно підтвердити ключ відновлення, щоб зберегти доступ до резервної копії чату."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "Підтвердіть ключ відновлення"; "crash_detection_dialog_content" = "%1$@ аварійно завершив роботу під час останнього використання. Бажаєте поділитися з нами звітом про збій?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Для того, щоб дозволити програмі використовувати камеру, надайте дозвіл у системних налаштуваннях."; "dialog_permission_generic" = "Будь ласка, надайте дозвіл в системних налаштуваннях."; "dialog_permission_location_description_ios" = "Надайте доступ в Налаштуваннях -> Місцезнаходження."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Тихі сповіщення"; "notification_incoming_call" = "Вхідний дзвінок"; "notification_inline_reply_failed" = "** Не вдалося надіслати - будь ласка, відкрийте кімнату"; -"notification_invitation_action_reject" = "Відхилити"; "notification_invite_body" = "Запросив (-ла) Вас до чату"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Згадав(-ла) вас: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Без відступу"; "rich_text_editor_url_placeholder" = "Посилання"; "rich_text_editor_a11y_add_attachment" = "Додати вкладення"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Користувацька URL-адреса Element Call"; "screen_advanced_settings_element_call_base_url_description" = "Встановіть URL-адресу для Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Неправильна URL-адреса, будь ласка, переконайтеся, що ви вказали протокол (http/https) та правильну адресу."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "Переглянути всі"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Змінити провайдера облікового запису"; "screen_account_provider_form_hint" = "Адреса домашнього сервера"; "screen_account_provider_form_notice" = "Уведіть пошуковий термін або адресу домену."; "screen_account_provider_form_subtitle" = "Пошук компанії, спільноти або приватного сервера."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Ви збираєтеся створити обліковий запис на %@"; "screen_advanced_settings_developer_mode" = "Режим розробника"; "screen_advanced_settings_developer_mode_description" = "Увімкніть доступ до функцій і можливостей для розробників."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Вимкніть редактор розширеного тексту, щоб вводити Markdown вручну."; "screen_advanced_settings_send_read_receipts" = "Читати журнали"; "screen_advanced_settings_send_read_receipts_description" = "Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Увімкнути резервне копіювання"; "screen_chat_backup_key_backup_description" = "Резервне копіювання гарантує, що ви не втратите історію повідомлень. %1$@."; "screen_chat_backup_key_backup_title" = "Резервне копіювання"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Змінити ключ відновлення"; -"screen_chat_backup_recovery_action_confirm" = "Підтвердити ключ відновлення"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Ваша резервна копія чату наразі не синхронізована."; -"screen_chat_backup_recovery_action_setup" = "Налаштувати відновлення"; "screen_chat_backup_recovery_action_setup_description" = "Отримайте доступ до своїх зашифрованих повідомлень, якщо ви втратите всі свої пристрої або вийшли з %1$@ системи."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Відкрийте %1$@ на комп'ютері"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Показувати результати тільки після закінчення опитування"; "screen_create_poll_anonymous_headline" = "Приховати голоси"; "screen_create_poll_answer_hint" = "Варіант %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Ваші зміни не будуть збережені"; "screen_create_poll_cancel_confirmation_title_ios" = "Скасувати опитування"; "screen_create_poll_question_desc" = "Питання або тема"; "screen_create_poll_question_hint" = "Про що йдеться в опитуванні?"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Групові чати"; "screen_notification_settings_invite_for_me_label" = "Запрошення"; "screen_notification_settings_mentions_only_disclaimer" = "Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."; -"screen_notification_settings_mentions_section_title" = "Згадки"; "screen_notification_settings_mode_all" = "Усі"; "screen_notification_settings_mode_mentions" = "Згадки"; "screen_notification_settings_notification_section_title" = "Повідомляти мене про"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Оберіть %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Підключити новий пристрій”"; "screen_qr_code_login_initial_state_item_4" = "Відскануйте QR-код цим пристроєм"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Відкрийте %1$@ на іншому пристрої, щоб отримати QR-код"; "screen_qr_code_login_invalid_scan_state_description" = "Використовуйте QR-код, показаний на іншому пристрої."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Неправильний QR-код"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "Ваш код підтвердження"; "screen_recovery_key_change_description" = "Отримайте новий ключ відновлення, якщо ви втратили існуючий ключ. Після зміни ключа відновлення ваш старий більше не буде працювати."; "screen_recovery_key_change_generate_key" = "Згенерувати новий ключ відновлення"; -"screen_recovery_key_change_generate_key_description" = "Переконайтеся, що ви можете зберігати ключ відновлення в безпечному місці"; "screen_recovery_key_change_success" = "Ключ відновлення змінено"; "screen_recovery_key_change_title" = "Змінити ключ відновлення?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Створити новий ключ відновлення"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "Ввести..."; "screen_recovery_key_confirm_lost_recovery_key" = "Загубили ключ відновлення?"; "screen_recovery_key_confirm_success" = "Ключ відновлення підтверджено"; -"screen_recovery_key_confirm_title" = "Підтвердіть ключ відновлення"; "screen_recovery_key_copied_to_clipboard" = "Скопійовано ключ відновлення"; "screen_recovery_key_generating_key" = "Створення…"; "screen_recovery_key_save_action" = "Зберегти ключ відновлення"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Так, скинути зараз"; "screen_reset_encryption_confirmation_alert_subtitle" = "Цей процес незворотний."; "screen_reset_encryption_confirmation_alert_title" = "Ви впевнені, що хочете скинути шифрування?"; -"screen_reset_encryption_password_placeholder" = "Ввести…"; "screen_reset_encryption_password_subtitle" = "Підтвердьте, що ви хочете скинути шифрування."; "screen_reset_encryption_password_title" = "Введіть пароль облікового запису, щоб продовжити"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Адміністратори автоматично мають права модератора"; "screen_room_change_role_moderators_title" = "Керувати модераторами"; "screen_room_change_role_unsaved_changes_description" = "У вас є не збережені зміни."; -"screen_room_change_role_unsaved_changes_title" = "Зберегти зміни?"; "screen_room_details_add_topic_title" = "Додати тему"; "screen_room_details_already_a_member" = "Уже учасник"; "screen_room_details_already_invited" = "Уже запрошені"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Не вдалося ввімкнути звук цієї кімнати. Повторіть спробу."; "screen_room_details_notification_mode_custom" = "Власні"; "screen_room_details_notification_mode_default" = "За замовчуванням"; -"screen_room_details_notification_title" = "Сповіщення"; "screen_room_details_share_room_title" = "Поділитися кімнатою"; "screen_room_details_title" = "Інформація про кімнату"; "screen_room_details_updating_room" = "Оновлення кімнати..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Розблокувати"; "screen_room_member_details_unblock_alert_description" = "Ви знову зможете бачити всі повідомлення від них."; "screen_room_member_details_unblock_user" = "Розблокувати користувача"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Заблокувати"; "screen_room_member_list_ban_member_confirmation_description" = "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять."; "screen_room_member_list_ban_member_confirmation_title" = "Ви точно хочете заблокувати цього користувача?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Блокування %1$@"; "screen_room_member_list_manage_member_ban" = "Вилучити й заблокувати учасника"; "screen_room_member_list_manage_member_remove" = "Вилучити з кімнати"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Видалити та заблокувати учасника"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Лише видалити учасника"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Видалити учасника та заборонити приєднання в майбутньому?"; "screen_room_member_list_manage_member_unban_action" = "Розблокувати"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Показувати менше"; "screen_room_timeline_message_copied" = "Повідомлення скопійовано"; "screen_room_timeline_no_permission_to_post" = "У Вас немає дозволу на публікацію в цій кімнаті"; -"screen_room_timeline_reactions_show_less" = "Показувати менше"; "screen_room_timeline_reactions_show_more" = "Показати більше"; "screen_room_timeline_read_marker_title" = "Нове"; "screen_room_title" = "Чат"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Позначити прочитаним"; "screen_roomlist_mark_as_unread" = "Позначити непрочитаним"; "screen_roomlist_room_directory_button_title" = "Переглянути всі кімнати"; -"screen_server_confirmation_change_server" = "Змінити провайдера облікового запису"; "screen_server_confirmation_message_login_element_dot_io" = "Приватний сервер для співробітників Element."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."; "screen_server_confirmation_message_register" = "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Порівняйте цифри"; "screen_session_verification_complete_subtitle" = "Ваш новий сеанс підтверджено. Він матиме доступ до ваших зашифрованих повідомлень, й інші користувачі вважатимуть його надійним."; "screen_session_verification_enter_recovery_key" = "Введіть ключ відновлення"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Доведіть, що це Ви, щоб отримати доступ до історії зашифрованих повідомлень."; "screen_session_verification_open_existing_session_title" = "Відкрийте існуючий сеанс"; "screen_session_verification_positive_button_canceled" = "Повторити перевірку"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Очікування збігу"; "screen_session_verification_ready_subtitle" = "Порівняйте унікальний набір емоджи."; "screen_session_verification_request_accepted_subtitle" = "Порівняйте унікальні емодзі, переконавшись, що вони відображаються в однаковому порядку."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Вони не збігаються"; "screen_session_verification_they_match" = "Вони збігаються"; "screen_session_verification_waiting_to_accept_subtitle" = "Щоб продовжити, прийміть запит на початок процесу перевірки в іншому сеансі."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."; "screen_signout_key_backup_disabled_title" = "Ви вимкнули резервне копіювання"; "screen_signout_key_backup_offline_subtitle" = "Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно підключіться, щоб зберегти резервну копію ключів перед виходом з системи."; -"screen_signout_key_backup_offline_title" = "Резервне копіювання ваших ключів ще триває"; "screen_signout_key_backup_ongoing_subtitle" = "Зачекайте, поки це завершиться, перш ніж вийти."; "screen_signout_key_backup_ongoing_title" = "Резервне копіювання ваших ключів ще триває"; "screen_signout_recovery_disabled_subtitle" = "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."; "screen_signout_recovery_disabled_title" = "Відновлення не налаштовано"; "screen_signout_save_recovery_key_subtitle" = "Ви збираєтеся вийти зі свого останнього сеансу. Якщо вийти зараз, ви можете втратити доступ до зашифрованих повідомлень."; -"screen_signout_save_recovery_key_title" = "Ви зберегли ключ відновлення?"; "screen_start_chat_error_starting_chat" = "Під час спроби почати чат сталася помилка"; "screen_view_location_title" = "Місцезнаходження"; "screen_welcome_bullet_1" = "Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."; @@ -919,7 +952,6 @@ "test_language_identifier" = "uk"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Усунення несправностей"; -"troubleshoot_notifications_entry_point_title" = "Усунення неполадок сповіщень"; "troubleshoot_notifications_screen_action" = "Запустити тести"; "troubleshoot_notifications_screen_action_again" = "Запустити тести знову"; "troubleshoot_notifications_screen_failure" = "Деякі тести не пройшли. Будь ласка, перегляньте деталі."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Переконується, що дистриб'ютори UnifiedPush доступні."; "troubleshoot_notifications_test_unified_push_failure" = "Дистриб'юторів не знайдено."; "troubleshoot_notifications_test_unified_push_title" = "Перевірка UnifiedPush"; +"a11y_poll" = "Опитування"; +"banner_set_up_recovery_submit" = "Налаштувати відновлення"; "dialog_title_error" = "Помилка"; "dialog_title_success" = "Успіх"; "notification_fallback_content" = "Сповіщення"; "notification_invitation_action_join" = "Доєднатися"; +"notification_invitation_action_reject" = "Відхилити"; "notification_room_action_mark_as_read" = "Позначити прочитаним"; "notification_room_action_quick_reply" = "Швидка відповідь"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Усі"; +"screen_account_provider_change" = "Змінити провайдера облікового запису"; "screen_account_provider_signin_subtitle" = "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."; "screen_account_provider_signup_subtitle" = "Тут будуть зберігатися Ваші розмови - так само, як Ви використовуєте поштову скриньку для зберігання своїх електронних листів."; "screen_analytics_settings_help_us_improve" = "Ділитися анонімними даними про використання, щоб допомогати нам виявляти проблеми."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Ви знову зможете бачити всі повідомлення від них."; "screen_blocked_users_unblock_alert_title" = "Розблокувати користувача"; "screen_bug_report_rash_logs_alert_title" = "%1$@ аварійно завершив роботу під час останнього використання. Бажаєте поділитися з нами звітом про збій?"; +"screen_chat_backup_recovery_action_confirm" = "Введіть ключ відновлення"; +"screen_chat_backup_recovery_action_setup" = "Налаштувати відновлення"; +"screen_create_poll_cancel_confirmation_content_ios" = "Внесені зміни не буде збережено"; "screen_create_room_add_people_title" = "Запросити людей"; "screen_create_room_room_name_label" = "Назва кімнати"; "screen_create_room_title" = "Створити кімнату"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "Редагувати опитування"; "screen_identity_use_another_device" = "Use another device"; "screen_login_subtitle" = "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації."; +"screen_notification_settings_mentions_section_title" = "Згадки"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Спробуйте ще раз"; +"screen_recovery_key_change_generate_key_description" = "Переконайтеся, що ви можете зберігати ключ відновлення в безпечному місці"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Заблокувати користувача"; +"screen_reset_encryption_password_placeholder" = "Ввести..."; "screen_room_attachment_source_camera_photo" = "Зробити фото"; "screen_room_change_permissions_everyone" = "Усі"; "screen_room_change_permissions_member_moderation" = "Модерація учасників"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Адміністратори"; "screen_room_change_role_section_moderators" = "Модератори"; "screen_room_change_role_section_users" = "Учасники"; +"screen_room_change_role_unsaved_changes_title" = "Зберегти зміни?"; "screen_room_details_invite_people_title" = "Запросити людей"; "screen_room_details_leave_conversation_title" = "Залишити розмову"; "screen_room_details_leave_room_title" = "Вийти з кімнати"; +"screen_room_details_notification_title" = "Сповіщення"; "screen_room_details_roles_and_permissions" = "Ролі та дозволи"; "screen_room_details_room_name_label" = "Назва кімнати"; "screen_room_details_security_title" = "Безпека"; "screen_room_details_topic_title" = "Тема"; "screen_room_error_failed_processing_media" = "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Вилучити й заблокувати учасника"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Тільки згадки та ключові слова"; +"screen_room_timeline_reactions_show_less" = "Показувати менше"; "screen_roomlist_filter_people" = "Люди"; +"screen_server_confirmation_change_server" = "Змінити провайдера облікового запису"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Вийти"; "screen_signout_confirmation_dialog_title" = "Вийти"; +"screen_signout_key_backup_offline_title" = "Резервне копіювання ваших ключів ще триває"; "screen_signout_preference_item" = "Вийти"; +"screen_signout_save_recovery_key_title" = "Ви зберегли ключ відновлення?"; +"troubleshoot_notifications_entry_point_title" = "Усунення неполадок сповіщень"; diff --git a/ElementX/Resources/Localizations/uz.lproj/Localizable.strings b/ElementX/Resources/Localizations/uz.lproj/Localizable.strings index 7a9d54d95a..a6242a13c4 100644 --- a/ElementX/Resources/Localizations/uz.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/uz.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "Pauza"; "a11y_pin_field" = "PIN field"; "a11y_play" = "O'ynang"; -"a11y_poll" = "So'ro'vnoma"; "a11y_poll_end" = "So‘rovnoma yakunlandi"; "a11y_react_with" = "React with %1$@"; "a11y_react_with_other_emojis" = "React with other emojis"; @@ -41,6 +40,7 @@ "action_create" = "Yaratmoq"; "action_create_a_room" = "Xonani yaratish"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "Rad etish"; "action_delete_poll" = "Delete Poll"; "action_disable" = "Oʻchirish"; @@ -54,6 +54,7 @@ "action_forgot_password" = "Parolni unutdingizmi?"; "action_forward" = "Oldinga"; "action_go_back" = "Go back"; +"action_ignore" = "Ignore"; "action_invite" = "Taklif qilish"; "action_invite_friends" = "Odamlarni taklif qiling"; "action_invite_friends_to_app" = "Odamlarni taklif qilish%1$@"; @@ -64,6 +65,7 @@ "action_leave" = "Tark etish"; "action_leave_conversation" = "Leave conversation"; "action_leave_room" = "Xonani tark etish"; +"action_load_more" = "Load more"; "action_manage_account" = "Hisobni boshqarish"; "action_manage_devices" = "Qurilmalarni boshqarish"; "action_message" = "Message"; @@ -93,6 +95,7 @@ "action_send_message" = "Xabar yuborish"; "action_share" = "Ulashish"; "action_share_link" = "Havolani ulashing"; +"action_show" = "Show"; "action_sign_in_again" = "Qaytadan kiring"; "action_signout" = "Tizimdan chiqish"; "action_signout_anyway" = "Baribir tizimdan chiqing"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "Manbani korish"; "action_yes" = "Ha"; -"action.load_more" = "Load more"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Haqida"; "common_acceptable_use_policy" = "Qabul qilinadigan foydalanish siyosati"; "common_advanced_settings" = "Kengaytirilgan sozlamalar"; @@ -133,10 +134,12 @@ "common_dark" = "Dark"; "common_decryption_error" = "Shifrni ochish xatosi"; "common_developer_options" = "Dasturchi variantlari"; +"common_device_id" = "Device ID"; "common_direct_chat" = "Direct chat"; "common_edited_suffix" = "(tahrirlangan)"; "common_editing" = "Tahrirlash"; "common_emote" = "*%1$@%2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Shifrlash yoqilgan"; "common_enter_your_pin" = "Enter your PIN"; "common_error" = "Xato"; @@ -147,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "Fayl"; "common_forward_message" = "Xabarni yo'naltirish"; +"common_frequently_used" = "Frequently used"; "common_gif" = ""; "common_image" = "Surat"; "common_in_reply_to" = "%1$@ga Javob bering"; @@ -162,6 +166,7 @@ "common_modern" = "Zamonaviy"; "common_mute" = "Ovozsiz qilish"; "common_no_results" = "Natijalar yoʻq"; +"common_no_room_name" = "No room name"; "common_offline" = "Oflayn"; "common_optic_id_ios" = "Optic ID"; "common_or" = "or"; @@ -170,6 +175,8 @@ "common_permalink" = "Doimiy havola"; "common_permission" = "Ruxsat"; "common_please_wait" = "Please wait…"; +"common_poll_end_confirmation" = "Haqiqatan ham bu soʻrovnomani tugatmoqchimisiz?"; +"common_poll_summary" = "So‘rov:%1$@"; "common_poll_total_votes" = "Jami ovozlar:%1$@"; "common_poll_undisclosed_text" = "Natijalar soʻrovnoma tugagandan soʻng koʻrsatiladi"; "common_privacy_policy" = "Maxfiylik siyosati"; @@ -200,6 +207,7 @@ "common_settings" = "Sozlamalar"; "common_shared_location" = "Joylashuvi ulashildi"; "common_signing_out" = "Signing out"; +"common_something_went_wrong" = "Something went wrong"; "common_starting_chat" = "Chat boshlanmoqda…"; "common_sticker" = "Stiker"; "common_success" = "Muvaffaqiyat"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "Bu xona nima haqida?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "Shifrni ochish imkonsiz"; +"common_unable_to_decrypt_no_access" = "You don't have access to this message"; "common_unable_to_invite_message" = "Takliflarni bir yoki bir nechta foydalanuvchiga yuborib bo‘lmadi."; "common_unable_to_invite_title" = "Taklif(lar)ni yuborib bo‘lmadi"; "common_unlock" = "Unlock"; @@ -221,23 +230,30 @@ "common_username" = "Foydalanuvchi nomi"; "common_verification_cancelled" = "Tasdiqlash bekor qilindi"; "common_verification_complete" = "Tasdiqlash yakunlandi"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "Verify device"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Ovozli xabar"; "common_waiting" = "Kutilmoqda…"; "common_waiting_for_decryption_key" = "Waiting for this message"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "Send to"; -"common_no_room_name" = "No room name"; -"common_poll_end_confirmation" = "Haqiqatan ham bu soʻrovnomani tugatmoqchimisiz?"; -"common_poll_summary" = "So‘rov:%1$@"; -"common_something_went_wrong" = "Something went wrong"; -"common_unable_to_decrypt_no_access" = "You don't have access to this message"; -"common_verify_device" = "Verify device"; -"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; -"confirm_recovery_key_banner_title" = "Enter your recovery key"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; +"confirm_recovery_key_banner_title" = "Your key storage is out of sync"; "crash_detection_dialog_content" = "%1$@oxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko'rmoqchimisiz?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "Ilovaga kameradan foydalanishiga ruxsat berish uchun tizim sozlamalarida ruxsat bering."; "dialog_permission_generic" = "Iltimos, tizim sozlamalarida ruxsat bering."; "dialog_permission_location_description_ios" = "Grant access in Settings -> Location."; @@ -290,7 +306,6 @@ "notification_channel_silent" = "Ovozsiz bildirishnomalar"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** Yuborilmadi - iltimos, xonani oching"; -"notification_invitation_action_reject" = "Rad etish"; "notification_invite_body" = "Sizni suhbatga taklif qildi"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "Mentioned you: %1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "Paragrafni bekor qilish"; "rich_text_editor_url_placeholder" = "Havola"; "rich_text_editor_a11y_add_attachment" = "Biriktirma qo'shing"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Maxsus element qo‘ng‘iroqlar bazasi URL manzili"; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "Hisob provayderini o'zgartiring"; "screen_account_provider_form_hint" = "Uy server manzili"; "screen_account_provider_form_notice" = "Qidiruv so'zini yoki domen manzilini kiriting."; "screen_account_provider_form_subtitle" = "Kompaniya, jamoa yoki shaxsiy serverni qidiring."; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "Siz %@da hisob yaratmoqchisiz"; "screen_advanced_settings_developer_mode" = "Dasturchi rejimi"; "screen_advanced_settings_developer_mode_description" = "Ishlab chiquvchilar uchun xususiyatlar va funksiyalarga kirishni yoqing."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Boy matn muharriri o'chiring Markdown bilan qo'lda yozish uchun"; "screen_advanced_settings_send_read_receipts" = "Read receipts"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "Zaxiralashni yoqing"; "screen_chat_backup_key_backup_description" = "Zaxiralash xabarlar tarixini yo'qotmaslikni ta'minlaydi.%1$@."; "screen_chat_backup_key_backup_title" = "Zaxira"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Qayta tiklash kalitini o'zgartiring"; -"screen_chat_backup_recovery_action_confirm" = "Qayta tiklash kalitini kiriting"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Sizning chat zaxirangiz hozirda sinxronlashtirilmagan."; -"screen_chat_backup_recovery_action_setup" = "Qayta tiklashni sozlang"; "screen_chat_backup_recovery_action_setup_description" = "Agar barcha qurilmalaringizni yo‘qotib qo‘ysangiz yoki tizimdan chiqqan bo‘lsangiz, shifrlangan xabarlaringizga ruxsat oling%1$@ hamma joyda."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "Natijalarni faqat soʻrov tugagandan keyin koʻrsatish"; "screen_create_poll_anonymous_headline" = "Ovozlarni yashirish"; "screen_create_poll_answer_hint" = "Variant%1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "Oʻzgartirishlaringiz saqlanmaydi"; "screen_create_poll_cancel_confirmation_title_ios" = "So‘rovni bekor qilish"; "screen_create_poll_question_desc" = "Savol yoki mavzu"; "screen_create_poll_question_hint" = "So'rovnoma nima haqida?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "Profil yangilanmoqda…"; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "Takliflar yo'q"; "screen_invites_invited_you" = "%1$@(%2$@ ) sizni taklif qildi"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "Guruh suhbatlari"; "screen_notification_settings_invite_for_me_label" = "Invitations"; "screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; -"screen_notification_settings_mentions_section_title" = "Eslatmalar"; "screen_notification_settings_mode_all" = "Hammasi"; "screen_notification_settings_mode_mentions" = "Eslatmalar"; "screen_notification_settings_notification_section_title" = "Menga xabar bering"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,18 +638,16 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Mavjud kalitingizni yo'qotgan bo'lsangiz, yangi tiklash kalitini oling. Qayta tiklash kalitini almashtirganingizdan so'ng, eski kalitingiz ishlamaydi."; "screen_recovery_key_change_generate_key" = "Yangi tiklash kalitini yarating"; -"screen_recovery_key_change_generate_key_description" = "Qayta tiklash kalitingizni xavfsiz joyda saqlashingiz mumkinligiga ishonch hosil qiling"; "screen_recovery_key_change_success" = "Qayta tiklash kaliti oʻzgartirildi"; "screen_recovery_key_change_title" = "Qayta tiklash kaliti almashtirilsinmi?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Hech kim bu ekranni kora olmasligiga ishonch hosil qiling!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Incorrect recovery key"; "screen_recovery_key_confirm_key_description" = "Agar sizda xavfsizlik kaliti yoki xavfsizlik iborasi bolsa, bu ham ishlaydi."; "screen_recovery_key_confirm_key_placeholder" = "Kirish…"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Qayta tiklash kaliti tasdiqlandi"; -"screen_recovery_key_confirm_title" = "Qayta tiklash kalitingizni kiriting"; "screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; "screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_save_action" = "Qayta tiklash kalitini saqlang"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "Edit Moderators"; "screen_room_change_role_unsaved_changes_description" = "You have unsaved changes."; -"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_add_topic_title" = "Mavzu qo'shish"; "screen_room_details_already_a_member" = "Allaqachon a'zo"; "screen_room_details_already_invited" = "Allaqachon taklif qilingan"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "Bu xonaning ovozi yoqilmadi, qayta urinib ko‘ring."; "screen_room_details_notification_mode_custom" = "Maxsus"; "screen_room_details_notification_mode_default" = "Standart"; -"screen_room_details_notification_title" = "Bildirishnomalar"; "screen_room_details_share_room_title" = "Xonani baham ko'ring"; "screen_room_details_title" = "Room info"; "screen_room_details_updating_room" = "Xona yangilanmoqda…"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "Blokdan chiqarish"; "screen_room_member_details_unblock_alert_description" = "Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."; "screen_room_member_details_unblock_user" = "Foydalanuvchini blokdan chiqarish"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "Ban"; "screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; "screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "Banning %1$@"; "screen_room_member_list_manage_member_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove" = "Remove from room"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; "screen_room_member_list_manage_member_unban_action" = "Unban"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Kamroq ko'rsatish"; "screen_room_timeline_message_copied" = "Xabar nusxalandi"; "screen_room_timeline_no_permission_to_post" = "Sizda bu xonaga post yozishga ruxsat yo‘q"; -"screen_room_timeline_reactions_show_less" = "Kamroq ko'rsatish"; "screen_room_timeline_reactions_show_more" = "Ko'proq ko'rsatish"; "screen_room_timeline_read_marker_title" = "Yangi"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "Mark as read"; "screen_roomlist_mark_as_unread" = "Mark as unread"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "Hisob provayderini o'zgartiring"; "screen_server_confirmation_message_login_element_dot_io" = "Element xodimlari uchun shaxsiy server."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."; "screen_server_confirmation_message_register" = "Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Compare numbers"; "screen_session_verification_complete_subtitle" = "Yangi seansingiz tasdiqlandi. U sizning shifrlangan xabarlaringizga kirish huquqiga ega va boshqa foydalanuvchilar uni ishonchli deb bilishadi."; "screen_session_verification_enter_recovery_key" = "Enter recovery key"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "Shifrlangan xabarlar tarixiga kirish uchun shaxsingizni tasdiqlang."; "screen_session_verification_open_existing_session_title" = "Mavjud seansni oching"; "screen_session_verification_positive_button_canceled" = "Tasdiqlashni qaytadan urining"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "Mos kelishi kutilmoqda"; "screen_session_verification_ready_subtitle" = "Compare a unique set of emojis."; "screen_session_verification_request_accepted_subtitle" = "Noyob emojilarni solishtiring, ular bir xil tartibda paydo bo'lishiga ishonch hosil qiling."; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Ular mos kelmaydi"; "screen_session_verification_they_match" = "Ular mos keladi"; "screen_session_verification_waiting_to_accept_subtitle" = "Davom etish uchun boshqa seansda tekshirish jarayonini boshlash soʻrovini qabul qiling."; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; "screen_signout_key_backup_disabled_title" = "You have turned off backup"; "screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; -"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; "screen_signout_key_backup_ongoing_title" = "Your keys are still being backed up"; "screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; "screen_signout_recovery_disabled_title" = "Recovery not set up"; "screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; -"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "Suhbatni boshlashda xatolik yuz berdi"; "screen_view_location_title" = "Joylashuv"; "screen_welcome_bullet_1" = "Qo'ng'iroqlar, so'ro'vlar, qidiruv va boshqalar shu yil oxirida qo'shiladi."; @@ -919,7 +952,6 @@ "test_language_identifier" = "en"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "So'ro'vnoma"; +"banner_set_up_recovery_submit" = "Qayta tiklashni sozlang"; "dialog_title_error" = "Xato"; "dialog_title_success" = "Muvaffaqiyat"; "notification_fallback_content" = "Bildirishnoma"; "notification_invitation_action_join" = "Qo'shilish"; +"notification_invitation_action_reject" = "Reject"; "notification_room_action_mark_as_read" = "Mark as read"; "notification_room_action_quick_reply" = "Tez javob"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "Har kim"; +"screen_account_provider_change" = "Hisob provayderini o'zgartiring"; "screen_account_provider_signin_subtitle" = "Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."; "screen_account_provider_signup_subtitle" = "Bu sizning suhbatlaringiz yashaydigan joy - xuddi siz elektron pochta xabarlaringizni saqlash uchun elektron pochta provayderidan foydalanganingiz kabi."; "screen_analytics_settings_help_us_improve" = "Muammolarni aniqlashda yordam berish uchun anonim foydalanish maʼlumotlarini baham koʻring."; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."; "screen_blocked_users_unblock_alert_title" = "Foydalanuvchini blokdan chiqarish"; "screen_bug_report_rash_logs_alert_title" = "%1$@oxirgi marta ishlatilganda qulab tushdi. Biz bilan nosozlik hisobotini baham ko'rmoqchimisiz?"; +"screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; +"screen_chat_backup_recovery_action_setup" = "Qayta tiklashni sozlang"; +"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "Odamlarni taklif qiling"; "screen_create_room_room_name_label" = "Xona nomi"; "screen_create_room_title" = "Xonani yaratish"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "So‘rovnomani tahrirlash"; "screen_identity_use_another_device" = "Use another device"; "screen_login_subtitle" = "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir."; +"screen_notification_settings_mentions_section_title" = "Eslatmalar"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Try again"; +"screen_recovery_key_change_generate_key_description" = "Qayta tiklash kalitingizni xavfsiz joyda saqlashingiz mumkinligiga ishonch hosil qiling"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Foydalanuvchini bloklash"; +"screen_reset_encryption_password_placeholder" = "Kirish…"; "screen_room_attachment_source_camera_photo" = "Rasmga olmoq"; "screen_room_change_permissions_everyone" = "Har kim"; "screen_room_change_permissions_member_moderation" = "Member moderation"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "Admins"; "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Members"; +"screen_room_change_role_unsaved_changes_title" = "Save changes?"; "screen_room_details_invite_people_title" = "Odamlarni taklif qiling"; "screen_room_details_leave_conversation_title" = "Leave conversation"; "screen_room_details_leave_room_title" = "Xonani tark etish"; +"screen_room_details_notification_title" = "Bildirishnomalar"; "screen_room_details_roles_and_permissions" = "Roles and permissions"; "screen_room_details_room_name_label" = "Xona nomi"; "screen_room_details_security_title" = "Xavfsizlik"; "screen_room_details_topic_title" = "Mavzu"; "screen_room_error_failed_processing_media" = "Mediani yuklab bo‘lmadi, qayta urinib ko‘ring."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Faqat eslatmalar va kalit so'zlar"; +"screen_room_timeline_reactions_show_less" = "Kamroq ko'rsatish"; "screen_roomlist_filter_people" = "Odamlar"; +"screen_server_confirmation_change_server" = "Hisob provayderini o'zgartiring"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Tizimdan chiqish"; "screen_signout_confirmation_dialog_title" = "Tizimdan chiqish"; +"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_preference_item" = "Tizimdan chiqish"; +"screen_signout_save_recovery_key_title" = "Zaxira kalitingizni saqladingizmi?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings index a87e1e8a0b..82305720b9 100644 --- a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "暂停"; "a11y_pin_field" = "PIN 字段"; "a11y_play" = "播放"; -"a11y_poll" = "投票"; "a11y_poll_end" = "投票已结束"; "a11y_react_with" = "使用 %1$@ 回应"; "a11y_react_with_other_emojis" = "使用其他表情符号回应"; @@ -27,7 +26,7 @@ "action_back" = "返回"; "action_call" = "呼叫"; "action_cancel" = "取消"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "暂时取消"; "action_choose_photo" = "选择照片"; "action_clear" = "清除"; "action_close" = "关闭"; @@ -41,6 +40,7 @@ "action_create" = "创建"; "action_create_a_room" = "创建房间"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "拒绝"; "action_delete_poll" = "删除投票"; "action_disable" = "停用"; @@ -54,6 +54,7 @@ "action_forgot_password" = "忘记密码?"; "action_forward" = "转发"; "action_go_back" = "返回"; +"action_ignore" = "Ignore"; "action_invite" = "邀请"; "action_invite_friends" = "邀请朋友"; "action_invite_friends_to_app" = "邀请朋友加入 %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "离开"; "action_leave_conversation" = "离开聊天"; "action_leave_room" = "离开房间"; +"action_load_more" = "载入更多"; "action_manage_account" = "管理账户"; "action_manage_devices" = "管理设备"; "action_message" = "发送消息给"; @@ -93,6 +95,7 @@ "action_send_message" = "发送消息"; "action_share" = "分享"; "action_share_link" = "分享链接"; +"action_show" = "Show"; "action_sign_in_again" = "再次登录"; "action_signout" = "登出"; "action_signout_anyway" = "仍然登出"; @@ -105,17 +108,15 @@ "action_tap_for_options" = "点按查看选项"; "action_try_again" = "再试一次"; "action_unpin" = "取消置顶"; -"action_view_in_timeline" = "View in timeline"; +"action_view_in_timeline" = "在时间轴中查看"; "action_view_source" = "查看源码"; "action_yes" = "是"; -"action.load_more" = "载入更多"; -"action_deactivate_account" = "Deactivate account"; -"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; -"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; -"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; -"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_migrate_to_native_sliding_sync_action" = "登出并升级"; +"banner_migrate_to_native_sliding_sync_description" = "您的服务器现在支持更快的新协议。现在登出并重新登录以进行升级。现在这样做可以帮助您避免在以后删除旧协议时被强制登出。"; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "您的服务器不再支持旧协议。请登出并重新登录以继续使用此应用。"; +"banner_migrate_to_native_sliding_sync_title" = "有可用升级"; +"banner_set_up_recovery_content" = "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"; +"banner_set_up_recovery_title" = "设置恢复"; "common_about" = "关于"; "common_acceptable_use_policy" = "可接受的使用政策"; "common_advanced_settings" = "高级设置"; @@ -133,10 +134,12 @@ "common_dark" = "暗色"; "common_decryption_error" = "解密错误"; "common_developer_options" = "开发者选项"; +"common_device_id" = "Device ID"; "common_direct_chat" = "私聊"; "common_edited_suffix" = "(已编辑)"; "common_editing" = "编辑中"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "已启用加密"; "common_enter_your_pin" = "输入 PIN 码"; "common_error" = "错误"; @@ -147,6 +150,7 @@ "common_favourited" = "已收藏"; "common_file" = "文件"; "common_forward_message" = "转发消息"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "图片"; "common_in_reply_to" = "回复 %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "现代"; "common_mute" = "静音"; "common_no_results" = "没有结果"; +"common_no_room_name" = "无房间名"; "common_offline" = "离线"; "common_optic_id_ios" = "光学 ID"; "common_or" = "或"; @@ -170,6 +175,8 @@ "common_permalink" = "固定链接"; "common_permission" = "权限"; "common_please_wait" = "请稍候……"; +"common_poll_end_confirmation" = "确定要结束这个投票吗?"; +"common_poll_summary" = "投票:%1$@"; "common_poll_total_votes" = "总票数: %1$@"; "common_poll_undisclosed_text" = "结果将在投票结束后显示"; "common_privacy_policy" = "隐私政策"; @@ -200,6 +207,7 @@ "common_settings" = "设置"; "common_shared_location" = "共享位置"; "common_signing_out" = "正在登出"; +"common_something_went_wrong" = "发生了一些错误"; "common_starting_chat" = "开始聊天..."; "common_sticker" = "贴纸"; "common_success" = "成功"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "这个房间是关于什么的?"; "common_touch_id_ios" = "触控 ID"; "common_unable_to_decrypt" = "无法解密"; +"common_unable_to_decrypt_no_access" = "无权访问此消息"; "common_unable_to_invite_message" = "无法向一个或多个用户发送邀请。"; "common_unable_to_invite_title" = "无法发送邀请"; "common_unlock" = "解锁"; @@ -221,23 +230,30 @@ "common_username" = "用户名"; "common_verification_cancelled" = "验证已取消"; "common_verification_complete" = "验证完成"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "验证设备"; +"common_verify_identity" = "Verify identity"; "common_video" = "视频"; "common_voice_message" = "语音消息"; "common_waiting" = "等待..."; "common_waiting_for_decryption_key" = "正在等待解密密钥"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "不再显示此内容"; "common.open_source_licenses" = "开源许可证"; "common.pinned" = "Pinned"; "common.send_to" = "发送至"; -"common_no_room_name" = "无房间名"; -"common_poll_end_confirmation" = "确定要结束这个投票吗?"; -"common_poll_summary" = "投票:%1$@"; -"common_something_went_wrong" = "发生了一些错误"; -"common_unable_to_decrypt_no_access" = "无权访问此消息"; -"common_verify_device" = "验证设备"; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; "confirm_recovery_key_banner_message" = "聊天备份目前不同步,需要输入恢复密钥才能访问聊天备份。"; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "输入恢复密钥"; "crash_detection_dialog_content" = "%1$@ 上次使用时崩溃了。想和我们分享崩溃报告吗?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "为了让应用程序使用相机,请在系统设置中授予权限。"; "dialog_permission_generic" = "请在系统设置中授予权限。"; "dialog_permission_location_description_ios" = "在设置->位置中授予访问权限。"; @@ -290,7 +306,6 @@ "notification_channel_silent" = "静默通知"; "notification_incoming_call" = "来电"; "notification_inline_reply_failed" = "** 无法发送——请打开房间"; -"notification_invitation_action_reject" = "拒绝"; "notification_invite_body" = "邀请您聊天"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "提到了你:%1$@"; @@ -299,7 +314,7 @@ "notification_room_invite_body" = "邀请你加入房间"; "notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; "notification_sender_me" = "我"; -"notification_sender_mention_reply" = "%1$@ mentioned or replied"; +"notification_sender_mention_reply" = "%1$@提及或回复"; "notification_test_push_notification_content" = "您正在查看通知!点击我!"; "notification_ticker_text_dm" = "%1$@:%2$@"; "notification_ticker_text_group" = "%1$@: %2$@ %3$@"; @@ -329,19 +344,34 @@ "rich_text_editor_unindent" = "取消缩进"; "rich_text_editor_url_placeholder" = "链接"; "rich_text_editor_a11y_add_attachment" = "添加附件"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "自定义 Element Call URL"; "screen_advanced_settings_element_call_base_url_description" = "为 Element 通话设置根 URL。"; "screen_advanced_settings_element_call_base_url_validation_error" = "URL 无效,请确保包含协议(http/https)和正确的地址。"; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "按下消息并选择 “%1$@” 将其包含在此处。"; "screen_pinned_timeline_empty_state_headline" = "固定重要消息,以便轻松发现它们"; -"screen_pinned_timeline_screen_title_empty" = "置顶消息"; -"screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; +"screen_reset_encryption_password_error" = "发生未知错误。请检查您的帐户密码是否正确,然后重试。"; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; "screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; "screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; -"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified all devices"; +"screen_resolve_send_failure_unsigned_device_title" = "您的消息未发送,因为%1$@尚未验证所有设备"; "screen_resolve_send_failure_you_unsigned_device_subtitle" = "One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."; "screen_resolve_send_failure_you_unsigned_device_title" = "Your message was not sent because you have not verified one or more of your devices"; "screen_room_mentions_at_room_subtitle" = "通知整个房间"; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "正在加载消息..."; "screen_room_pinned_banner_view_all_button_title" = "查看全部"; "screen_room_details_pinned_events_row_title" = "置顶消息"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; -"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "消息未发送,因为%1$@尚未验证所有设备。"; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "更改账户提供者"; "screen_account_provider_form_hint" = "服务器地址"; "screen_account_provider_form_notice" = "输入搜索词或域名地址。"; "screen_account_provider_form_subtitle" = "搜索公司、社区或私人服务器。"; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "您即将在 %@ 上创建一个帐户"; "screen_advanced_settings_developer_mode" = "开发者模式"; "screen_advanced_settings_developer_mode_description" = "允许开发人员访问特性和功能。"; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "禁用富文本编辑器,手动输入 Markdown。"; "screen_advanced_settings_send_read_receipts" = "已读回执"; "screen_advanced_settings_send_read_receipts_description" = "如果关闭,您的已读回执将不会发送给别人。您仍能收到别人的已读回执。"; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "开启备份"; "screen_chat_backup_key_backup_description" = "备份可确保你不会丢失消息历史记录。%1$@。"; "screen_chat_backup_key_backup_title" = "备份"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "更改恢复密钥"; -"screen_chat_backup_recovery_action_confirm" = "输入恢复密钥"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "您的聊天备份当前不同步。"; -"screen_chat_backup_recovery_action_setup" = "设置恢复密钥"; "screen_chat_backup_recovery_action_setup_description" = "在丢失或从 %1$@ 登出所有设备的情况下访问加密消息。"; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "在桌面设备中打开 %1$@"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "仅在投票结束后显示结果"; "screen_create_poll_anonymous_headline" = "隐藏投票"; "screen_create_poll_answer_hint" = "选项 %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "您的更改不会保存"; "screen_create_poll_cancel_confirmation_title_ios" = "取消投票"; "screen_create_poll_question_desc" = "问题或话题"; "screen_create_poll_question_hint" = "投票的内容是什么?"; @@ -477,7 +510,7 @@ "screen_edit_profile_error_title" = "无法更新个人资料"; "screen_edit_profile_title" = "编辑个人资料"; "screen_edit_profile_updating_details" = "更新个人资料……"; -"screen_encryption_reset_action_continue_reset" = "Continue reset"; +"screen_encryption_reset_action_continue_reset" = "继续重置"; "screen_encryption_reset_bullet_1" = "您的账户信息、联系人、偏好设置和聊天列表将被保留"; "screen_encryption_reset_bullet_2" = "您将丢失现有的消息历史记录"; "screen_encryption_reset_bullet_3" = "您将需要再次验证所有您的现有设备和联系人"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "群聊"; "screen_notification_settings_invite_for_me_label" = "邀请"; "screen_notification_settings_mentions_only_disclaimer" = "您的服务器在加密房间中不支持此选项,因此在某些房间您可能无法收到通知。"; -"screen_notification_settings_mentions_section_title" = "提及"; "screen_notification_settings_mode_all" = "全部"; "screen_notification_settings_mode_mentions" = "提及"; "screen_notification_settings_notification_section_title" = "请通知我:"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "选择 %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "「连接新设备」"; "screen_qr_code_login_initial_state_item_4" = "使用此设备扫描二维码"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "在另一台设备上打开 %1$@ 以获取二维码"; "screen_qr_code_login_invalid_scan_state_description" = "使用其他设备上显示的二维码。"; "screen_qr_code_login_invalid_scan_state_subtitle" = "二维码错误"; @@ -605,7 +638,6 @@ "screen_qr_code_login_verify_code_title" = "您的验证码"; "screen_recovery_key_change_description" = "如果您丢失了现有的恢复密钥,请获取新的恢复密钥。更改恢复密钥后,您的旧密钥将不再起作用。"; "screen_recovery_key_change_generate_key" = "生成新的恢复密钥"; -"screen_recovery_key_change_generate_key_description" = "确保您可以将恢复密钥存储在安全的地方"; "screen_recovery_key_change_success" = "恢复密钥已更改"; "screen_recovery_key_change_title" = "更改恢复密钥?"; "screen_recovery_key_confirm_create_new_recovery_key" = "创建新的恢复密钥"; @@ -616,7 +648,6 @@ "screen_recovery_key_confirm_key_placeholder" = "输入……"; "screen_recovery_key_confirm_lost_recovery_key" = "丢失了恢复密钥?"; "screen_recovery_key_confirm_success" = "恢复密钥已确认"; -"screen_recovery_key_confirm_title" = "输入您的恢复密钥"; "screen_recovery_key_copied_to_clipboard" = "恢复密钥已复制"; "screen_recovery_key_generating_key" = "正在生成……"; "screen_recovery_key_save_action" = "保存恢复密钥"; @@ -636,11 +667,10 @@ "screen_reset_encryption_confirmation_alert_action" = "是的,立即重置"; "screen_reset_encryption_confirmation_alert_subtitle" = "此过程不可逆。"; "screen_reset_encryption_confirmation_alert_title" = "您确定要重置加密吗?"; -"screen_reset_encryption_password_placeholder" = "输入..."; "screen_reset_encryption_password_subtitle" = "确认您要重置加密。"; "screen_reset_encryption_password_title" = "输入您的账户密码以继续"; -"screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; -"screen_reset_identity_confirmation_title" = "Can't confirm? Go to your account to reset your identity."; +"screen_reset_identity_confirmation_subtitle" = "您将要转到您的%1$@帐户来重置您的身份信息。之后,您将被带回该应用。"; +"screen_reset_identity_confirmation_title" = "无法确认?请前往您的帐户重置您的身份。"; "screen_room_alias_resolver_resolve_alias_failure" = "无法解析房间别名。"; "screen_room_attachment_source_camera" = "相机"; "screen_room_attachment_source_camera_video" = "录制视频"; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "管理员自动拥有协管员权限"; "screen_room_change_role_moderators_title" = "编辑协管员"; "screen_room_change_role_unsaved_changes_description" = "您有未保存的更改。"; -"screen_room_change_role_unsaved_changes_title" = "保存更改?"; "screen_room_details_add_topic_title" = "添加主题"; "screen_room_details_already_a_member" = "已经是成员"; "screen_room_details_already_invited" = "已邀请"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "无法取消此房间的静音,请重试。"; "screen_room_details_notification_mode_custom" = "自定义"; "screen_room_details_notification_mode_default" = "默认"; -"screen_room_details_notification_title" = "通知"; "screen_room_details_share_room_title" = "分享房间"; "screen_room_details_title" = "聊天室信息"; "screen_room_details_updating_room" = "正在更新房间……"; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "解封"; "screen_room_member_details_unblock_alert_description" = "可以重新接收他们的消息。"; "screen_room_member_details_unblock_user" = "解封用户"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "封禁"; "screen_room_member_list_ban_member_confirmation_description" = "即使受到邀请,他们也无法再次加入房间。"; "screen_room_member_list_ban_member_confirmation_title" = "您确定要封禁该成员吗?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "封禁 %1$@"; "screen_room_member_list_manage_member_ban" = "移除并封禁成员"; "screen_room_member_list_manage_member_remove" = "从房间移除"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "移除并封禁成员"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "仅移除成员"; "screen_room_member_list_manage_member_remove_confirmation_title" = "删除成员并禁止重新加入?"; "screen_room_member_list_manage_member_unban_action" = "取消封禁"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "折叠"; "screen_room_timeline_message_copied" = "消息已复制"; "screen_room_timeline_no_permission_to_post" = "您无权在此房间发言"; -"screen_room_timeline_reactions_show_less" = "折叠"; "screen_room_timeline_reactions_show_more" = "展开"; "screen_room_timeline_read_marker_title" = "新消息"; "screen_room_title" = "聊天"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "标记为已读"; "screen_roomlist_mark_as_unread" = "标记为未读"; "screen_roomlist_room_directory_button_title" = "浏览所有房间"; -"screen_server_confirmation_change_server" = "更改账户提供者"; "screen_server_confirmation_message_login_element_dot_io" = "专为 Element 员工提供的私人服务器。"; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix 是一个用于安全、去中心化通信的开放网络。"; "screen_server_confirmation_message_register" = "这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "比较数字"; "screen_session_verification_complete_subtitle" = "新设备已经成功验证。现在新设备可以访问加密信息,其他用户也会信任这个设备。"; "screen_session_verification_enter_recovery_key" = "输入恢复密钥"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "证明自己的身份以访问加密历史消息。"; "screen_session_verification_open_existing_session_title" = "打开已有会话"; "screen_session_verification_positive_button_canceled" = "重试验证"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "等待比对"; "screen_session_verification_ready_subtitle" = "比较一组表情符号。"; "screen_session_verification_request_accepted_subtitle" = "比较表情符号,确保它们以相同顺序排列。"; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "不匹配"; "screen_session_verification_they_match" = "匹配"; "screen_session_verification_waiting_to_accept_subtitle" = "请在其他会话中接受验证请求。"; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"; "screen_signout_key_backup_disabled_title" = "您已关闭备份"; "screen_signout_key_backup_offline_subtitle" = "当你离线时,密钥仍在备份中。重新连接以便在登出之前备份密钥。"; -"screen_signout_key_backup_offline_title" = "您的密钥仍在备份中"; "screen_signout_key_backup_ongoing_subtitle" = "请等待此操作完成后再登出。"; "screen_signout_key_backup_ongoing_title" = "您的密钥仍在备份中"; "screen_signout_recovery_disabled_subtitle" = "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"; "screen_signout_recovery_disabled_title" = "未设置恢复"; "screen_signout_save_recovery_key_subtitle" = "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。"; -"screen_signout_save_recovery_key_title" = "您保存了恢复密钥吗?"; "screen_start_chat_error_starting_chat" = "在开始聊天时发生了错误"; "screen_view_location_title" = "位置"; "screen_welcome_bullet_1" = "今年晚些时候将增加通话、投票、搜索等功能。"; @@ -919,7 +952,6 @@ "test_language_identifier" = "zh-Hans"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "排查问题"; -"troubleshoot_notifications_entry_point_title" = "排查通知问题"; "troubleshoot_notifications_screen_action" = "运行测试"; "troubleshoot_notifications_screen_action_again" = "再次运行测试"; "troubleshoot_notifications_screen_failure" = "一些测试失败了。请查看详情。"; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "确保 UnifiedPush distributor 可用。"; "troubleshoot_notifications_test_unified_push_failure" = "未找到推送 distributor。"; "troubleshoot_notifications_test_unified_push_title" = "检查 UnifiedPush"; +"a11y_poll" = "投票"; +"banner_set_up_recovery_submit" = "设置恢复"; "dialog_title_error" = "错误"; "dialog_title_success" = "成功"; "notification_fallback_content" = "通知"; "notification_invitation_action_join" = "加入"; +"notification_invitation_action_reject" = "拒绝"; "notification_room_action_mark_as_read" = "标记为已读"; "notification_room_action_quick_reply" = "快速回复"; +"screen_pinned_timeline_screen_title_empty" = "置顶消息"; "screen_room_mentions_at_room_title" = "所有人"; +"screen_account_provider_change" = "更改账户提供者"; "screen_account_provider_signin_subtitle" = "这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"; "screen_account_provider_signup_subtitle" = "这是您的对话将进行的地方,就像您使用电子邮件提供商来保存电子邮件一样。"; "screen_analytics_settings_help_us_improve" = "共享匿名使用数据以帮助我们排查问题。"; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "可以重新接收他们的消息。"; "screen_blocked_users_unblock_alert_title" = "解封用户"; "screen_bug_report_rash_logs_alert_title" = "%1$@ 上次使用时崩溃了。想和我们分享崩溃报告吗?"; +"screen_chat_backup_recovery_action_confirm" = "输入恢复密钥"; +"screen_chat_backup_recovery_action_setup" = "设置恢复"; +"screen_create_poll_cancel_confirmation_content_ios" = "更改不会保存"; "screen_create_room_add_people_title" = "邀请朋友"; "screen_create_room_room_name_label" = "房间名称"; "screen_create_room_title" = "创建房间"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "编辑投票"; "screen_identity_use_another_device" = "使用其他设备"; "screen_login_subtitle" = "Matrix 是一个用于安全、去中心化通信的开放网络。"; +"screen_notification_settings_mentions_section_title" = "提及"; "screen_qr_code_login_invalid_scan_state_retry_button" = "再试一次"; +"screen_recovery_key_change_generate_key_description" = "确保将恢复密钥存储在安全的地方"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "封禁用户"; +"screen_reset_encryption_password_placeholder" = "输入……"; "screen_room_attachment_source_camera_photo" = "拍摄照片"; "screen_room_change_permissions_everyone" = "所有人"; "screen_room_change_permissions_member_moderation" = "成员权限"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "管理员"; "screen_room_change_role_section_moderators" = "协管员"; "screen_room_change_role_section_users" = "成员"; +"screen_room_change_role_unsaved_changes_title" = "保存更改?"; "screen_room_details_invite_people_title" = "邀请朋友"; "screen_room_details_leave_conversation_title" = "离开聊天"; "screen_room_details_leave_room_title" = "离开房间"; +"screen_room_details_notification_title" = "通知"; "screen_room_details_roles_and_permissions" = "角色与权限"; "screen_room_details_room_name_label" = "房间名称"; "screen_room_details_security_title" = "安全"; "screen_room_details_topic_title" = "话题"; "screen_room_error_failed_processing_media" = "处理要上传的媒体失败,请重试。"; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "移除并封禁成员"; "screen_room_notification_settings_mode_mentions_and_keywords" = "仅限提及和关键词"; +"screen_room_timeline_reactions_show_less" = "折叠"; "screen_roomlist_filter_people" = "用户"; +"screen_server_confirmation_change_server" = "更改账户提供者"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "登出"; "screen_signout_confirmation_dialog_title" = "登出"; +"screen_signout_key_backup_offline_title" = "您的密钥仍在备份中"; "screen_signout_preference_item" = "登出"; +"screen_signout_save_recovery_key_title" = "您保存了恢复密钥吗?"; +"troubleshoot_notifications_entry_point_title" = "排查通知问题"; diff --git a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings index 42aeacb27e..ebb04ae153 100644 --- a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings @@ -8,7 +8,6 @@ "a11y_pause" = "暫停"; "a11y_pin_field" = "PIN 碼欄位"; "a11y_play" = "播放"; -"a11y_poll" = "投票"; "a11y_poll_end" = "投票已結束"; "a11y_react_with" = "使用 %1$@ 回應"; "a11y_react_with_other_emojis" = "用其他表情符號回應"; @@ -41,6 +40,7 @@ "action_create" = "建立"; "action_create_a_room" = "建立聊天室"; "action_deactivate" = "Deactivate"; +"action_deactivate_account" = "Deactivate account"; "action_decline" = "拒絕"; "action_delete_poll" = "刪除投票"; "action_disable" = "停用"; @@ -54,6 +54,7 @@ "action_forgot_password" = "忘記密碼?"; "action_forward" = "轉寄"; "action_go_back" = "返回"; +"action_ignore" = "Ignore"; "action_invite" = "邀請"; "action_invite_friends" = "邀請夥伴"; "action_invite_friends_to_app" = "邀請朋友使用 %1$@"; @@ -64,6 +65,7 @@ "action_leave" = "離開"; "action_leave_conversation" = "離開對話"; "action_leave_room" = "離開聊天室"; +"action_load_more" = "載入更多"; "action_manage_account" = "管理帳號"; "action_manage_devices" = "管理裝置"; "action_message" = "聊天"; @@ -93,6 +95,7 @@ "action_send_message" = "傳送訊息"; "action_share" = "分享"; "action_share_link" = "分享連結"; +"action_show" = "Show"; "action_sign_in_again" = "再登入一次"; "action_signout" = "登出"; "action_signout_anyway" = "直接登出"; @@ -108,14 +111,12 @@ "action_view_in_timeline" = "View in timeline"; "action_view_source" = "檢視原始碼"; "action_yes" = "是"; -"action.load_more" = "載入更多"; -"action_deactivate_account" = "Deactivate account"; "banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "關於"; "common_acceptable_use_policy" = "可接受使用政策"; "common_advanced_settings" = "進階設定"; @@ -133,10 +134,12 @@ "common_dark" = "深色"; "common_decryption_error" = "解密錯誤"; "common_developer_options" = "開發者選項"; +"common_device_id" = "Device ID"; "common_direct_chat" = "私訊"; "common_edited_suffix" = "(已編輯)"; "common_editing" = "編輯中"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "已啟用加密"; "common_enter_your_pin" = "輸入您的 PIN 碼"; "common_error" = "錯誤"; @@ -147,6 +150,7 @@ "common_favourited" = "我的最愛"; "common_file" = "檔案"; "common_forward_message" = "轉寄訊息"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "圖片"; "common_in_reply_to" = "回覆 %1$@"; @@ -162,6 +166,7 @@ "common_modern" = "現代"; "common_mute" = "關閉通知"; "common_no_results" = "查無結果"; +"common_no_room_name" = "無聊天室名稱"; "common_offline" = "離線"; "common_optic_id_ios" = "Optic ID"; "common_or" = "或"; @@ -170,6 +175,8 @@ "common_permalink" = "永久連結"; "common_permission" = "權限"; "common_please_wait" = "請稍等..."; +"common_poll_end_confirmation" = "您確定要結束這項投票嗎?"; +"common_poll_summary" = "投票:%1$@"; "common_poll_total_votes" = "總票數:%1$@"; "common_poll_undisclosed_text" = "結果將在投票結束後公佈"; "common_privacy_policy" = "隱私權政策"; @@ -200,6 +207,7 @@ "common_settings" = "設定"; "common_shared_location" = "位置分享"; "common_signing_out" = "正在登出"; +"common_something_went_wrong" = "有錯誤發生"; "common_starting_chat" = "開始聊天..."; "common_sticker" = "貼圖"; "common_success" = "成功"; @@ -213,6 +221,7 @@ "common_topic_placeholder" = "What is this room about?"; "common_touch_id_ios" = "Touch ID"; "common_unable_to_decrypt" = "無法解密"; +"common_unable_to_decrypt_no_access" = "您無法存取此則訊息"; "common_unable_to_invite_message" = "無法發送邀請給一或多個使用者。"; "common_unable_to_invite_title" = "無法發送邀請"; "common_unlock" = "解鎖"; @@ -221,23 +230,30 @@ "common_username" = "使用者名稱"; "common_verification_cancelled" = "驗證已取消"; "common_verification_complete" = "驗證完成"; +"common_verification_failed" = "Verification failed"; +"common_verified" = "Verified"; +"common_verify_device" = "驗證裝置"; +"common_verify_identity" = "Verify identity"; "common_video" = "影片"; "common_voice_message" = "語音訊息"; "common_waiting" = "等待中..."; "common_waiting_for_decryption_key" = "等待此則訊息"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "不再顯示"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; "common.send_to" = "傳送給"; -"common_no_room_name" = "無聊天室名稱"; -"common_poll_end_confirmation" = "您確定要結束這項投票嗎?"; -"common_poll_summary" = "投票:%1$@"; -"common_something_went_wrong" = "有錯誤發生"; -"common_unable_to_decrypt_no_access" = "您無法存取此則訊息"; -"common_verify_device" = "驗證裝置"; -"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; +"common.you" = "You"; +"common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; +"common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; "confirm_recovery_key_banner_title" = "輸入您的復原金鑰"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; "dialog_permission_camera" = "為了讓應用程式使用相機,請到系統設定中開啟權限。"; "dialog_permission_generic" = "請到系統設定中開啟權限。"; "dialog_permission_location_description_ios" = "在「設定」->「位置」中開啟權限。"; @@ -290,7 +306,6 @@ "notification_channel_silent" = "無聲通知"; "notification_incoming_call" = "Incoming call"; "notification_inline_reply_failed" = "** 無法傳送,請開啟聊天室"; -"notification_invitation_action_reject" = "拒絕"; "notification_invite_body" = "邀請您聊天"; "notification_invite_body_with_sender" = "%1$@ invited you to chat"; "notification_mentioned_you_body" = "提及您:%1$@"; @@ -329,12 +344,27 @@ "rich_text_editor_unindent" = "減少縮排"; "rich_text_editor_url_placeholder" = "連結"; "rich_text_editor_a11y_add_attachment" = "新增附件"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address."; +"screen_create_room_room_address_section_title" = "Room address"; +"screen_create_room_room_visibility_section_title" = "Room visibility"; +"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_access_section_header" = "Room Access"; +"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request"; +"screen_create_room_access_section_knocking_option_title" = "Ask to join"; +"screen_join_room_cancel_knock_action" = "Cancel request"; +"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel"; +"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?"; +"screen_join_room_cancel_knock_alert_title" = "Cancel request to join"; +"screen_join_room_knock_message_description" = "Message (optional)"; +"screen_join_room_knock_sent_description" = "You will receive an invite to join the room if your request is accepted."; +"screen_join_room_knock_sent_title" = "Request to join sent"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; "screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; "screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; @@ -350,10 +380,10 @@ "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_roomlist_knock_event_sent_description" = "Request to join sent"; "screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; "screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices."; "screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices."; -"screen_account_provider_change" = "更改帳號提供者"; "screen_account_provider_form_hint" = "家伺服器位址"; "screen_account_provider_form_notice" = "輸入關鍵字或網域名稱。"; "screen_account_provider_form_subtitle" = "搜尋公司、社群、私有伺服器。"; @@ -362,6 +392,8 @@ "screen_account_provider_signup_title" = "您即將在 %@ 建立帳號"; "screen_advanced_settings_developer_mode" = "開發者模式"; "screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "手動輸入 Markdown,停用格式化文字編輯器。"; "screen_advanced_settings_send_read_receipts" = "已讀回條"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -430,10 +462,12 @@ "screen_chat_backup_key_backup_action_enable" = "開啟備份功能"; "screen_chat_backup_key_backup_description" = "備份可確保您不會遺失歷史訊息。%1$@。"; "screen_chat_backup_key_backup_title" = "備份"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "變更復原金鑰"; -"screen_chat_backup_recovery_action_confirm" = "輸入復原金鑰"; -"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; -"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; +"screen_chat_backup_recovery_action_confirm_description" = "Your key storage is currently out of sync."; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -447,7 +481,6 @@ "screen_create_poll_anonymous_desc" = "只在投票結束後顯示結果"; "screen_create_poll_anonymous_headline" = "隱藏票數"; "screen_create_poll_answer_hint" = "選項 %1$d"; -"screen_create_poll_cancel_confirmation_content_ios" = "您的變更不會保留"; "screen_create_poll_cancel_confirmation_title_ios" = "捨棄投票"; "screen_create_poll_question_desc" = "問題或主題"; "screen_create_poll_question_hint" = "投什麼?"; @@ -479,7 +512,7 @@ "screen_edit_profile_updating_details" = "正在更新個人檔案..."; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose any message history that’s stored only on the server"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; @@ -499,7 +532,7 @@ "screen_invites_empty_list" = "沒有邀請"; "screen_invites_invited_you" = "%1$@(%2$@)邀請您"; "screen_join_room_join_action" = "Join room"; -"screen_join_room_knock_action" = "Knock to join"; +"screen_join_room_knock_action" = "Send request to join"; "screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web."; "screen_join_room_space_not_supported_title" = "Spaces are not supported yet"; "screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."; @@ -509,10 +542,10 @@ "screen_key_backup_disable_confirmation_action_turn_off" = "關閉"; "screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; "screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; -"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; -"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; -"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; -"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"; +"screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?"; "screen_login_error_deactivated_account" = "這個帳號已被停用。"; "screen_login_error_invalid_credentials" = "不正確的使用者名稱或密碼"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; @@ -544,7 +577,6 @@ "screen_notification_settings_group_chats" = "群組聊天"; "screen_notification_settings_invite_for_me_label" = "邀請"; "screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; -"screen_notification_settings_mentions_section_title" = "提及"; "screen_notification_settings_mode_all" = "All"; "screen_notification_settings_mode_mentions" = "提及"; "screen_notification_settings_notification_section_title" = "Notify me for"; @@ -591,6 +623,7 @@ "screen_qr_code_login_initial_state_item_3" = "Select %1$@"; "screen_qr_code_login_initial_state_item_3_action" = "“Link new device”"; "screen_qr_code_login_initial_state_item_4" = "Scan the QR code with this device"; +"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; "screen_qr_code_login_initial_state_title" = "Open %1$@ on another device to get the QR code"; "screen_qr_code_login_invalid_scan_state_description" = "Use the QR code shown on the other device."; "screen_qr_code_login_invalid_scan_state_subtitle" = "Wrong QR code"; @@ -605,29 +638,27 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_generate_key" = "Generate a new recovery key"; -"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; "screen_recovery_key_change_success" = "Recovery key changed"; "screen_recovery_key_change_title" = "Change recovery key?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Make sure nobody can see this screen!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Incorrect recovery key"; "screen_recovery_key_confirm_key_description" = "If you have a security key or security phrase, this will work too."; "screen_recovery_key_confirm_key_placeholder" = "Enter…"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Recovery key confirmed"; -"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; "screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_save_action" = "Save recovery key"; -"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_description" = "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe."; "screen_recovery_key_save_key_description" = "點擊以複製復原金鑰"; -"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key somewhere safe"; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; -"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_description" = "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."; "screen_recovery_key_setup_generate_key" = "Generate your recovery key"; -"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_title" = "Set up recovery"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; @@ -636,7 +667,6 @@ "screen_reset_encryption_confirmation_alert_action" = "Yes, reset now"; "screen_reset_encryption_confirmation_alert_subtitle" = "This process is irreversible."; "screen_reset_encryption_confirmation_alert_title" = "Are you sure you want to reset your identity?"; -"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_subtitle" = "Confirm that you want to reset your identity."; "screen_reset_encryption_password_title" = "Enter your account password to continue"; "screen_reset_identity_confirmation_subtitle" = "You're about to go to your %1$@ account to reset your identity. Afterwards you'll be taken back to the app."; @@ -669,7 +699,6 @@ "screen_room_change_role_moderators_admin_section_footer" = "Admins automatically have moderator privileges"; "screen_room_change_role_moderators_title" = "編輯版主"; "screen_room_change_role_unsaved_changes_description" = "您有尚未儲存的變更"; -"screen_room_change_role_unsaved_changes_title" = "是否儲存變更?"; "screen_room_details_add_topic_title" = "新增主題"; "screen_room_details_already_a_member" = "已是成員"; "screen_room_details_already_invited" = "已邀請"; @@ -686,7 +715,6 @@ "screen_room_details_error_unmuting" = "無法開啟聊天室通知,請再試一次。"; "screen_room_details_notification_mode_custom" = "自訂"; "screen_room_details_notification_mode_default" = "預設"; -"screen_room_details_notification_title" = "通知"; "screen_room_details_share_room_title" = "分享聊天室"; "screen_room_details_title" = "聊天室資訊"; "screen_room_details_updating_room" = "正在更新聊天室..."; @@ -704,6 +732,8 @@ "screen_room_member_details_unblock_alert_action" = "解除封鎖"; "screen_room_member_details_unblock_alert_description" = "您將無法看到任何來自他們的訊息。"; "screen_room_member_details_unblock_user" = "解除封鎖使用者"; +"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; +"screen_room_member_details_verify_button_title" = "Verify %1$@"; "screen_room_member_list_ban_member_confirmation_action" = "加入黑名單"; "screen_room_member_list_ban_member_confirmation_description" = "即使收到邀請,他們仍然無法加入聊天室。"; "screen_room_member_list_ban_member_confirmation_title" = "您確定要將此成員加入黑名單?"; @@ -711,7 +741,6 @@ "screen_room_member_list_banning_user" = "正在將 %1$@ 加入黑名單"; "screen_room_member_list_manage_member_ban" = "踢出並加入黑名單"; "screen_room_member_list_manage_member_remove" = "踢出聊天室"; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "踢出並加入黑名單"; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; "screen_room_member_list_manage_member_unban_action" = "解除黑名單"; @@ -761,7 +790,6 @@ "screen_room_timeline_less_reactions" = "較少"; "screen_room_timeline_message_copied" = "訊息已複製"; "screen_room_timeline_no_permission_to_post" = "您沒有權限在此聊天室傳送訊息"; -"screen_room_timeline_reactions_show_less" = "較少"; "screen_room_timeline_reactions_show_more" = "更多"; "screen_room_timeline_read_marker_title" = "新訊息"; "screen_room_title" = "Chat"; @@ -790,7 +818,6 @@ "screen_roomlist_mark_as_read" = "標為已讀"; "screen_roomlist_mark_as_unread" = "標為未讀"; "screen_roomlist_room_directory_button_title" = "Browse all rooms"; -"screen_server_confirmation_change_server" = "更改帳號提供者"; "screen_server_confirmation_message_login_element_dot_io" = "A private server for Element employees."; "screen_server_confirmation_message_login_matrix_dot_org" = "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"; "screen_server_confirmation_message_register" = "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"; @@ -803,6 +830,7 @@ "screen_session_verification_compare_numbers_title" = "Compare numbers"; "screen_session_verification_complete_subtitle" = "新的工作階段已完成驗證。它能夠存取您的加密訊息,而其他使用者會將它視為可信任的。"; "screen_session_verification_enter_recovery_key" = "Enter recovery key"; +"screen_session_verification_failed_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_open_existing_session_subtitle" = "為了存取被加密的歷史訊息,您需要證明這是您本人。"; "screen_session_verification_open_existing_session_title" = "開啟一個現存的工作階段"; "screen_session_verification_positive_button_canceled" = "重新嘗試驗證"; @@ -810,6 +838,13 @@ "screen_session_verification_positive_button_verifying_ongoing" = "等待比對"; "screen_session_verification_ready_subtitle" = "比對一組唯一的表情符號。"; "screen_session_verification_request_accepted_subtitle" = "表情符號是唯一的,請相互比對,確認它們的排列順序是否相同。"; +"screen_session_verification_request_details_timestamp" = "Signed in"; +"screen_session_verification_request_failure_title" = "Verification failed"; +"screen_session_verification_request_footer" = "Only continue if you initiated this verification."; +"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; +"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device."; +"screen_session_verification_request_success_title" = "Device verified"; +"screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "不一樣"; "screen_session_verification_they_match" = "一樣"; "screen_session_verification_waiting_to_accept_subtitle" = "準備開始驗證,請到您的其他工作階段接受請求。"; @@ -830,13 +865,11 @@ "screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; "screen_signout_key_backup_disabled_title" = "You have turned off backup"; "screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; -"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; "screen_signout_key_backup_ongoing_title" = "Your keys are still being backed up"; "screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; "screen_signout_recovery_disabled_title" = "Recovery not set up"; "screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; -"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_view_location_title" = "位置"; "screen_welcome_bullet_1" = "通話、投票、搜尋等更多功能將在今年登場。"; @@ -919,7 +952,6 @@ "test_language_identifier" = "zh-tw"; "test_untranslated_default_language_identifier" = "en"; "troubleshoot_notifications_entry_point_section" = "Troubleshoot"; -"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; "troubleshoot_notifications_screen_action" = "Run tests"; "troubleshoot_notifications_screen_action_again" = "Run tests again"; "troubleshoot_notifications_screen_failure" = "Some tests failed. Please check the details."; @@ -961,13 +993,18 @@ "troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available."; "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; +"a11y_poll" = "投票"; +"banner_set_up_recovery_submit" = "Set up recovery"; "dialog_title_error" = "錯誤"; "dialog_title_success" = "成功"; "notification_fallback_content" = "通知"; "notification_invitation_action_join" = "加入"; +"notification_invitation_action_reject" = "拒絕"; "notification_room_action_mark_as_read" = "標為已讀"; "notification_room_action_quick_reply" = "快速回覆"; +"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_room_mentions_at_room_title" = "所有人"; +"screen_account_provider_change" = "更改帳號提供者"; "screen_account_provider_signin_subtitle" = "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"; "screen_account_provider_signup_subtitle" = "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"; "screen_analytics_settings_help_us_improve" = "提供匿名的使用數據以協助我們釐清問題。"; @@ -977,6 +1014,9 @@ "screen_blocked_users_unblock_alert_description" = "您將無法看到任何來自他們的訊息。"; "screen_blocked_users_unblock_alert_title" = "解除封鎖使用者"; "screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_create_poll_cancel_confirmation_content_ios" = "您的變更不會儲存"; "screen_create_room_add_people_title" = "邀請夥伴"; "screen_create_room_room_name_label" = "聊天室名稱"; "screen_create_room_title" = "建立聊天室"; @@ -990,8 +1030,12 @@ "screen_edit_poll_title" = "編輯投票"; "screen_identity_use_another_device" = "使用另一部裝置"; "screen_login_subtitle" = "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。"; +"screen_notification_settings_mentions_section_title" = "提及"; "screen_qr_code_login_invalid_scan_state_retry_button" = "再試一次"; +"screen_recovery_key_change_generate_key_description" = "Do not share this with anyone!"; +"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "封鎖使用者"; +"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_room_attachment_source_camera_photo" = "拍照"; "screen_room_change_permissions_everyone" = "所有人"; "screen_room_change_permissions_member_moderation" = "成員管理"; @@ -1000,16 +1044,25 @@ "screen_room_change_role_section_administrators" = "管理員"; "screen_room_change_role_section_moderators" = "版主"; "screen_room_change_role_section_users" = "成員"; +"screen_room_change_role_unsaved_changes_title" = "是否儲存變更?"; "screen_room_details_invite_people_title" = "邀請夥伴"; "screen_room_details_leave_conversation_title" = "離開對話"; "screen_room_details_leave_room_title" = "離開聊天室"; +"screen_room_details_notification_title" = "通知"; "screen_room_details_roles_and_permissions" = "身份與權限"; "screen_room_details_room_name_label" = "聊天室名稱"; "screen_room_details_security_title" = "安全性"; "screen_room_details_topic_title" = "主題"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "踢出並加入黑名單"; "screen_room_notification_settings_mode_mentions_and_keywords" = "僅限提及與關鍵字"; +"screen_room_timeline_reactions_show_less" = "較少"; "screen_roomlist_filter_people" = "夥伴"; +"screen_server_confirmation_change_server" = "更改帳號提供者"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "登出"; "screen_signout_confirmation_dialog_title" = "登出"; +"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; "screen_signout_preference_item" = "登出"; +"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; +"troubleshoot_notifications_entry_point_title" = "Troubleshoot notifications"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index c5b70a1100..e220fce2d3 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -26,8 +26,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg /// Common background task to continue long-running tasks in the background. private var backgroundTask: UIBackgroundTaskIdentifier? - - private var isSuspended = false private var userSession: UserSessionProtocol? { didSet { @@ -70,7 +68,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg let appSettings = appHooks.appSettingsHook.configure(AppSettings()) - MXLog.configure(logLevel: appSettings.logLevel) + MXLog.configure(currentTarget: "elementx", filePrefix: nil, logLevel: appSettings.logLevel) let appName = InfoPlistReader.main.bundleDisplayName let appVersion = InfoPlistReader.main.bundleShortVersionString @@ -150,6 +148,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg switch action { case .startCall(let roomID): self?.handleAppRoute(.call(roomID: roomID)) + case .receivedIncomingCallRequest: + // When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload` + // the UIApplication states don't change and syncing is neither started nor ran on + // a background task. Handle both manually here. + self?.startSync() + self?.scheduleDelayedSyncStop() default: break } @@ -195,12 +199,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg if let route = appRouteURLParser.route(from: url) { switch route { - case .oidcCallback(let url): - if stateMachine.state == .softLogout { - softLogoutCoordinator?.handleOIDCRedirectURL(url) - } else { - authenticationFlowCoordinator?.handleOIDCRedirectURL(url) - } case .genericCallLink(let url): if let userSessionFlowCoordinator { userSessionFlowCoordinator.handleAppRoute(route, animated: true) @@ -334,6 +332,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg switch await roomProxy.timeline.sendMessage(replyText, html: nil, + inReplyToEventID: nil, intentionalMentions: .empty) { case .success: break @@ -484,7 +483,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg encryptionKeyProvider: EncryptionKeyProvider(), appSettings: appSettings, appHooks: appHooks) - _ = await authenticationService.configure(for: userSession.clientProxy.homeserver) + _ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login) let parameters = SoftLogoutScreenCoordinatorParameters(authenticationService: authenticationService, credentials: credentials, @@ -573,7 +572,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg // The user will log out, clear any existing notifications and unregister from receving new ones UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removeAllDeliveredNotifications() - UIApplication.shared.applicationIconBadgeNumber = 0 + UNUserNotificationCenter.current().setBadgeCount(0) unregisterForRemoteNotifications() @@ -918,6 +917,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg private func applicationWillResignActive() { MXLog.info("Application will resign active") + scheduleDelayedSyncStop() + scheduleBackgroundAppRefresh() + } + + private func scheduleDelayedSyncStop() { guard backgroundTask == nil else { return } @@ -932,12 +936,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg self.backgroundTask = nil } } - - isSuspended = true - - // This does seem to work if scheduled from the background task above - // Schedule it here instead but with an earliest being date of 30 seconds - scheduleBackgroundAppRefresh() } @objc @@ -948,12 +946,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg appMediator.endBackgroundTask(backgroundTask) self.backgroundTask = nil } - - if isSuspended { - startSync() - } - - isSuspended = false + + startSync() } // MARK: Background app refresh @@ -992,7 +986,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg // This is important for the app to keep refreshing in the background scheduleBackgroundAppRefresh() - task.expirationHandler = { + task.expirationHandler = { [weak self] in + if UIApplication.shared.applicationState != .active { + // Attempt to stop the sync loop cleanly, only if the app not already running + self?.stopSync() + } + MXLog.info("Background app refresh task expired") task.setTaskCompleted(success: true) } @@ -1011,8 +1010,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg .collect(.byTimeOrCount(DispatchQueue.main, .seconds(10), 10)) .sink(receiveValue: { [weak self] _ in guard let self else { return } - MXLog.info("Background app refresh finished") + + // Make sure we stop the sync loop, otherwise the ongoing request is immediately + // handled the next time the app refreshes, which can trigger timeout failures. + stopSync() backgroundRefreshSyncObserver?.cancel() task.setTaskCompleted(success: true) diff --git a/ElementX/Sources/Application/AppMediatorProtocol.swift b/ElementX/Sources/Application/AppMediatorProtocol.swift index d7043f2983..090dffba59 100644 --- a/ElementX/Sources/Application/AppMediatorProtocol.swift +++ b/ElementX/Sources/Application/AppMediatorProtocol.swift @@ -29,7 +29,7 @@ protocol AppMediatorProtocol { func requestAuthorizationIfNeeded() async -> Bool } -extension UIApplication.State: CustomStringConvertible { +extension UIApplication.State: @retroactive CustomStringConvertible { public var description: String { switch self { case .active: @@ -44,7 +44,7 @@ extension UIApplication.State: CustomStringConvertible { } } -extension UIUserInterfaceActiveAppearance: CustomStringConvertible { +extension UIUserInterfaceActiveAppearance: @retroactive CustomStringConvertible { public var description: String { switch self { case .active: diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 731b69ed6f..e3b4b39655 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -11,7 +11,8 @@ import SwiftUI // Common settings between app and NSE protocol CommonSettingsProtocol { var logLevel: TracingConfiguration.LogLevel { get } - var invisibleCryptoEnabled: Bool { get } + var enableOnlySignedDeviceIsolationMode: Bool { get } + var hideTimelineMedia: Bool { get } } /// Store Element specific app settings. @@ -31,9 +32,11 @@ final class AppSettings { case pusherProfileTag case logLevel case viewSourceEnabled + case optimizeMediaUploads case appAppearance case sharePresence case hideUnreadMessagesBadge + case hideTimelineMedia case elementCallBaseURLOverride case elementCallEncryptionEnabled @@ -42,8 +45,9 @@ final class AppSettings { case slidingSyncDiscovery case publicSearchEnabled case fuzzyRoomListSearchEnabled - case pinningEnabled - case invisibleCryptoEnabled + case enableOnlySignedDeviceIsolationMode + case knockingEnabled + case frequentEmojisEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -131,6 +135,8 @@ final class AppSettings { let encryptionURL: URL = "https://element.io/help#encryption" /// A URL where users can go read more about the chat backup. let chatBackupDetailsURL: URL = "https://element.io/help#encryption5" + /// A URL where users can go read more about identity pinning violations + let identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18" /// Any domains that Element web may be hosted on - used for handling links. let elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"] @@ -156,14 +162,8 @@ final class AppSettings { /// Any pre-defined static client registrations for OIDC issuers. let oidcStaticRegistrations: [URL: String] = ["https://id.thirdroom.io/realms/thirdroom": "elementx"] - /// The redirect URL used for OIDC. - let oidcRedirectURL = { - guard let url = URL(string: "\(InfoPlistReader.main.appScheme):/callback") else { - fatalError("Invalid OIDC redirect URL") - } - - return url - }() + /// The redirect URL used for OIDC. This no longer uses universal links so we don't need the bundle ID to avoid conflicts between Element X, Nightly and PR builds. + let oidcRedirectURL: URL = "https://element.io/oidc/login" private(set) lazy var oidcConfiguration = OIDCConfigurationProxy(clientName: InfoPlistReader.main.bundleDisplayName, redirectURI: oidcRedirectURL, @@ -246,6 +246,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.viewSourceEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) var viewSourceEnabled + + @UserPreference(key: UserDefaultsKeys.optimizeMediaUploads, defaultValue: true, storageType: .userDefaults(store)) + var optimizeMediaUploads // MARK: - Element Call @@ -280,12 +283,15 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.fuzzyRoomListSearchEnabled, defaultValue: false, storageType: .userDefaults(store)) var fuzzyRoomListSearchEnabled - @UserPreference(key: UserDefaultsKeys.pinningEnabled, defaultValue: true, storageType: .userDefaults(store)) - var pinningEnabled - enum SlidingSyncDiscovery: Codable { case proxy, native, forceNative } @UserPreference(key: UserDefaultsKeys.slidingSyncDiscovery, defaultValue: .native, storageType: .userDefaults(store)) var slidingSyncDiscovery: SlidingSyncDiscovery + + @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) + var knockingEnabled + + @UserPreference(key: UserDefaultsKeys.frequentEmojisEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) + var frequentEmojisEnabled #endif @@ -294,9 +300,12 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.logLevel, defaultValue: TracingConfiguration.LogLevel.info, storageType: .userDefaults(store)) var logLevel - /// Configuration to enable invisible crypto. In this mode only devices signed by their owner will be considered in e2ee rooms. - @UserPreference(key: UserDefaultsKeys.invisibleCryptoEnabled, defaultValue: false, storageType: .userDefaults(store)) - var invisibleCryptoEnabled + /// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms. + @UserPreference(key: UserDefaultsKeys.enableOnlySignedDeviceIsolationMode, defaultValue: false, storageType: .userDefaults(store)) + var enableOnlySignedDeviceIsolationMode + + @UserPreference(key: UserDefaultsKeys.hideTimelineMedia, defaultValue: false, storageType: .userDefaults(store)) + var hideTimelineMedia } extension AppSettings: CommonSettingsProtocol { } diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index 507e77b4b9..d90b9282a5 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -9,8 +9,6 @@ import Foundation import MatrixRustSDK enum AppRoute: Equatable { - /// The callback used to complete login with OIDC. - case oidcCallback(url: URL) /// The app's home screen. case roomList /// A room, shown as the root of the stack (popping any child rooms). @@ -52,7 +50,6 @@ struct AppRouteURLParser { urlParsers = [ MatrixPermalinkParser(), ElementWebURLParser(domains: appSettings.elementWebHosts), - OIDCCallbackURLParser(appSettings: appSettings), ElementCallURLParser() ] } @@ -76,16 +73,6 @@ protocol URLParser { func route(from url: URL) -> AppRoute? } -/// The parser for the OIDC callback URL. This always returns a `.oidcCallback`. -struct OIDCCallbackURLParser: URLParser { - let appSettings: AppSettings - - func route(from url: URL) -> AppRoute? { - guard url.absoluteString.starts(with: appSettings.oidcRedirectURL.absoluteString) else { return nil } - return .oidcCallback(url: url) - } -} - /// The parser for Element Call links. This always returns a `.genericCallLink`. struct ElementCallURLParser: URLParser { private let knownHosts = ["call.element.io"] diff --git a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift index 7441c85dfb..e17c247812 100644 --- a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift +++ b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift @@ -452,15 +452,15 @@ private struct NavigationSplitCoordinatorView: View { } // Handle `horizontalSizeClass` changes breaking the navigation bar // https://github.com/element-hq/element-x-ios/issues/617 - .onChange(of: horizontalSizeClass) { value in + .onChange(of: horizontalSizeClass) { _, newValue in guard scenePhase != .background else { return } - isInSplitMode = value == .regular + isInSplitMode = newValue == .regular } - .onChange(of: scenePhase) { value in - guard value == .active else { + .onChange(of: scenePhase) { _, newValue in + guard newValue == .active else { return } diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index 996ce5b743..7a93de1a0b 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -65,15 +65,6 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { fatalError() } - func handleOIDCRedirectURL(_ url: URL) { - guard let oidcPresenter else { - MXLog.error("Failed to find an OIDC request in progress.") - return - } - - oidcPresenter.handleUniversalLinkCallback(url) - } - // MARK: - Private private func showStartScreen() { @@ -86,11 +77,11 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { switch action { case .loginManually: - Task { await self.startAuthentication(flow: .login) } + showServerConfirmationScreen(authenticationFlow: .login) case .loginWithQR: startQRCodeLogin() case .register: - Task { await self.startAuthentication(flow: .register) } + showServerConfirmationScreen(authenticationFlow: .register) case .reportProblem: showReportProblemScreen() } @@ -113,7 +104,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { switch action { case .signInManually: navigationStackCoordinator.setSheetCoordinator(nil) - Task { await self.startAuthentication(flow: .login) } + showServerConfirmationScreen(authenticationFlow: .login) case .cancel: navigationStackCoordinator.setSheetCoordinator(nil) case .done(let userSession): @@ -137,69 +128,15 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { bugReportFlowCoordinator?.start() } - private func startAuthentication(flow: AuthenticationFlow) async { - startLoading() - - switch await authenticationService.configure(for: appSettings.defaultHomeserverAddress) { - case .success: - stopLoading() - showServerConfirmationScreen(authenticationFlow: flow) - case .failure: - stopLoading() - showServerSelectionScreen(authenticationFlow: flow, isModallyPresented: false) - } - } - - private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow, isModallyPresented: Bool) { - let navigationCoordinator = NavigationStackCoordinator() - - let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService, - userIndicatorController: userIndicatorController, - isModallyPresented: isModallyPresented) - let coordinator = ServerSelectionScreenCoordinator(parameters: parameters) - - coordinator.actions - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .updated: - if isModallyPresented { - navigationStackCoordinator.setSheetCoordinator(nil) - } else { - // We are here because the default server failed to respond. - if authenticationService.homeserver.value.loginMode == .password { - if authenticationFlow == .login { - // Add the password login screen directly to the flow, its fine. - showLoginScreen() - } else { - // Add the web registration screen directly to the flow, its fine. - showWebRegistration() - } - } else { - // OIDC is presented from the confirmation screen so replace the - // server selection screen which was inserted to handle the failure. - navigationStackCoordinator.pop() - showServerConfirmationScreen(authenticationFlow: authenticationFlow) - } - } - case .dismiss: - navigationStackCoordinator.setSheetCoordinator(nil) - } - } - .store(in: &cancellables) - - if isModallyPresented { - navigationCoordinator.setRootCoordinator(coordinator) - navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) - } else { - navigationStackCoordinator.push(coordinator) - } - } - private func showServerConfirmationScreen(authenticationFlow: AuthenticationFlow) { + // Reset the service back to the default homeserver before continuing. This ensures + // we check that registration is supported if it was previously configured for login. + authenticationService.reset() + let parameters = ServerConfirmationScreenCoordinatorParameters(authenticationService: authenticationService, - authenticationFlow: authenticationFlow) + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL, + userIndicatorController: userIndicatorController) let coordinator = ServerConfirmationScreenCoordinator(parameters: parameters) coordinator.actions.sink { [weak self] action in @@ -215,7 +152,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { showLoginScreen() } case .changeServer: - showServerSelectionScreen(authenticationFlow: authenticationFlow, isModallyPresented: true) + showServerSelectionScreen(authenticationFlow: authenticationFlow) } } .store(in: &cancellables) @@ -223,6 +160,32 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator) } + private func showServerSelectionScreen(authenticationFlow: AuthenticationFlow) { + let navigationCoordinator = NavigationStackCoordinator() + + let parameters = ServerSelectionScreenCoordinatorParameters(authenticationService: authenticationService, + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL, + userIndicatorController: userIndicatorController) + let coordinator = ServerSelectionScreenCoordinator(parameters: parameters) + + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .updated: + navigationStackCoordinator.setSheetCoordinator(nil) + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + navigationCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) + } + private func showWebRegistration() { let parameters = WebRegistrationScreenCoordinatorParameters(authenticationService: authenticationService, userIndicatorController: userIndicatorController) @@ -271,8 +234,9 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { private func showLoginScreen() { let parameters = LoginScreenCoordinatorParameters(authenticationService: authenticationService, - analytics: analytics, - userIndicatorController: userIndicatorController) + slidingSyncLearnMoreURL: appSettings.slidingSyncLearnMoreURL, + userIndicatorController: userIndicatorController, + analytics: analytics) let coordinator = LoginScreenCoordinator(parameters: parameters) coordinator.actions diff --git a/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift new file mode 100644 index 0000000000..aea6264c49 --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/EncryptionResetFlowCoordinator.swift @@ -0,0 +1,158 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import SwiftState + +enum EncryptionResetFlowCoordinatorAction: Equatable { + /// The flow is complete. + case resetComplete + /// The flow was cancelled. + case cancel +} + +struct EncryptionResetFlowCoordinatorParameters { + let userSession: UserSessionProtocol + let userIndicatorController: UserIndicatorControllerProtocol + let navigationStackCoordinator: NavigationStackCoordinator + let windowManger: WindowManagerProtocol +} + +class EncryptionResetFlowCoordinator: FlowCoordinatorProtocol { + private let userSession: UserSessionProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private let navigationStackCoordinator: NavigationStackCoordinator + private let windowManager: WindowManagerProtocol + + enum State: StateType { + /// The state machine hasn't started. + case initial + /// The root screen for this flow. + case encryptionResetScreen + /// Confirming the user's password to continue. + case confirmingPassword + } + + enum Event: EventType { + /// The flow is being started. + case start + + /// The user needs to confirm their password to reset. + case confirmPassword + /// The user confirmed their password. + case finishedConfirmingPassword + } + + private let stateMachine: StateMachine + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: EncryptionResetFlowCoordinatorParameters) { + userSession = parameters.userSession + userIndicatorController = parameters.userIndicatorController + navigationStackCoordinator = parameters.navigationStackCoordinator + windowManager = parameters.windowManger + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start() { + stateMachine.tryEvent(.start) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + // There aren't any routes to this screen, so always clear the stack. + clearRoute(animated: animated) + } + + func clearRoute(animated: Bool) { + // As we push screens on top of an existing stack, popping to root wouldn't be safe. + switch stateMachine.state { + case .initial: + break + case .encryptionResetScreen: + navigationStackCoordinator.pop(animated: animated) + case .confirmingPassword: + navigationStackCoordinator.pop(animated: animated) // Password screen. + navigationStackCoordinator.pop(animated: animated) // EncryptionReset screen. + } + } + + // MARK: - Private + + private func configureStateMachine() { + stateMachine.addRoutes(event: .start, transitions: [.initial => .encryptionResetScreen]) { [weak self] _ in + self?.presentEncryptionResetScreen() + } + + stateMachine.addRoutes(event: .confirmPassword, transitions: [.encryptionResetScreen => .confirmingPassword]) { [weak self] context in + guard let passwordPublisher = context.userInfo as? PassthroughSubject else { fatalError("Expected a publisher in the userInfo.") } + self?.presentPasswordScreen(passwordPublisher: passwordPublisher) + } + stateMachine.addRoutes(event: .finishedConfirmingPassword, transitions: [.confirmingPassword => .encryptionResetScreen]) + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } + } + + private func presentEncryptionResetScreen() { + let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, + userIndicatorController: userIndicatorController)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .requestOIDCAuthorisation(let url): + presentOIDCAuthorization(for: url) + case .requestPassword(let passwordPublisher): + stateMachine.tryEvent(.confirmPassword, userInfo: passwordPublisher) + case .cancel: + actionsSubject.send(.cancel) + case .resetFinished: + actionsSubject.send(.resetComplete) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.setRootCoordinator(coordinator) + } + + private func presentPasswordScreen(passwordPublisher: PassthroughSubject) { + let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init(passwordPublisher: passwordPublisher)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .passwordEntered: + navigationStackCoordinator.pop() + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedConfirmingPassword) + } + } + + private var accountSettingsPresenter: OIDCAccountSettingsPresenter? + private func presentOIDCAuthorization(for url: URL) { + // Note to anyone in the future if you come back here to make this open in Safari instead of a WAS. + // As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷‍♂️ + accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: windowManager.mainWindow) + accountSettingsPresenter?.start() + } +} diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift new file mode 100644 index 0000000000..af620ef40b --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -0,0 +1,198 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import SwiftState + +enum EncryptionSettingsFlowCoordinatorAction: Equatable { + /// The flow is complete. + case complete +} + +struct EncryptionSettingsFlowCoordinatorParameters { + let userSession: UserSessionProtocol + let appSettings: AppSettings + let userIndicatorController: UserIndicatorControllerProtocol + let navigationStackCoordinator: NavigationStackCoordinator +} + +class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { + private let userSession: UserSessionProtocol + private let appSettings: AppSettings + private let userIndicatorController: UserIndicatorControllerProtocol + private let navigationStackCoordinator: NavigationStackCoordinator + + // periphery:ignore - retaining purpose + private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator? + + enum State: StateType { + /// The state machine hasn't started. + case initial + /// The root screen for this flow. + case secureBackupScreen + /// The user is managing their recovery key. + case recoveryKeyScreen + /// The user is disabling key backups. + case keyBackupScreen + } + + enum Event: EventType { + /// The flow is being started. + case start + + /// The user would like to manage their recovery key. + case manageRecoveryKey + /// The user finished managing their recovery key. + case finishedManagingRecoveryKey + + /// The user doesn't want to use key backup any more. + case disableKeyBackup + /// The key backup screen was dismissed. + case finishedDisablingKeyBackup + } + + private let stateMachine: StateMachine + private var cancellables: Set = [] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: EncryptionSettingsFlowCoordinatorParameters) { + userSession = parameters.userSession + appSettings = parameters.appSettings + userIndicatorController = parameters.userIndicatorController + navigationStackCoordinator = parameters.navigationStackCoordinator + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start() { + stateMachine.tryEvent(.start) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + switch appRoute { + case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, + .roomDetails, .roomMemberDetails, .userProfile, + .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, + .call, .genericCallLink, .settings: + // These routes aren't in this flow so clear the entire stack. + clearRoute(animated: animated) + case .chatBackupSettings: + popToRootScreen(animated: animated) + } + } + + func clearRoute(animated: Bool) { + let fromState = stateMachine.state + popToRootScreen(animated: animated) + guard fromState != .initial else { return } + navigationStackCoordinator.pop(animated: animated) // SecureBackup screen. + } + + func popToRootScreen(animated: Bool) { + // As we push screens on top of an existing stack, a literal pop to root wouldn't be safe. + switch stateMachine.state { + case .initial, .secureBackupScreen: + break + case .recoveryKeyScreen: + navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // RecoveryKey screen. + case .keyBackupScreen: + navigationStackCoordinator.setSheetCoordinator(nil, animated: animated) // KeyBackup screen. + } + } + + // MARK: - Private + + private func configureStateMachine() { + stateMachine.addRoutes(event: .start, transitions: [.initial => .secureBackupScreen]) { [weak self] _ in + self?.presentSecureBackupScreen() + } + + stateMachine.addRoutes(event: .manageRecoveryKey, transitions: [.secureBackupScreen => .recoveryKeyScreen]) { [weak self] _ in + self?.presentRecoveryKeyScreen() + } + stateMachine.addRoutes(event: .finishedManagingRecoveryKey, transitions: [.recoveryKeyScreen => .secureBackupScreen]) + + stateMachine.addRoutes(event: .disableKeyBackup, transitions: [.secureBackupScreen => .keyBackupScreen]) { [weak self] _ in + self?.presentKeyBackupScreen() + } + stateMachine.addRoutes(event: .finishedDisablingKeyBackup, transitions: [.keyBackupScreen => .secureBackupScreen]) + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } + } + + private func presentSecureBackupScreen(animated: Bool = true) { + let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: appSettings, + clientProxy: userSession.clientProxy, + userIndicatorController: userIndicatorController)) + coordinator.actions.sink { [weak self] action in + guard let self else { return } + + switch action { + case .manageRecoveryKey: + stateMachine.tryEvent(.manageRecoveryKey) + case .disableKeyBackup: + stateMachine.tryEvent(.disableKeyBackup) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in + self?.actionsSubject.send(.complete) + } + } + + private func presentRecoveryKeyScreen() { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, + userIndicatorController: userIndicatorController, + isModallyPresented: true)) + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true) + + navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedManagingRecoveryKey) + } + } + + private func presentKeyBackupScreen() { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + + let coordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, + userIndicatorController: userIndicatorController)) + + coordinator.actions.sink { [weak self] action in + switch action { + case .done: + self?.navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + sheetNavigationStackCoordinator.setRootCoordinator(coordinator, animated: true) + + navigationStackCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator) { [stateMachine] in + stateMachine.tryEvent(.finishedDisablingKeyBackup) + } + } +} diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index ff1b48e11a..7c65f7bf4b 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -46,6 +46,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { // periphery: ignore - used to store the coordinator to avoid deallocation private var appLockFlowCoordinator: AppLockSetupFlowCoordinator? + // periphery: ignore - used to store the coordinator to avoid deallocation + private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator? private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -239,16 +241,14 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { switch action { case .otherDevice: - Task { - await self.presentSessionVerificationScreen() - } + presentSessionVerificationScreen() case .recoveryKey: presentRecoveryKeyScreen() case .skip: appSettings.hasRunIdentityConfirmationOnboarding = true stateMachine.tryEvent(.nextSkippingIdentityConfimed) case .reset: - presentEncryptionResetScreen() + startEncryptionResetFlow() case .logout: actionsSubject.send(.logout) } @@ -258,12 +258,13 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { presentCoordinator(coordinator) } - private func presentSessionVerificationScreen() async { - guard case let .success(sessionVerificationController) = await userSession.clientProxy.sessionVerificationControllerProxy() else { + private func presentSessionVerificationScreen() { + guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else { fatalError("The sessionVerificationController should aways be valid at this point") } - let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) + let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController, + flow: .initiator) let coordinator = SessionVerificationScreenCoordinator(parameters: parameters) @@ -291,12 +292,8 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { guard let self else { return } switch action { - case .recoveryFixed: + case .complete: break // Moving to next state is Handled by the global session verification listener - case .resetEncryption: - presentEncryptionResetScreen() - default: - MXLog.error("Unexpected recovery action: \(action)") } } .store(in: &cancellables) @@ -304,31 +301,31 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { presentCoordinator(coordinator) } - private func presentEncryptionResetScreen() { + private func startEncryptionResetFlow() { let resetNavigationStackCoordinator = NavigationStackCoordinator() - - let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, - navigationStackCoordinator: resetNavigationStackCoordinator, - userIndicatorController: userIndicatorController)) + let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession, + userIndicatorController: userIndicatorController, + navigationStackCoordinator: resetNavigationStackCoordinator, + windowManger: windowManager)) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { return } - switch action { - case .cancel: - navigationStackCoordinator.setSheetCoordinator(nil) - case .requestOIDCAuthorisation(let url): - presentOIDCAuthorisationScreen(url: url) - case .resetFinished: + case .resetComplete: // Moving to next state is handled by the global session verification listener navigationStackCoordinator.setSheetCoordinator(nil) + case .cancel: + navigationStackCoordinator.setSheetCoordinator(nil) } } .store(in: &cancellables) - resetNavigationStackCoordinator.setRootCoordinator(coordinator) + encryptionResetFlowCoordinator = coordinator + coordinator.start() - navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) + navigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) { [weak self] in + self?.encryptionResetFlowCoordinator = nil + } } private func presentIdentityConfirmedScreen() { @@ -407,12 +404,4 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(coordinator, dismissalCallback: dismissalCallback) } } - - private var accountSettingsPresenter: OIDCAccountSettingsPresenter? - private func presentOIDCAuthorisationScreen(url: URL) { - // Note to anyone in the future if you come back here to make this open in Safari instead of a WAS. - // As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷‍♂️ - accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: windowManager.mainWindow) - accountSettingsPresenter?.start() - } } diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index fd021d3cbb..f900329307 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -22,6 +22,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { private let roomProxy: JoinedRoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let appMediator: AppMediatorProtocol + private let emojiProvider: EmojiProviderProtocol private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { @@ -35,13 +36,15 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol, roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol, - appMediator: AppMediatorProtocol) { + appMediator: AppMediatorProtocol, + emojiProvider: EmojiProviderProtocol) { self.navigationStackCoordinator = navigationStackCoordinator self.userSession = userSession self.roomTimelineControllerFactory = roomTimelineControllerFactory self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.appMediator = appMediator + self.emojiProvider = emojiProvider } func start() { @@ -71,7 +74,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), voiceMessageMediaManager: userSession.voiceMessageMediaManager, - appMediator: appMediator)) + appMediator: appMediator, + emojiProvider: emojiProvider)) coordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 76a1d7cb52..e9fb052493 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -163,7 +163,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: break // These are converted to a room ID route one level above. - case .roomList, .userProfile, .call, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings: + case .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings: break // These routes can't be handled. } } @@ -587,13 +587,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { timelineItemFactory: timelineItemFactory) self.timelineController = timelineController - analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace) + analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace) let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy) let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory) - let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, + let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy, + roomProxy: roomProxy, focussedEvent: focussedEvent, timelineController: timelineController, mediaProvider: userSession.mediaProvider, @@ -664,7 +665,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { via: via, clientProxy: userSession.clientProxy, mediaProvider: userSession.mediaProvider, - userIndicatorController: userIndicatorController)) + userIndicatorController: userIndicatorController, + appSettings: appSettings)) joinRoomScreenCoordinator = coordinator @@ -682,7 +684,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { await storeAndSubscribeToRoomProxy(roomProxy) stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated)) - analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount)) + analytics.trackJoinedRoom(isDM: roomProxy.infoPublisher.value.isDirect, + isSpace: roomProxy.infoPublisher.value.isSpace, + activeMemberCount: UInt(roomProxy.infoPublisher.value.activeMembersCount)) } else { stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated)) } @@ -812,6 +816,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let roomDetailsEditParameters = RoomDetailsEditScreenCoordinatorParameters(roomProxy: roomProxy, mediaProvider: userSession.mediaProvider, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings), navigationStackCoordinator: stackCoordinator, userIndicatorController: userIndicatorController, orientationManager: appMediator.windowManager) @@ -897,7 +902,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController, roomProxy: roomProxy, - mediaUploadingPreprocessor: MediaUploadingPreprocessor(), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings), title: url.lastPathComponent, url: url) @@ -934,7 +939,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { MXLog.debug("Selected \(emoji) for \(itemID)") navigationStackCoordinator.setSheetCoordinator(nil) Task { - await self.timelineController?.toggleReaction(emoji, to: itemID) + guard case let .event(_, eventOrTransactionID) = itemID else { + fatalError() + } + + await self.timelineController?.toggleReaction(emoji, to: eventOrTransactionID) } case .dismiss: navigationStackCoordinator.setSheetCoordinator(nil) @@ -1336,7 +1345,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: roomTimelineControllerFactory, roomProxy: roomProxy, userIndicatorController: userIndicatorController, - appMediator: appMediator) + appMediator: appMediator, + emojiProvider: emojiProvider) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index e78d5ab59f..3ff63c7bbe 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -27,6 +27,7 @@ struct SettingsFlowCoordinatorParameters { let appSettings: AppSettings let navigationSplitCoordinator: NavigationSplitCoordinator let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } class SettingsFlowCoordinator: FlowCoordinatorProtocol { @@ -38,9 +39,10 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { // periphery:ignore - retaining purpose private var appLockSetupFlowCoordinator: AppLockSetupFlowCoordinator? - // periphery:ignore - retaining purpose private var bugReportFlowCoordinator: BugReportFlowCoordinator? + // periphery:ignore - retaining purpose + private var encryptionSettingsFlowCoordinator: EncryptionSettingsFlowCoordinator? private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -67,7 +69,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { // The navigation stack doesn't like it if the root and the push happen // on the same loop run DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self.presentSecureBackupScreen(animated: animated) + self.startEncryptionSettingsFlow(animated: animated) } default: break @@ -101,7 +103,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { self.actionsSubject.send(.runLogoutFlow) } case .secureBackup: - presentSecureBackupScreen(animated: true) + startEncryptionSettingsFlow(animated: true) case .userDetails: presentUserDetailsEditScreen() case let .manageAccount(url): @@ -144,27 +146,29 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentedSettings) } - private func presentSecureBackupScreen(animated: Bool) { - let coordinator = SecureBackupScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, - clientProxy: parameters.userSession.clientProxy, - navigationStackCoordinator: navigationStackCoordinator, - userIndicatorController: parameters.userIndicatorController)) - - coordinator.actions.sink { [weak self] action in + private func startEncryptionSettingsFlow(animated: Bool) { + let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: parameters.userSession, + appSettings: parameters.appSettings, + userIndicatorController: parameters.userIndicatorController, + navigationStackCoordinator: navigationStackCoordinator)) + coordinator.actionsPublisher.sink { [weak self] action in switch action { - case .requestOIDCAuthorisation(let url): - self?.presentAccountManagementURL(url) + case .complete: + // The flow coordinator tidies up the stack, no need to do anything. + self?.encryptionSettingsFlowCoordinator = nil } } .store(in: &cancellables) - navigationStackCoordinator.push(coordinator, animated: animated) + encryptionSettingsFlowCoordinator = coordinator + coordinator.start() } private func presentUserDetailsEditScreen() { let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(orientationManager: parameters.windowManager, clientProxy: parameters.userSession.clientProxy, mediaProvider: parameters.userSession.mediaProvider, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: parameters.appSettings), navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: parameters.userIndicatorController)) @@ -173,7 +177,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { private func presentAnalyticsScreen() { let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, - analytics: ServiceLocator.shared.analytics)) + analytics: parameters.analytics)) navigationStackCoordinator?.push(coordinator) } @@ -220,7 +224,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { } private func presentAdvancedSettings() { - let coordinator = AdvancedSettingsScreenCoordinator() + let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(appSettings: parameters.appSettings, + analytics: parameters.analytics)) navigationStackCoordinator.push(coordinator) } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 1f5f8137ff..2bde7f7b58 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -7,6 +7,7 @@ import AVKit import Combine +import MatrixRustSDK import SwiftUI enum UserSessionFlowCoordinatorAction { @@ -41,6 +42,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { // periphery:ignore - retaining purpose private var bugReportFlowCoordinator: BugReportFlowCoordinator? + // periphery:ignore - retaining purpose + private var encryptionResetFlowCoordinator: EncryptionResetFlowCoordinator? + // periphery:ignore - retaining purpose private var globalSearchScreenCoordinator: GlobalSearchScreenCoordinator? @@ -98,7 +102,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { secureBackupController: userSession.clientProxy.secureBackupController, appSettings: appSettings, navigationSplitCoordinator: navigationSplitCoordinator, - userIndicatorController: ServiceLocator.shared.userIndicatorController)) + userIndicatorController: ServiceLocator.shared.userIndicatorController, + analytics: analytics)) onboardingFlowCoordinator = OnboardingFlowCoordinator(userSession: userSession, appLockService: appLockService, @@ -112,81 +117,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { setupStateMachine() - userSession.sessionSecurityStatePublisher - .map(\.verificationState) - .filter { $0 != .unknown } - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self else { return } - - attemptStartingOnboarding() - } - .store(in: &cancellables) - - settingsFlowCoordinator.actions.sink { [weak self] action in - guard let self else { return } - - switch action { - case .presentedSettings: - stateMachine.processEvent(.showSettingsScreen) - case .dismissedSettings: - stateMachine.processEvent(.dismissedSettingsScreen) - case .runLogoutFlow: - Task { await self.runLogoutFlow() } - case .clearCache: - actionsSubject.send(.clearCache) - case .forceLogout: - actionsSubject.send(.forceLogout) - } - } - .store(in: &cancellables) - - userSession.clientProxy.actionsPublisher - .receive(on: DispatchQueue.main) - .sink { action in - guard case let .receivedDecryptionError(info) = action else { - return - } - - let timeToDecryptMs: Int - if let unsignedTimeToDecryptMs = info.timeToDecryptMs { - timeToDecryptMs = Int(unsignedTimeToDecryptMs) - } else { - timeToDecryptMs = -1 - } - - switch info.cause { - case .unknown: - analytics.trackError(context: nil, domain: .E2EE, name: .OlmKeysNotSentError, timeToDecryptMillis: timeToDecryptMs) - case .membership: - analytics.trackError(context: nil, domain: .E2EE, name: .HistoricalMessage, timeToDecryptMillis: timeToDecryptMs) - } - } - .store(in: &cancellables) - - elementCallService.actions - .receive(on: DispatchQueue.main) - .sink { [weak self] action in - switch action { - case .endCall: - self?.dismissCallScreenIfNeeded() - default: - break - } - } - .store(in: &cancellables) - - onboardingFlowCoordinator.actions - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .logout: - logout() - } - } - .store(in: &cancellables) + setupObservers() } func start() { @@ -272,8 +203,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { presentCallScreen(genericCallLink: url) case .settings, .chatBackupSettings: settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated) - case .oidcCallback: - break } } @@ -331,6 +260,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { bugReportFlowCoordinator?.start() case (.feedbackScreen, .dismissedFeedbackScreen, .roomList): break + case (.roomList, .showRecoveryKeyScreen, .recoveryKeyScreen): + presentRecoveryKeyScreen(animated: animated) + case (.recoveryKeyScreen, .dismissedRecoveryKeyScreen, .roomList): + break + case (.roomList, .startEncryptionResetFlow, .encryptionResetFlow): + startEncryptionResetFlow(animated: animated) + case (.encryptionResetFlow, .finishedEncryptionResetFlow, .roomList): + break case (.roomList, .showStartChatScreen, .startChatScreen): presentStartChat(animated: animated) case (.startChatScreen, .dismissedStartChatScreen, .roomList): @@ -370,6 +307,129 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } + private func setupObservers() { + userSession.sessionSecurityStatePublisher + .map(\.verificationState) + .filter { $0 != .unknown } + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { return } + + attemptStartingOnboarding() + + setupSessionVerificationRequestsObserver() + } + .store(in: &cancellables) + + settingsFlowCoordinator.actions.sink { [weak self] action in + guard let self else { return } + + switch action { + case .presentedSettings: + stateMachine.processEvent(.showSettingsScreen) + case .dismissedSettings: + stateMachine.processEvent(.dismissedSettingsScreen) + case .runLogoutFlow: + Task { await self.runLogoutFlow() } + case .clearCache: + actionsSubject.send(.clearCache) + case .forceLogout: + actionsSubject.send(.forceLogout) + } + } + .store(in: &cancellables) + + userSession.clientProxy.actionsPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] action in + guard let self, case let .receivedDecryptionError(info) = action else { + return + } + + let timeToDecryptMs: Int + if let unsignedTimeToDecryptMs = info.timeToDecryptMs { + timeToDecryptMs = Int(unsignedTimeToDecryptMs) + } else { + timeToDecryptMs = -1 + } + + switch info.cause { + case .unknown: + analytics.trackError(context: nil, domain: .E2EE, name: .UnknownError, timeToDecryptMillis: timeToDecryptMs) + case .unknownDevice: + analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs) + case .unsignedDevice: + analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs) + case .verificationViolation: + analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedVerificationViolation, timeToDecryptMillis: timeToDecryptMs) + case .sentBeforeWeJoined: + analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedDueToMembership, timeToDecryptMillis: timeToDecryptMs) + } + } + .store(in: &cancellables) + + elementCallService.actions + .receive(on: DispatchQueue.main) + .sink { [weak self] action in + switch action { + case .endCall: + self?.dismissCallScreenIfNeeded() + default: + break + } + } + .store(in: &cancellables) + + onboardingFlowCoordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .logout: + logout() + } + } + .store(in: &cancellables) + } + + private func setupSessionVerificationRequestsObserver() { + userSession.clientProxy.sessionVerificationController?.actions + .receive(on: DispatchQueue.main) + .sink { [weak self] action in + guard let self, case .receivedVerificationRequest(let details) = action else { + return + } + + MXLog.info("Received session verification request") + + presentSessionVerificationScreen(details: details) + } + .store(in: &cancellables) + } + + private func presentSessionVerificationScreen(details: SessionVerificationRequestDetails) { + guard let sessionVerificationController = userSession.clientProxy.sessionVerificationController else { + fatalError("The sessionVerificationController should aways be valid at this point") + } + + let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController, + flow: .responder(details: details)) + + let coordinator = SessionVerificationScreenCoordinator(parameters: parameters) + + coordinator.actions + .sink { [weak self] action in + switch action { + case .done: + self?.navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + navigationSplitCoordinator.setSheetCoordinator(coordinator) + } + private func presentHomeScreen() { let parameters = HomeScreenCoordinatorParameters(userSession: userSession, bugReportService: bugReportService, @@ -396,6 +456,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { stateMachine.processEvent(.feedbackScreen) case .presentSecureBackupSettings: settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true) + case .presentRecoveryKeyScreen: + stateMachine.processEvent(.showRecoveryKeyScreen) + case .presentEncryptionResetScreen: + stateMachine.processEvent(.startEncryptionResetFlow) case .presentStartChatScreen: stateMachine.processEvent(.showStartChatScreen) case .presentGlobalSearch: @@ -475,7 +539,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { isChildFlow: false, roomTimelineControllerFactory: roomTimelineControllerFactory, navigationStackCoordinator: detailNavigationStackCoordinator, - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: appSettings), ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher, appMediator: appMediator, appSettings: appSettings, @@ -532,7 +596,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { userSession: userSession, userIndicatorController: ServiceLocator.shared.userIndicatorController, navigationStackCoordinator: startChatNavigationStackCoordinator, - userDiscoveryService: userDiscoveryService) + userDiscoveryService: userDiscoveryService, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings)) let coordinator = StartChatScreenCoordinator(parameters: parameters) coordinator.actions.sink { [weak self] action in @@ -553,7 +618,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { self?.stateMachine.processEvent(.dismissedStartChatScreen) } } - + + // MARK: Session Verification + // MARK: Calls private func presentCallScreen(genericCallLink url: URL) { @@ -635,7 +702,59 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationSplitCoordinator.setOverlayCoordinator(nil) } - // MARK: Secure backup confirmation + // MARK: Secure backup + + private func presentRecoveryKeyScreen(animated: Bool) { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + let parameters = SecureBackupRecoveryKeyScreenCoordinatorParameters(secureBackupController: userSession.clientProxy.secureBackupController, + userIndicatorController: ServiceLocator.shared.userIndicatorController, + isModallyPresented: true) + + let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters) + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + sheetNavigationStackCoordinator.setRootCoordinator(coordinator) + + navigationSplitCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator, animated: animated) { [weak self] in + self?.stateMachine.processEvent(.dismissedRecoveryKeyScreen) + } + } + + private func startEncryptionResetFlow(animated: Bool) { + let sheetNavigationStackCoordinator = NavigationStackCoordinator() + let parameters = EncryptionResetFlowCoordinatorParameters(userSession: userSession, + userIndicatorController: ServiceLocator.shared.userIndicatorController, + navigationStackCoordinator: sheetNavigationStackCoordinator, + windowManger: appMediator.windowManager) + + let coordinator = EncryptionResetFlowCoordinator(parameters: parameters) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .resetComplete: + encryptionResetFlowCoordinator = nil + navigationSplitCoordinator.setSheetCoordinator(nil) + case .cancel: + encryptionResetFlowCoordinator = nil + navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + coordinator.start() + encryptionResetFlowCoordinator = coordinator + + navigationSplitCoordinator.setSheetCoordinator(sheetNavigationStackCoordinator, animated: animated) { [weak self] in + self?.stateMachine.processEvent(.finishedEncryptionResetFlow) + } + } private func presentSecureBackupLogoutConfirmationScreen() { let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index 652d1e5992..1c482c1286 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -24,6 +24,12 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the settings screen case settingsScreen(selectedRoomID: String?) + /// Showing the recovery key screen. + case recoveryKeyScreen(selectedRoomID: String?) + + /// Showing the encryption reset flow. + case encryptionResetFlow(selectedRoomID: String?) + /// Showing the start chat screen case startChatScreen(selectedRoomID: String?) @@ -44,6 +50,8 @@ class UserSessionFlowCoordinatorStateMachine { case .roomList(let selectedRoomID), .feedbackScreen(let selectedRoomID), .settingsScreen(let selectedRoomID), + .recoveryKeyScreen(let selectedRoomID), + .encryptionResetFlow(let selectedRoomID), .startChatScreen(let selectedRoomID), .logoutConfirmationScreen(let selectedRoomID), .roomDirectorySearchScreen(let selectedRoomID): @@ -79,12 +87,22 @@ class UserSessionFlowCoordinatorStateMachine { /// The feedback screen has been dismissed case dismissedFeedbackScreen + /// Request presentation of the recovery key screen. + case showRecoveryKeyScreen + /// The recovery key screen has been dismissed. + case dismissedRecoveryKeyScreen + + /// Request presentation of the encryption reset flow. + case startEncryptionResetFlow + /// The encryption reset flow is complete and has been dismissed. + case finishedEncryptionResetFlow + /// Request the start of the start chat flow case showStartChatScreen /// Start chat has been dismissed case dismissedStartChatScreen - /// Logout has been requested and this is the last sesion + /// Logout has been requested and this is the last session case showLogoutConfirmationScreen /// Logout has been cancelled case dismissedLogoutConfirmationScreen @@ -139,6 +157,18 @@ class UserSessionFlowCoordinatorStateMachine { case (.feedbackScreen(let selectedRoomID), .dismissedFeedbackScreen): return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let selectedRoomID), .showRecoveryKeyScreen): + return .recoveryKeyScreen(selectedRoomID: selectedRoomID) + + case (.recoveryKeyScreen(let selectedRoomID), .dismissedRecoveryKeyScreen): + return .roomList(selectedRoomID: selectedRoomID) + + case (.roomList(let selectedRoomID), .startEncryptionResetFlow): + return .encryptionResetFlow(selectedRoomID: selectedRoomID) + + case (.encryptionResetFlow(let selectedRoomID), .finishedEncryptionResetFlow): + return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let selectedRoomID), .showStartChatScreen): return .startChatScreen(selectedRoomID: selectedRoomID) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 23d1461ba7..2c89b62e8c 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -140,6 +140,8 @@ internal enum L10n { internal static var actionForward: String { return L10n.tr("Localizable", "action_forward") } /// Go back internal static var actionGoBack: String { return L10n.tr("Localizable", "action_go_back") } + /// Ignore + internal static var actionIgnore: String { return L10n.tr("Localizable", "action_ignore") } /// Invite internal static var actionInvite: String { return L10n.tr("Localizable", "action_invite") } /// Invite people @@ -164,6 +166,8 @@ internal enum L10n { internal static var actionLeaveConversation: String { return L10n.tr("Localizable", "action_leave_conversation") } /// Leave room internal static var actionLeaveRoom: String { return L10n.tr("Localizable", "action_leave_room") } + /// Load more + internal static var actionLoadMore: String { return L10n.tr("Localizable", "action_load_more") } /// Manage account internal static var actionManageAccount: String { return L10n.tr("Localizable", "action_manage_account") } /// Manage devices @@ -222,6 +226,8 @@ internal enum L10n { internal static var actionShare: String { return L10n.tr("Localizable", "action_share") } /// Share link internal static var actionShareLink: String { return L10n.tr("Localizable", "action_share_link") } + /// Show + internal static var actionShow: String { return L10n.tr("Localizable", "action_show") } /// Sign in again internal static var actionSignInAgain: String { return L10n.tr("Localizable", "action_sign_in_again") } /// Sign out @@ -260,6 +266,12 @@ internal enum L10n { internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") } /// Upgrade available internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") } + /// Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices. + internal static var bannerSetUpRecoveryContent: String { return L10n.tr("Localizable", "banner_set_up_recovery_content") } + /// Set up recovery + internal static var bannerSetUpRecoverySubmit: String { return L10n.tr("Localizable", "banner_set_up_recovery_submit") } + /// Set up recovery to protect your account + internal static var bannerSetUpRecoveryTitle: String { return L10n.tr("Localizable", "banner_set_up_recovery_title") } /// About internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } /// Acceptable use policy @@ -294,6 +306,8 @@ internal enum L10n { internal static var commonDecryptionError: String { return L10n.tr("Localizable", "common_decryption_error") } /// Developer options internal static var commonDeveloperOptions: String { return L10n.tr("Localizable", "common_developer_options") } + /// Device ID + internal static var commonDeviceId: String { return L10n.tr("Localizable", "common_device_id") } /// Direct chat internal static var commonDirectChat: String { return L10n.tr("Localizable", "common_direct_chat") } /// (edited) @@ -304,6 +318,8 @@ internal enum L10n { internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) } + /// Encryption + internal static var commonEncryption: String { return L10n.tr("Localizable", "common_encryption") } /// Encryption enabled internal static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") } /// Enter your PIN @@ -324,6 +340,8 @@ internal enum L10n { internal static var commonFile: String { return L10n.tr("Localizable", "common_file") } /// Forward message internal static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") } + /// Frequently used + internal static var commonFrequentlyUsed: String { return L10n.tr("Localizable", "common_frequently_used") } /// GIF internal static var commonGif: String { return L10n.tr("Localizable", "common_gif") } /// Image @@ -480,8 +498,12 @@ internal enum L10n { internal static var commonTouchIdIos: String { return L10n.tr("Localizable", "common_touch_id_ios") } /// Unable to decrypt internal static var commonUnableToDecrypt: String { return L10n.tr("Localizable", "common_unable_to_decrypt") } + /// Sent from an insecure device + internal static var commonUnableToDecryptInsecureDevice: String { return L10n.tr("Localizable", "common_unable_to_decrypt_insecure_device") } /// You don't have access to this message internal static var commonUnableToDecryptNoAccess: String { return L10n.tr("Localizable", "common_unable_to_decrypt_no_access") } + /// Sender's verified identity has changed + internal static var commonUnableToDecryptVerificationViolation: String { return L10n.tr("Localizable", "common_unable_to_decrypt_verification_violation") } /// Invites couldn't be sent to one or more users. internal static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") } /// Unable to send invite(s) @@ -498,8 +520,14 @@ internal enum L10n { internal static var commonVerificationCancelled: String { return L10n.tr("Localizable", "common_verification_cancelled") } /// Verification complete internal static var commonVerificationComplete: String { return L10n.tr("Localizable", "common_verification_complete") } + /// Verification failed + internal static var commonVerificationFailed: String { return L10n.tr("Localizable", "common_verification_failed") } + /// Verified + internal static var commonVerified: String { return L10n.tr("Localizable", "common_verified") } /// Verify device internal static var commonVerifyDevice: String { return L10n.tr("Localizable", "common_verify_device") } + /// Verify identity + internal static var commonVerifyIdentity: String { return L10n.tr("Localizable", "common_verify_identity") } /// Video internal static var commonVideo: String { return L10n.tr("Localizable", "common_video") } /// Voice message @@ -508,14 +536,30 @@ internal enum L10n { internal static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") } /// Waiting for this message internal static var commonWaitingForDecryptionKey: String { return L10n.tr("Localizable", "common_waiting_for_decryption_key") } - /// Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup. + /// Confirm your recovery key to maintain access to your key storage and message history. internal static var confirmRecoveryKeyBannerMessage: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_message") } /// Enter your recovery key + internal static var confirmRecoveryKeyBannerPrimaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_primary_button_title") } + /// Forgot your recovery key? + internal static var confirmRecoveryKeyBannerSecondaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_secondary_button_title") } + /// Your key storage is out of sync internal static var confirmRecoveryKeyBannerTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_title") } /// %1$@ crashed the last time it was used. Would you like to share a crash report with us? internal static func crashDetectionDialogContent(_ p1: Any) -> String { return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1)) } + /// %1$@'s identity appears to have changed. %2$@ + internal static func cryptoIdentityChangePinViolation(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "crypto_identity_change_pin_violation", String(describing: p1), String(describing: p2)) + } + /// %1$@’s %2$@ identity appears to have changed. %3$@ + internal static func cryptoIdentityChangePinViolationNew(_ p1: Any, _ p2: Any, _ p3: Any) -> String { + return L10n.tr("Localizable", "crypto_identity_change_pin_violation_new", String(describing: p1), String(describing: p2), String(describing: p3)) + } + /// (%1$@) + internal static func cryptoIdentityChangePinViolationNewUserId(_ p1: Any) -> String { + return L10n.tr("Localizable", "crypto_identity_change_pin_violation_new_user_id", String(describing: p1)) + } /// In order to let the application use the camera, please grant the permission in the system settings. internal static var dialogPermissionCamera: String { return L10n.tr("Localizable", "dialog_permission_camera") } /// Please grant the permission in the system settings. @@ -736,6 +780,8 @@ internal enum L10n { internal static var richTextEditorCloseFormattingOptions: String { return L10n.tr("Localizable", "rich_text_editor_close_formatting_options") } /// Toggle code block internal static var richTextEditorCodeBlock: String { return L10n.tr("Localizable", "rich_text_editor_code_block") } + /// Optional caption… + internal static var richTextEditorComposerCaptionPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_composer_caption_placeholder") } /// Message… internal static var richTextEditorComposerPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_composer_placeholder") } /// Create a link @@ -802,6 +848,10 @@ internal enum L10n { internal static var screenAdvancedSettingsElementCallBaseUrlDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_description") } /// Invalid URL, please make sure you include the protocol (http/https) and the correct address. internal static var screenAdvancedSettingsElementCallBaseUrlValidationError: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_validation_error") } + /// Upload photos and videos faster and reduce data usage + internal static var screenAdvancedSettingsMediaCompressionDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_media_compression_description") } + /// Optimise media quality + internal static var screenAdvancedSettingsMediaCompressionTitle: String { return L10n.tr("Localizable", "screen_advanced_settings_media_compression_title") } /// Disable the rich text editor to type Markdown manually. internal static var screenAdvancedSettingsRichTextEditorDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_rich_text_editor_description") } /// Read receipts @@ -973,21 +1023,29 @@ internal enum L10n { internal static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") } /// Select your server internal static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } - /// Turn off backup + /// Delete key storage internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } /// Turn on backup internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } - /// Backup ensures that you don't lose your message history. %1$@. + /// Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@. internal static func screenChatBackupKeyBackupDescription(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_description", String(describing: p1)) } - /// Backup + /// Key storage internal static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") } + /// Key storage must be turned on to set up recovery. + internal static var screenChatBackupKeyStorageDisabledError: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_disabled_error") } + /// Upload keys from this device + internal static var screenChatBackupKeyStorageToggleDescription: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_description") } + /// Allow key storage + internal static var screenChatBackupKeyStorageToggleTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_title") } /// Change recovery key internal static var screenChatBackupRecoveryActionChange: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change") } + /// Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. + internal static var screenChatBackupRecoveryActionChangeDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change_description") } /// Enter recovery key internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } - /// Your chat backup is currently out of sync. + /// Your key storage is currently out of sync. internal static var screenChatBackupRecoveryActionConfirmDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm_description") } /// Set up recovery internal static var screenChatBackupRecoveryActionSetup: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_setup") } @@ -1035,22 +1093,39 @@ internal enum L10n { internal static var screenCreatePollQuestionHint: String { return L10n.tr("Localizable", "screen_create_poll_question_hint") } /// Create Poll internal static var screenCreatePollTitle: String { return L10n.tr("Localizable", "screen_create_poll_title") } + /// Anyone can join this room + internal static var screenCreateRoomAccessSectionAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_access_section_anyone_option_description") } + /// Anyone + internal static var screenCreateRoomAccessSectionAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_access_section_anyone_option_title") } + /// Room Access + internal static var screenCreateRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_create_room_access_section_header") } + /// Anyone can ask to join the room but an administrator or a moderator will have to accept the request + internal static var screenCreateRoomAccessSectionKnockingOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_access_section_knocking_option_description") } + /// Ask to join + internal static var screenCreateRoomAccessSectionKnockingOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_access_section_knocking_option_title") } /// New room internal static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") } /// Invite people internal static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") } /// An error occurred when creating the room internal static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") } - /// Messages in this room are encrypted. Encryption can’t be disabled afterwards. + /// Only people invited can access this room. All messages are end-to-end encrypted. internal static var screenCreateRoomPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_private_option_description") } - /// Private room (invite only) + /// Private room internal static var screenCreateRoomPrivateOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_private_option_title") } - /// Messages are not encrypted and anyone can read them. You can enable encryption at a later date. + /// Anyone can find this room. + /// You can change this anytime in room settings. internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") } - /// Public room (anyone) + /// Public room internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") } + /// In order for this room to be visible in the public room directory, you will need a room address. + internal static var screenCreateRoomRoomAddressSectionFooter: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_footer") } + /// Room address + internal static var screenCreateRoomRoomAddressSectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_title") } /// Room name internal static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") } + /// Room visibility + internal static var screenCreateRoomRoomVisibilitySectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_visibility_section_title") } /// Create a room internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } /// Topic (optional) @@ -1115,7 +1190,7 @@ internal enum L10n { internal static var screenEncryptionResetActionContinueReset: String { return L10n.tr("Localizable", "screen_encryption_reset_action_continue_reset") } /// Your account details, contacts, preferences, and chat list will be kept internal static var screenEncryptionResetBullet1: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_1") } - /// You will lose your existing message history + /// You will lose any message history that’s stored only on the server internal static var screenEncryptionResetBullet2: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_2") } /// You will need to verify all your existing devices and contacts again internal static var screenEncryptionResetBullet3: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_3") } @@ -1161,10 +1236,24 @@ internal enum L10n { internal static func screenInvitesInvitedYou(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "screen_invites_invited_you", String(describing: p1), String(describing: p2)) } + /// Cancel request + internal static var screenJoinRoomCancelKnockAction: String { return L10n.tr("Localizable", "screen_join_room_cancel_knock_action") } + /// Yes, cancel + internal static var screenJoinRoomCancelKnockAlertConfirmation: String { return L10n.tr("Localizable", "screen_join_room_cancel_knock_alert_confirmation") } + /// Are you sure that you want to cancel your request to join this room? + internal static var screenJoinRoomCancelKnockAlertDescription: String { return L10n.tr("Localizable", "screen_join_room_cancel_knock_alert_description") } + /// Cancel request to join + internal static var screenJoinRoomCancelKnockAlertTitle: String { return L10n.tr("Localizable", "screen_join_room_cancel_knock_alert_title") } /// Join room internal static var screenJoinRoomJoinAction: String { return L10n.tr("Localizable", "screen_join_room_join_action") } - /// Knock to join + /// Send request to join internal static var screenJoinRoomKnockAction: String { return L10n.tr("Localizable", "screen_join_room_knock_action") } + /// Message (optional) + internal static var screenJoinRoomKnockMessageDescription: String { return L10n.tr("Localizable", "screen_join_room_knock_message_description") } + /// You will receive an invite to join the room if your request is accepted. + internal static var screenJoinRoomKnockSentDescription: String { return L10n.tr("Localizable", "screen_join_room_knock_sent_description") } + /// Request to join sent + internal static var screenJoinRoomKnockSentTitle: String { return L10n.tr("Localizable", "screen_join_room_knock_sent_title") } /// %1$@ does not support spaces yet. You can access spaces on web. internal static func screenJoinRoomSpaceNotSupportedDescription(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_join_room_space_not_supported_description", String(describing: p1)) @@ -1185,15 +1274,15 @@ internal enum L10n { internal static var screenKeyBackupDisableConfirmationDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_description") } /// Are you sure you want to turn off backup? internal static var screenKeyBackupDisableConfirmationTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_title") } - /// Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will: + /// Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features: internal static var screenKeyBackupDisableDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_description") } - /// Not have encrypted message history on new devices + /// You will not have encrypted message history on new devices internal static var screenKeyBackupDisableDescriptionPoint1: String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_1") } - /// Lose access to your encrypted messages if you are signed out of %1$@ everywhere + /// You will lose access to your encrypted messages if you are signed out of %1$@ everywhere internal static func screenKeyBackupDisableDescriptionPoint2(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_2", String(describing: p1)) } - /// Are you sure you want to turn off backup? + /// Are you sure you want to turn off key storage and delete it? internal static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") } /// This account has been deactivated. internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } @@ -1387,6 +1476,8 @@ internal enum L10n { internal static var screenQrCodeLoginInitialStateItem3Action: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_item_3_action") } /// Scan the QR code with this device internal static var screenQrCodeLoginInitialStateItem4: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_item_4") } + /// Only available if your account provider supports it. + internal static var screenQrCodeLoginInitialStateSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_subtitle") } /// Open %1$@ on another device to get the QR code internal static func screenQrCodeLoginInitialStateTitle(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_title", String(describing: p1)) @@ -1421,7 +1512,7 @@ internal enum L10n { internal static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } /// Generate a new recovery key internal static var screenRecoveryKeyChangeGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key") } - /// Make sure you can store your recovery key somewhere safe + /// Do not share this with anyone! internal static var screenRecoveryKeyChangeGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key_description") } /// Recovery key changed internal static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") } @@ -1431,7 +1522,7 @@ internal enum L10n { internal static var screenRecoveryKeyConfirmCreateNewRecoveryKey: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_create_new_recovery_key") } /// Make sure nobody can see this screen! internal static var screenRecoveryKeyConfirmDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_description") } - /// Please try again to confirm access to your chat backup. + /// Please try again to confirm access to your key storage. internal static var screenRecoveryKeyConfirmErrorContent: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_content") } /// Incorrect recovery key internal static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") } @@ -1451,21 +1542,21 @@ internal enum L10n { internal static var screenRecoveryKeyGeneratingKey: String { return L10n.tr("Localizable", "screen_recovery_key_generating_key") } /// Save recovery key internal static var screenRecoveryKeySaveAction: String { return L10n.tr("Localizable", "screen_recovery_key_save_action") } - /// Write down your recovery key somewhere safe or save it in a password manager. + /// Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe. internal static var screenRecoveryKeySaveDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_description") } /// Tap to copy recovery key internal static var screenRecoveryKeySaveKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_key_description") } - /// Save your recovery key + /// Save your recovery key somewhere safe internal static var screenRecoveryKeySaveTitle: String { return L10n.tr("Localizable", "screen_recovery_key_save_title") } /// You will not be able to access your new recovery key after this step. internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") } /// Have you saved your recovery key? internal static var screenRecoveryKeySetupConfirmationTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_title") } - /// Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’. + /// Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’. internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } /// Generate your recovery key internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") } - /// Make sure you can store your recovery key somewhere safe + /// Do not share this with anyone! internal static var screenRecoveryKeySetupGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key_description") } /// Recovery setup successful internal static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") } @@ -1685,6 +1776,12 @@ internal enum L10n { internal static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") } /// Unblock user internal static var screenRoomMemberDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_user") } + /// Use the web app to verify this user. + internal static var screenRoomMemberDetailsVerifyButtonSubtitle: String { return L10n.tr("Localizable", "screen_room_member_details_verify_button_subtitle") } + /// Verify %1$@ + internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1)) + } /// Ban internal static var screenRoomMemberListBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_action") } /// They won’t be able to join this room again if invited. @@ -1905,6 +2002,8 @@ internal enum L10n { /// Congrats! /// You don’t have any unread messages! internal static var screenRoomlistFilterUnreadsEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_unreads_empty_state_title") } + /// Request to join sent + internal static var screenRoomlistKnockEventSentDescription: String { return L10n.tr("Localizable", "screen_roomlist_knock_event_sent_description") } /// Chats internal static var screenRoomlistMainSpaceTitle: String { return L10n.tr("Localizable", "screen_roomlist_main_space_title") } /// Mark as read @@ -1943,6 +2042,8 @@ internal enum L10n { internal static var screenSessionVerificationCompleteSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_complete_subtitle") } /// Enter recovery key internal static var screenSessionVerificationEnterRecoveryKey: String { return L10n.tr("Localizable", "screen_session_verification_enter_recovery_key") } + /// Either the request timed out, the request was denied, or there was a verification mismatch. + internal static var screenSessionVerificationFailedSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_failed_subtitle") } /// Prove it’s you in order to access your encrypted message history. internal static var screenSessionVerificationOpenExistingSessionSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_open_existing_session_subtitle") } /// Open an existing session @@ -1951,12 +2052,28 @@ internal enum L10n { internal static var screenSessionVerificationPositiveButtonCanceled: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_canceled") } /// I am ready internal static var screenSessionVerificationPositiveButtonInitial: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_initial") } - /// Waiting to match + /// Waiting to match… internal static var screenSessionVerificationPositiveButtonVerifyingOngoing: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_verifying_ongoing") } /// Compare a unique set of emojis. internal static var screenSessionVerificationReadySubtitle: String { return L10n.tr("Localizable", "screen_session_verification_ready_subtitle") } /// Compare the unique emoji, ensuring they appear in the same order. internal static var screenSessionVerificationRequestAcceptedSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_accepted_subtitle") } + /// Signed in + internal static var screenSessionVerificationRequestDetailsTimestamp: String { return L10n.tr("Localizable", "screen_session_verification_request_details_timestamp") } + /// Either the request timed out, the request was denied, or there was a verification mismatch. + internal static var screenSessionVerificationRequestFailureSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_failure_subtitle") } + /// Verification failed + internal static var screenSessionVerificationRequestFailureTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_failure_title") } + /// Only continue if you initiated this verification. + internal static var screenSessionVerificationRequestFooter: String { return L10n.tr("Localizable", "screen_session_verification_request_footer") } + /// Verify the other device to keep your message history secure. + internal static var screenSessionVerificationRequestSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_subtitle") } + /// Now you can read or send messages securely on your other device. + internal static var screenSessionVerificationRequestSuccessSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_success_subtitle") } + /// Device verified + internal static var screenSessionVerificationRequestSuccessTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_success_title") } + /// Verification requested + internal static var screenSessionVerificationRequestTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_title") } /// They don’t match internal static var screenSessionVerificationTheyDontMatch: String { return L10n.tr("Localizable", "screen_session_verification_they_dont_match") } /// They match @@ -2396,21 +2513,9 @@ internal enum L10n { /// Check UnifiedPush internal static var troubleshootNotificationsTestUnifiedPushTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_unified_push_title") } - internal enum Action { - /// Load more - internal static var loadMore: String { return L10n.tr("Localizable", "action.load_more") } - } - - internal enum Banner { - internal enum SetUpRecovery { - /// Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices. - internal static var content: String { return L10n.tr("Localizable", "banner.set_up_recovery.content") } - /// Set up recovery - internal static var title: String { return L10n.tr("Localizable", "banner.set_up_recovery.title") } - } - } - internal enum Common { + /// Copied to clipboard + internal static var copiedToClipboard: String { return L10n.tr("Localizable", "common.copied_to_clipboard") } /// Do not show this again internal static var doNotShowThisAgain: String { return L10n.tr("Localizable", "common.do_not_show_this_again") } /// Open source licenses @@ -2419,6 +2524,8 @@ internal enum L10n { internal static var pinned: String { return L10n.tr("Localizable", "common.pinned") } /// Send to internal static var sendTo: String { return L10n.tr("Localizable", "common.send_to") } + /// You + internal static var you: String { return L10n.tr("Localizable", "common.you") } } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift new file mode 100644 index 0000000000..841a0701d3 --- /dev/null +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderFactoryMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension AuthenticationClientBuilderFactoryMock { + struct Configuration { + var builderConfiguration: AuthenticationClientBuilderMock.Configuration = .init() + } + + convenience init(configuration: Configuration) { + self.init() + + let clientBuilder = AuthenticationClientBuilderMock(configuration: configuration.builderConfiguration) + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue = clientBuilder + } +} diff --git a/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift new file mode 100644 index 0000000000..0cccf9e539 --- /dev/null +++ b/ElementX/Sources/Mocks/AuthenticationClientBuilderMock.swift @@ -0,0 +1,47 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension AuthenticationClientBuilderMock { + struct Configuration { + var homeserverClients = [ + "matrix.org": ClientSDKMock(configuration: .init()), + "example.com": ClientSDKMock(configuration: .init(serverAddress: "example.com", + homeserverURL: "https://matrix.example.com", + slidingSyncVersion: .native, + supportsPasswordLogin: true, + elementWellKnown: "")), + "company.com": ClientSDKMock(configuration: .init(serverAddress: "company.com", + homeserverURL: "https://matrix.company.com", + slidingSyncVersion: .native, + oidcLoginURL: "https://auth.company.com/oidc", + supportsPasswordLogin: false, + elementWellKnown: "")), + "server.net": ClientSDKMock(configuration: .init(serverAddress: "server.net", + homeserverURL: "https://matrix.example.com", + slidingSyncVersion: .native, + supportsPasswordLogin: false, + elementWellKnown: "")) + ] + var qrCodeClient = ClientSDKMock(configuration: .init()) + } + + convenience init(configuration: Configuration) { + self.init() + + buildHomeserverAddressClosure = { address in + guard let client = configuration.homeserverClients[address] else { + throw ClientBuildError.ServerUnreachable(message: "Not a known homeserver.") + } + return client + } + + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue = configuration.qrCodeClient + } +} diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index e727ba6af3..b5b52018bd 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -13,6 +13,8 @@ struct ClientProxyMockConfiguration { var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) var roomDirectorySearchProxy: RoomDirectorySearchProxyProtocol? + + var recoveryState: SecureBackupRecoveryState = .enabled } enum ClientProxyMockError: Error { @@ -50,7 +52,7 @@ extension ClientProxyMock { canDeactivateAccount = false directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) @@ -60,7 +62,6 @@ extension ClientProxyMock { logoutReturnValue = nil searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false)) profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user")) - sessionVerificationControllerProxyReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) ignoreUserReturnValue = .success(()) unignoreUserReturnValue = .success(()) @@ -77,14 +78,10 @@ extension ClientProxyMock { loadMediaContentForSourceThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) - loadMediaFileForSourceBodyThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) + loadMediaFileForSourceFilenameThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic) - secureBackupController = { - let secureBackupController = SecureBackupControllerMock() - secureBackupController.underlyingRecoveryState = .init(CurrentValueSubject(.enabled)) - secureBackupController.underlyingKeyBackupState = .init(CurrentValueSubject(.enabled)) - return secureBackupController - }() + secureBackupController = SecureBackupControllerMock(.init(recoveryState: configuration.recoveryState)) + resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init())) roomForIdentifierClosure = { [weak self] identifier in guard let room = self?.roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else { @@ -93,5 +90,7 @@ extension ClientProxyMock { return await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name))) } + + userIdentityForReturnValue = .success(UserIdentitySDKMock(configuration: .init())) } } diff --git a/ElementX/Sources/Mocks/EventTimelineItem.swift b/ElementX/Sources/Mocks/EventTimelineItem.swift new file mode 100644 index 0000000000..229fe658ca --- /dev/null +++ b/ElementX/Sources/Mocks/EventTimelineItem.swift @@ -0,0 +1,54 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import LoremSwiftum +import MatrixRustSDK + +struct EventTimelineItemSDKMockConfiguration { + var eventID: String = UUID().uuidString + var sender = "" + var isOwn = false + var content: TimelineItemContent = .redactedMessage +} + +extension EventTimelineItem { + init(configuration: EventTimelineItemSDKMockConfiguration) { + self.init(isRemote: true, + eventOrTransactionId: .eventId(eventId: configuration.eventID), + sender: configuration.sender, + senderProfile: .pending, + isOwn: configuration.isOwn, + isEditable: false, + content: configuration.content, + timestamp: 0, + reactions: [], + localSendState: nil, + readReceipts: [:], + origin: nil, + canBeRepliedTo: false, + lazyProvider: LazyTimelineItemProviderSDKMock()) + } + + static var mockMessage: EventTimelineItem { + let body = Lorem.sentences(Int.random(in: 1...5)) + let messageType = MessageType.text(content: .init(body: body, formatted: nil)) + + let content = TimelineItemContent.message(content: .init(msgType: messageType, + body: body, + inReplyTo: nil, + threadRoot: nil, + isEdited: false, + mentions: nil)) + + return .init(configuration: .init(content: content)) + } + + static func mockCallInvite(sender: String) -> EventTimelineItem { + .init(configuration: .init(sender: sender, content: .callInvite)) + } +} diff --git a/ElementX/Sources/Mocks/EventTimelineItemSDKMock.swift b/ElementX/Sources/Mocks/EventTimelineItemSDKMock.swift deleted file mode 100644 index fbeea26eb8..0000000000 --- a/ElementX/Sources/Mocks/EventTimelineItemSDKMock.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Foundation - -struct EventTimelineItemSDKMockConfiguration { - var eventID: String = UUID().uuidString -} - -extension EventTimelineItemSDKMock { - convenience init(configuration: EventTimelineItemSDKMockConfiguration) { - self.init() - eventIdReturnValue = configuration.eventID - isOwnReturnValue = false - timestampReturnValue = 0 - isEditableReturnValue = false - canBeRepliedToReturnValue = false - senderReturnValue = "" - senderProfileReturnValue = .pending - - let timelineItemContent = TimelineItemContentSDKMock() - timelineItemContent.kindReturnValue = .redactedMessage - contentReturnValue = timelineItemContent - } -} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index ba521052fb..c7524379ca 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1819,6 +1819,230 @@ class AudioSessionMock: AudioSessionProtocol { try setActiveOptionsClosure?(active, options) } } +class AuthenticationClientBuilderFactoryMock: AuthenticationClientBuilderFactoryProtocol { + + //MARK: - makeBuilder + + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = 0 + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount: Int { + get { + if Thread.isMainThread { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingCallsCount = newValue + } + } + } + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCalled: Bool { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount > 0 + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments: (sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)? + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations: [(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks)] = [] + + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue: AuthenticationClientBuilderProtocol! + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue: AuthenticationClientBuilderProtocol! { + get { + if Thread.isMainThread { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue + } else { + var returnValue: AuthenticationClientBuilderProtocol? = nil + DispatchQueue.main.sync { + returnValue = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksUnderlyingReturnValue = newValue + } + } + } + } + var makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure: ((SessionDirectories, String, ClientSessionDelegate, AppSettings, AppHooks) -> AuthenticationClientBuilderProtocol)? + + func makeBuilder(sessionDirectories: SessionDirectories, passphrase: String, clientSessionDelegate: ClientSessionDelegate, appSettings: AppSettings, appHooks: AppHooks) -> AuthenticationClientBuilderProtocol { + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount += 1 + makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedArguments = (sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks) + DispatchQueue.main.async { + self.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReceivedInvocations.append((sessionDirectories: sessionDirectories, passphrase: passphrase, clientSessionDelegate: clientSessionDelegate, appSettings: appSettings, appHooks: appHooks)) + } + if let makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure = makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksClosure(sessionDirectories, passphrase, clientSessionDelegate, appSettings, appHooks) + } else { + return makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksReturnValue + } + } +} +class AuthenticationClientBuilderMock: AuthenticationClientBuilderProtocol { + + //MARK: - build + + var buildHomeserverAddressThrowableError: Error? + var buildHomeserverAddressUnderlyingCallsCount = 0 + var buildHomeserverAddressCallsCount: Int { + get { + if Thread.isMainThread { + return buildHomeserverAddressUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildHomeserverAddressUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildHomeserverAddressUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildHomeserverAddressUnderlyingCallsCount = newValue + } + } + } + } + var buildHomeserverAddressCalled: Bool { + return buildHomeserverAddressCallsCount > 0 + } + var buildHomeserverAddressReceivedHomeserverAddress: String? + var buildHomeserverAddressReceivedInvocations: [String] = [] + + var buildHomeserverAddressUnderlyingReturnValue: ClientProtocol! + var buildHomeserverAddressReturnValue: ClientProtocol! { + get { + if Thread.isMainThread { + return buildHomeserverAddressUnderlyingReturnValue + } else { + var returnValue: ClientProtocol? = nil + DispatchQueue.main.sync { + returnValue = buildHomeserverAddressUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildHomeserverAddressUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildHomeserverAddressUnderlyingReturnValue = newValue + } + } + } + } + var buildHomeserverAddressClosure: ((String) async throws -> ClientProtocol)? + + func build(homeserverAddress: String) async throws -> ClientProtocol { + if let error = buildHomeserverAddressThrowableError { + throw error + } + buildHomeserverAddressCallsCount += 1 + buildHomeserverAddressReceivedHomeserverAddress = homeserverAddress + DispatchQueue.main.async { + self.buildHomeserverAddressReceivedInvocations.append(homeserverAddress) + } + if let buildHomeserverAddressClosure = buildHomeserverAddressClosure { + return try await buildHomeserverAddressClosure(homeserverAddress) + } else { + return buildHomeserverAddressReturnValue + } + } + //MARK: - buildWithQRCode + + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError: Error? + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = 0 + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount: Int { + get { + if Thread.isMainThread { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingCallsCount = newValue + } + } + } + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCalled: Bool { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount > 0 + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments: (qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)? + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations: [(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy)] = [] + + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue: ClientProtocol! + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue: ClientProtocol! { + get { + if Thread.isMainThread { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue + } else { + var returnValue: ClientProtocol? = nil + DispatchQueue.main.sync { + returnValue = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerUnderlyingReturnValue = newValue + } + } + } + } + var buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure: ((QrCodeData, OIDCConfigurationProxy, QrLoginProgressListenerProxy) async throws -> ClientProtocol)? + + func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol { + if let error = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerThrowableError { + throw error + } + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerCallsCount += 1 + buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedArguments = (qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener) + DispatchQueue.main.async { + self.buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReceivedInvocations.append((qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration, progressListener: progressListener)) + } + if let buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure = buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure { + return try await buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerClosure(qrCodeData, oidcConfiguration, progressListener) + } else { + return buildWithQRCodeQrCodeDataOidcConfigurationProgressListenerReturnValue + } + } +} class BugReportServiceMock: BugReportServiceProtocol { var crashedLastRun: Bool { get { return underlyingCrashedLastRun } @@ -1986,6 +2210,7 @@ class ClientProxyMock: ClientProxyProtocol { set(value) { underlyingSecureBackupController = value } } var underlyingSecureBackupController: SecureBackupControllerProtocol! + var sessionVerificationController: SessionVerificationControllerProxyProtocol? //MARK: - isOnlyDeviceLeft @@ -2403,15 +2628,15 @@ class ClientProxyMock: ClientProxyProtocol { } //MARK: - createRoom - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = 0 - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount: Int { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount: Int { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount } return returnValue! @@ -2419,29 +2644,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue } } } } - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCalled: Bool { - return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount > 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCalled: Bool { + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount > 0 } - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?)? - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?)] = [] + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)] = [] - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue: Result! - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue: Result! { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue: Result! + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result! { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue } return returnValue! @@ -2449,26 +2674,26 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue } } } } - var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure: ((String, String?, Bool, [String], URL?) async -> Result)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure: ((String, String?, Bool, Bool, [String], URL?) async -> Result)? - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result { - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount += 1 - createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, userIDs: userIDs, avatarURL: avatarURL) + func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result { + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount += 1 + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL) DispatchQueue.main.async { - self.createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, userIDs: userIDs, avatarURL: avatarURL)) + self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL)) } - if let createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure { - return await createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure(name, topic, isRoomPrivate, userIDs, avatarURL) + if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure { + return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL) } else { - return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue } } //MARK: - joinRoom @@ -2611,6 +2836,146 @@ class ClientProxyMock: ClientProxyProtocol { return joinRoomAliasReturnValue } } + //MARK: - knockRoom + + var knockRoomViaMessageUnderlyingCallsCount = 0 + var knockRoomViaMessageCallsCount: Int { + get { + if Thread.isMainThread { + return knockRoomViaMessageUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = knockRoomViaMessageUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomViaMessageUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + knockRoomViaMessageUnderlyingCallsCount = newValue + } + } + } + } + var knockRoomViaMessageCalled: Bool { + return knockRoomViaMessageCallsCount > 0 + } + var knockRoomViaMessageReceivedArguments: (roomID: String, via: [String], message: String?)? + var knockRoomViaMessageReceivedInvocations: [(roomID: String, via: [String], message: String?)] = [] + + var knockRoomViaMessageUnderlyingReturnValue: Result! + var knockRoomViaMessageReturnValue: Result! { + get { + if Thread.isMainThread { + return knockRoomViaMessageUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = knockRoomViaMessageUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomViaMessageUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + knockRoomViaMessageUnderlyingReturnValue = newValue + } + } + } + } + var knockRoomViaMessageClosure: ((String, [String], String?) async -> Result)? + + func knockRoom(_ roomID: String, via: [String], message: String?) async -> Result { + knockRoomViaMessageCallsCount += 1 + knockRoomViaMessageReceivedArguments = (roomID: roomID, via: via, message: message) + DispatchQueue.main.async { + self.knockRoomViaMessageReceivedInvocations.append((roomID: roomID, via: via, message: message)) + } + if let knockRoomViaMessageClosure = knockRoomViaMessageClosure { + return await knockRoomViaMessageClosure(roomID, via, message) + } else { + return knockRoomViaMessageReturnValue + } + } + //MARK: - knockRoomAlias + + var knockRoomAliasMessageUnderlyingCallsCount = 0 + var knockRoomAliasMessageCallsCount: Int { + get { + if Thread.isMainThread { + return knockRoomAliasMessageUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = knockRoomAliasMessageUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomAliasMessageUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + knockRoomAliasMessageUnderlyingCallsCount = newValue + } + } + } + } + var knockRoomAliasMessageCalled: Bool { + return knockRoomAliasMessageCallsCount > 0 + } + var knockRoomAliasMessageReceivedArguments: (roomAlias: String, message: String?)? + var knockRoomAliasMessageReceivedInvocations: [(roomAlias: String, message: String?)] = [] + + var knockRoomAliasMessageUnderlyingReturnValue: Result! + var knockRoomAliasMessageReturnValue: Result! { + get { + if Thread.isMainThread { + return knockRoomAliasMessageUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = knockRoomAliasMessageUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomAliasMessageUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + knockRoomAliasMessageUnderlyingReturnValue = newValue + } + } + } + } + var knockRoomAliasMessageClosure: ((String, String?) async -> Result)? + + func knockRoomAlias(_ roomAlias: String, message: String?) async -> Result { + knockRoomAliasMessageCallsCount += 1 + knockRoomAliasMessageReceivedArguments = (roomAlias: roomAlias, message: message) + DispatchQueue.main.async { + self.knockRoomAliasMessageReceivedInvocations.append((roomAlias: roomAlias, message: message)) + } + if let knockRoomAliasMessageClosure = knockRoomAliasMessageClosure { + return await knockRoomAliasMessageClosure(roomAlias, message) + } else { + return knockRoomAliasMessageReturnValue + } + } //MARK: - uploadMedia var uploadMediaUnderlyingCallsCount = 0 @@ -3155,17 +3520,17 @@ class ClientProxyMock: ClientProxyProtocol { return removeUserAvatarReturnValue } } - //MARK: - sessionVerificationControllerProxy + //MARK: - deactivateAccount - var sessionVerificationControllerProxyUnderlyingCallsCount = 0 - var sessionVerificationControllerProxyCallsCount: Int { + var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0 + var deactivateAccountPasswordEraseDataCallsCount: Int { get { if Thread.isMainThread { - return sessionVerificationControllerProxyUnderlyingCallsCount + return deactivateAccountPasswordEraseDataUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sessionVerificationControllerProxyUnderlyingCallsCount + returnValue = deactivateAccountPasswordEraseDataUnderlyingCallsCount } return returnValue! @@ -3173,27 +3538,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - sessionVerificationControllerProxyUnderlyingCallsCount = newValue + deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sessionVerificationControllerProxyUnderlyingCallsCount = newValue + deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue } } } } - var sessionVerificationControllerProxyCalled: Bool { - return sessionVerificationControllerProxyCallsCount > 0 + var deactivateAccountPasswordEraseDataCalled: Bool { + return deactivateAccountPasswordEraseDataCallsCount > 0 } + var deactivateAccountPasswordEraseDataReceivedArguments: (password: String?, eraseData: Bool)? + var deactivateAccountPasswordEraseDataReceivedInvocations: [(password: String?, eraseData: Bool)] = [] - var sessionVerificationControllerProxyUnderlyingReturnValue: Result! - var sessionVerificationControllerProxyReturnValue: Result! { + var deactivateAccountPasswordEraseDataUnderlyingReturnValue: Result! + var deactivateAccountPasswordEraseDataReturnValue: Result! { get { if Thread.isMainThread { - return sessionVerificationControllerProxyUnderlyingReturnValue + return deactivateAccountPasswordEraseDataUnderlyingReturnValue } else { - var returnValue: Result? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sessionVerificationControllerProxyUnderlyingReturnValue + returnValue = deactivateAccountPasswordEraseDataUnderlyingReturnValue } return returnValue! @@ -3201,87 +3568,21 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - sessionVerificationControllerProxyUnderlyingReturnValue = newValue + deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sessionVerificationControllerProxyUnderlyingReturnValue = newValue + deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue } } } } - var sessionVerificationControllerProxyClosure: (() async -> Result)? + var deactivateAccountPasswordEraseDataClosure: ((String?, Bool) async -> Result)? - func sessionVerificationControllerProxy() async -> Result { - sessionVerificationControllerProxyCallsCount += 1 - if let sessionVerificationControllerProxyClosure = sessionVerificationControllerProxyClosure { - return await sessionVerificationControllerProxyClosure() - } else { - return sessionVerificationControllerProxyReturnValue - } - } - //MARK: - deactivateAccount - - var deactivateAccountPasswordEraseDataUnderlyingCallsCount = 0 - var deactivateAccountPasswordEraseDataCallsCount: Int { - get { - if Thread.isMainThread { - return deactivateAccountPasswordEraseDataUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = deactivateAccountPasswordEraseDataUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - deactivateAccountPasswordEraseDataUnderlyingCallsCount = newValue - } - } - } - } - var deactivateAccountPasswordEraseDataCalled: Bool { - return deactivateAccountPasswordEraseDataCallsCount > 0 - } - var deactivateAccountPasswordEraseDataReceivedArguments: (password: String?, eraseData: Bool)? - var deactivateAccountPasswordEraseDataReceivedInvocations: [(password: String?, eraseData: Bool)] = [] - - var deactivateAccountPasswordEraseDataUnderlyingReturnValue: Result! - var deactivateAccountPasswordEraseDataReturnValue: Result! { - get { - if Thread.isMainThread { - return deactivateAccountPasswordEraseDataUnderlyingReturnValue - } else { - var returnValue: Result? = nil - DispatchQueue.main.sync { - returnValue = deactivateAccountPasswordEraseDataUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - deactivateAccountPasswordEraseDataUnderlyingReturnValue = newValue - } - } - } - } - var deactivateAccountPasswordEraseDataClosure: ((String?, Bool) async -> Result)? - - func deactivateAccount(password: String?, eraseData: Bool) async -> Result { - deactivateAccountPasswordEraseDataCallsCount += 1 - deactivateAccountPasswordEraseDataReceivedArguments = (password: password, eraseData: eraseData) - DispatchQueue.main.async { - self.deactivateAccountPasswordEraseDataReceivedInvocations.append((password: password, eraseData: eraseData)) + func deactivateAccount(password: String?, eraseData: Bool) async -> Result { + deactivateAccountPasswordEraseDataCallsCount += 1 + deactivateAccountPasswordEraseDataReceivedArguments = (password: password, eraseData: eraseData) + DispatchQueue.main.async { + self.deactivateAccountPasswordEraseDataReceivedInvocations.append((password: password, eraseData: eraseData)) } if let deactivateAccountPasswordEraseDataClosure = deactivateAccountPasswordEraseDataClosure { return await deactivateAccountPasswordEraseDataClosure(password, eraseData) @@ -4202,6 +4503,76 @@ class ClientProxyMock: ClientProxyProtocol { return curve25519Base64ReturnValue } } + //MARK: - pinUserIdentity + + var pinUserIdentityUnderlyingCallsCount = 0 + var pinUserIdentityCallsCount: Int { + get { + if Thread.isMainThread { + return pinUserIdentityUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinUserIdentityUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUserIdentityUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinUserIdentityUnderlyingCallsCount = newValue + } + } + } + } + var pinUserIdentityCalled: Bool { + return pinUserIdentityCallsCount > 0 + } + var pinUserIdentityReceivedUserID: String? + var pinUserIdentityReceivedInvocations: [String] = [] + + var pinUserIdentityUnderlyingReturnValue: Result! + var pinUserIdentityReturnValue: Result! { + get { + if Thread.isMainThread { + return pinUserIdentityUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = pinUserIdentityUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUserIdentityUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + pinUserIdentityUnderlyingReturnValue = newValue + } + } + } + } + var pinUserIdentityClosure: ((String) async -> Result)? + + func pinUserIdentity(_ userID: String) async -> Result { + pinUserIdentityCallsCount += 1 + pinUserIdentityReceivedUserID = userID + DispatchQueue.main.async { + self.pinUserIdentityReceivedInvocations.append(userID) + } + if let pinUserIdentityClosure = pinUserIdentityClosure { + return await pinUserIdentityClosure(userID) + } else { + return pinUserIdentityReturnValue + } + } //MARK: - resetIdentity var resetIdentityUnderlyingCallsCount = 0 @@ -4266,6 +4637,76 @@ class ClientProxyMock: ClientProxyProtocol { return resetIdentityReturnValue } } + //MARK: - userIdentity + + var userIdentityForUnderlyingCallsCount = 0 + var userIdentityForCallsCount: Int { + get { + if Thread.isMainThread { + return userIdentityForUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = userIdentityForUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userIdentityForUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + userIdentityForUnderlyingCallsCount = newValue + } + } + } + } + var userIdentityForCalled: Bool { + return userIdentityForCallsCount > 0 + } + var userIdentityForReceivedUserID: String? + var userIdentityForReceivedInvocations: [String] = [] + + var userIdentityForUnderlyingReturnValue: Result! + var userIdentityForReturnValue: Result! { + get { + if Thread.isMainThread { + return userIdentityForUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = userIdentityForUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userIdentityForUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + userIdentityForUnderlyingReturnValue = newValue + } + } + } + } + var userIdentityForClosure: ((String) async -> Result)? + + func userIdentity(for userID: String) async -> Result { + userIdentityForCallsCount += 1 + userIdentityForReceivedUserID = userID + DispatchQueue.main.async { + self.userIdentityForReceivedInvocations.append(userID) + } + if let userIdentityForClosure = userIdentityForClosure { + return await userIdentityForClosure(userID) + } else { + return userIdentityForReturnValue + } + } //MARK: - loadMediaContentForSource var loadMediaContentForSourceThrowableError: Error? @@ -4416,16 +4857,16 @@ class ClientProxyMock: ClientProxyProtocol { } //MARK: - loadMediaFileForSource - var loadMediaFileForSourceBodyThrowableError: Error? - var loadMediaFileForSourceBodyUnderlyingCallsCount = 0 - var loadMediaFileForSourceBodyCallsCount: Int { + var loadMediaFileForSourceFilenameThrowableError: Error? + var loadMediaFileForSourceFilenameUnderlyingCallsCount = 0 + var loadMediaFileForSourceFilenameCallsCount: Int { get { if Thread.isMainThread { - return loadMediaFileForSourceBodyUnderlyingCallsCount + return loadMediaFileForSourceFilenameUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = loadMediaFileForSourceBodyUnderlyingCallsCount + returnValue = loadMediaFileForSourceFilenameUnderlyingCallsCount } return returnValue! @@ -4433,29 +4874,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - loadMediaFileForSourceBodyUnderlyingCallsCount = newValue + loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - loadMediaFileForSourceBodyUnderlyingCallsCount = newValue + loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue } } } } - var loadMediaFileForSourceBodyCalled: Bool { - return loadMediaFileForSourceBodyCallsCount > 0 + var loadMediaFileForSourceFilenameCalled: Bool { + return loadMediaFileForSourceFilenameCallsCount > 0 } - var loadMediaFileForSourceBodyReceivedArguments: (source: MediaSourceProxy, body: String?)? - var loadMediaFileForSourceBodyReceivedInvocations: [(source: MediaSourceProxy, body: String?)] = [] + var loadMediaFileForSourceFilenameReceivedArguments: (source: MediaSourceProxy, filename: String?)? + var loadMediaFileForSourceFilenameReceivedInvocations: [(source: MediaSourceProxy, filename: String?)] = [] - var loadMediaFileForSourceBodyUnderlyingReturnValue: MediaFileHandleProxy! - var loadMediaFileForSourceBodyReturnValue: MediaFileHandleProxy! { + var loadMediaFileForSourceFilenameUnderlyingReturnValue: MediaFileHandleProxy! + var loadMediaFileForSourceFilenameReturnValue: MediaFileHandleProxy! { get { if Thread.isMainThread { - return loadMediaFileForSourceBodyUnderlyingReturnValue + return loadMediaFileForSourceFilenameUnderlyingReturnValue } else { var returnValue: MediaFileHandleProxy? = nil DispatchQueue.main.sync { - returnValue = loadMediaFileForSourceBodyUnderlyingReturnValue + returnValue = loadMediaFileForSourceFilenameUnderlyingReturnValue } return returnValue! @@ -4463,29 +4904,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - loadMediaFileForSourceBodyUnderlyingReturnValue = newValue + loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - loadMediaFileForSourceBodyUnderlyingReturnValue = newValue + loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue } } } } - var loadMediaFileForSourceBodyClosure: ((MediaSourceProxy, String?) async throws -> MediaFileHandleProxy)? + var loadMediaFileForSourceFilenameClosure: ((MediaSourceProxy, String?) async throws -> MediaFileHandleProxy)? - func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy { - if let error = loadMediaFileForSourceBodyThrowableError { + func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy { + if let error = loadMediaFileForSourceFilenameThrowableError { throw error } - loadMediaFileForSourceBodyCallsCount += 1 - loadMediaFileForSourceBodyReceivedArguments = (source: source, body: body) + loadMediaFileForSourceFilenameCallsCount += 1 + loadMediaFileForSourceFilenameReceivedArguments = (source: source, filename: filename) DispatchQueue.main.async { - self.loadMediaFileForSourceBodyReceivedInvocations.append((source: source, body: body)) + self.loadMediaFileForSourceFilenameReceivedInvocations.append((source: source, filename: filename)) } - if let loadMediaFileForSourceBodyClosure = loadMediaFileForSourceBodyClosure { - return try await loadMediaFileForSourceBodyClosure(source, body) + if let loadMediaFileForSourceFilenameClosure = loadMediaFileForSourceFilenameClosure { + return try await loadMediaFileForSourceFilenameClosure(source, filename) } else { - return loadMediaFileForSourceBodyReturnValue + return loadMediaFileForSourceFilenameReturnValue } } } @@ -5320,67 +5761,21 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { } } class InvitedRoomProxyMock: InvitedRoomProxyProtocol { - var inviterCallsCount = 0 - var inviterCalled: Bool { - return inviterCallsCount > 0 - } - - var inviter: RoomMemberProxyProtocol? { - get async { - inviterCallsCount += 1 - if let inviterClosure = inviterClosure { - return await inviterClosure() - } else { - return underlyingInviter - } - } + var info: RoomInfoProxy { + get { return underlyingInfo } + set(value) { underlyingInfo = value } } - var underlyingInviter: RoomMemberProxyProtocol? - var inviterClosure: (() async -> RoomMemberProxyProtocol?)? + var underlyingInfo: RoomInfoProxy! var id: String { get { return underlyingId } set(value) { underlyingId = value } } var underlyingId: String! - var canonicalAlias: String? var ownUserID: String { get { return underlyingOwnUserID } set(value) { underlyingOwnUserID = value } } var underlyingOwnUserID: String! - var name: String? - var topic: String? - var avatar: RoomAvatar { - get { return underlyingAvatar } - set(value) { underlyingAvatar = value } - } - var underlyingAvatar: RoomAvatar! - var avatarURL: URL? - var isPublic: Bool { - get { return underlyingIsPublic } - set(value) { underlyingIsPublic = value } - } - var underlyingIsPublic: Bool! - var isDirect: Bool { - get { return underlyingIsDirect } - set(value) { underlyingIsDirect = value } - } - var underlyingIsDirect: Bool! - var isSpace: Bool { - get { return underlyingIsSpace } - set(value) { underlyingIsSpace = value } - } - var underlyingIsSpace: Bool! - var joinedMembersCount: Int { - get { return underlyingJoinedMembersCount } - set(value) { underlyingJoinedMembersCount = value } - } - var underlyingJoinedMembersCount: Int! - var activeMembersCount: Int { - get { return underlyingActiveMembersCount } - set(value) { underlyingActiveMembersCount = value } - } - var underlyingActiveMembersCount: Int! //MARK: - rejectInvitation @@ -5517,46 +5912,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { set(value) { underlyingIsEncrypted = value } } var underlyingIsEncrypted: Bool! - var isFavouriteCallsCount = 0 - var isFavouriteCalled: Bool { - return isFavouriteCallsCount > 0 - } - - var isFavourite: Bool { - get async { - isFavouriteCallsCount += 1 - if let isFavouriteClosure = isFavouriteClosure { - return await isFavouriteClosure() - } else { - return underlyingIsFavourite - } - } - } - var underlyingIsFavourite: Bool! - var isFavouriteClosure: (() async -> Bool)? - var pinnedEventIDsCallsCount = 0 - var pinnedEventIDsCalled: Bool { - return pinnedEventIDsCallsCount > 0 - } - - var pinnedEventIDs: Set { - get async { - pinnedEventIDsCallsCount += 1 - if let pinnedEventIDsClosure = pinnedEventIDsClosure { - return await pinnedEventIDsClosure() - } else { - return underlyingPinnedEventIDs - } - } - } - var underlyingPinnedEventIDs: Set! - var pinnedEventIDsClosure: (() async -> Set)? - var hasOngoingCall: Bool { - get { return underlyingHasOngoingCall } - set(value) { underlyingHasOngoingCall = value } + var infoPublisher: CurrentValuePublisher { + get { return underlyingInfoPublisher } + set(value) { underlyingInfoPublisher = value } } - var underlyingHasOngoingCall: Bool! - var activeRoomCallParticipants: [String] = [] + var underlyingInfoPublisher: CurrentValuePublisher! var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get { return underlyingMembersPublisher } set(value) { underlyingMembersPublisher = value } @@ -5567,11 +5927,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { set(value) { underlyingTypingMembersPublisher = value } } var underlyingTypingMembersPublisher: CurrentValuePublisher<[String], Never>! - var actionsPublisher: AnyPublisher { - get { return underlyingActionsPublisher } - set(value) { underlyingActionsPublisher = value } + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { + get { return underlyingIdentityStatusChangesPublisher } + set(value) { underlyingIdentityStatusChangesPublisher = value } } - var underlyingActionsPublisher: AnyPublisher! + var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>! var timeline: TimelineProxyProtocol { get { return underlyingTimeline } set(value) { underlyingTimeline = value } @@ -5599,45 +5959,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { set(value) { underlyingId = value } } var underlyingId: String! - var canonicalAlias: String? var ownUserID: String { get { return underlyingOwnUserID } set(value) { underlyingOwnUserID = value } } var underlyingOwnUserID: String! - var name: String? - var topic: String? - var avatar: RoomAvatar { - get { return underlyingAvatar } - set(value) { underlyingAvatar = value } - } - var underlyingAvatar: RoomAvatar! - var avatarURL: URL? - var isPublic: Bool { - get { return underlyingIsPublic } - set(value) { underlyingIsPublic = value } - } - var underlyingIsPublic: Bool! - var isDirect: Bool { - get { return underlyingIsDirect } - set(value) { underlyingIsDirect = value } - } - var underlyingIsDirect: Bool! - var isSpace: Bool { - get { return underlyingIsSpace } - set(value) { underlyingIsSpace = value } - } - var underlyingIsSpace: Bool! - var joinedMembersCount: Int { - get { return underlyingJoinedMembersCount } - set(value) { underlyingJoinedMembersCount = value } - } - var underlyingJoinedMembersCount: Int! - var activeMembersCount: Int { - get { return underlyingActiveMembersCount } - set(value) { underlyingActiveMembersCount = value } - } - var underlyingActiveMembersCount: Int! //MARK: - subscribeForUpdates @@ -9242,6 +9568,88 @@ class KeychainControllerMock: KeychainControllerProtocol { removePINCodeBiometricStateClosure?() } } +class KnockedRoomProxyMock: KnockedRoomProxyProtocol { + var info: RoomInfoProxy { + get { return underlyingInfo } + set(value) { underlyingInfo = value } + } + var underlyingInfo: RoomInfoProxy! + var id: String { + get { return underlyingId } + set(value) { underlyingId = value } + } + var underlyingId: String! + var ownUserID: String { + get { return underlyingOwnUserID } + set(value) { underlyingOwnUserID = value } + } + var underlyingOwnUserID: String! + + //MARK: - cancelKnock + + var cancelKnockUnderlyingCallsCount = 0 + var cancelKnockCallsCount: Int { + get { + if Thread.isMainThread { + return cancelKnockUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = cancelKnockUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + cancelKnockUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + cancelKnockUnderlyingCallsCount = newValue + } + } + } + } + var cancelKnockCalled: Bool { + return cancelKnockCallsCount > 0 + } + + var cancelKnockUnderlyingReturnValue: Result! + var cancelKnockReturnValue: Result! { + get { + if Thread.isMainThread { + return cancelKnockUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = cancelKnockUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + cancelKnockUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + cancelKnockUnderlyingReturnValue = newValue + } + } + } + } + var cancelKnockClosure: (() async -> Result)? + + func cancelKnock() async -> Result { + cancelKnockCallsCount += 1 + if let cancelKnockClosure = cancelKnockClosure { + return await cancelKnockClosure() + } else { + return cancelKnockReturnValue + } + } +} class MediaLoaderMock: MediaLoaderProtocol { //MARK: - loadMediaContentForSource @@ -9394,16 +9802,16 @@ class MediaLoaderMock: MediaLoaderProtocol { } //MARK: - loadMediaFileForSource - var loadMediaFileForSourceBodyThrowableError: Error? - var loadMediaFileForSourceBodyUnderlyingCallsCount = 0 - var loadMediaFileForSourceBodyCallsCount: Int { + var loadMediaFileForSourceFilenameThrowableError: Error? + var loadMediaFileForSourceFilenameUnderlyingCallsCount = 0 + var loadMediaFileForSourceFilenameCallsCount: Int { get { if Thread.isMainThread { - return loadMediaFileForSourceBodyUnderlyingCallsCount + return loadMediaFileForSourceFilenameUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = loadMediaFileForSourceBodyUnderlyingCallsCount + returnValue = loadMediaFileForSourceFilenameUnderlyingCallsCount } return returnValue! @@ -9411,29 +9819,29 @@ class MediaLoaderMock: MediaLoaderProtocol { } set { if Thread.isMainThread { - loadMediaFileForSourceBodyUnderlyingCallsCount = newValue + loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - loadMediaFileForSourceBodyUnderlyingCallsCount = newValue + loadMediaFileForSourceFilenameUnderlyingCallsCount = newValue } } } } - var loadMediaFileForSourceBodyCalled: Bool { - return loadMediaFileForSourceBodyCallsCount > 0 + var loadMediaFileForSourceFilenameCalled: Bool { + return loadMediaFileForSourceFilenameCallsCount > 0 } - var loadMediaFileForSourceBodyReceivedArguments: (source: MediaSourceProxy, body: String?)? - var loadMediaFileForSourceBodyReceivedInvocations: [(source: MediaSourceProxy, body: String?)] = [] + var loadMediaFileForSourceFilenameReceivedArguments: (source: MediaSourceProxy, filename: String?)? + var loadMediaFileForSourceFilenameReceivedInvocations: [(source: MediaSourceProxy, filename: String?)] = [] - var loadMediaFileForSourceBodyUnderlyingReturnValue: MediaFileHandleProxy! - var loadMediaFileForSourceBodyReturnValue: MediaFileHandleProxy! { + var loadMediaFileForSourceFilenameUnderlyingReturnValue: MediaFileHandleProxy! + var loadMediaFileForSourceFilenameReturnValue: MediaFileHandleProxy! { get { if Thread.isMainThread { - return loadMediaFileForSourceBodyUnderlyingReturnValue + return loadMediaFileForSourceFilenameUnderlyingReturnValue } else { var returnValue: MediaFileHandleProxy? = nil DispatchQueue.main.sync { - returnValue = loadMediaFileForSourceBodyUnderlyingReturnValue + returnValue = loadMediaFileForSourceFilenameUnderlyingReturnValue } return returnValue! @@ -9441,29 +9849,29 @@ class MediaLoaderMock: MediaLoaderProtocol { } set { if Thread.isMainThread { - loadMediaFileForSourceBodyUnderlyingReturnValue = newValue + loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - loadMediaFileForSourceBodyUnderlyingReturnValue = newValue + loadMediaFileForSourceFilenameUnderlyingReturnValue = newValue } } } } - var loadMediaFileForSourceBodyClosure: ((MediaSourceProxy, String?) async throws -> MediaFileHandleProxy)? + var loadMediaFileForSourceFilenameClosure: ((MediaSourceProxy, String?) async throws -> MediaFileHandleProxy)? - func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy { - if let error = loadMediaFileForSourceBodyThrowableError { + func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy { + if let error = loadMediaFileForSourceFilenameThrowableError { throw error } - loadMediaFileForSourceBodyCallsCount += 1 - loadMediaFileForSourceBodyReceivedArguments = (source: source, body: body) + loadMediaFileForSourceFilenameCallsCount += 1 + loadMediaFileForSourceFilenameReceivedArguments = (source: source, filename: filename) DispatchQueue.main.async { - self.loadMediaFileForSourceBodyReceivedInvocations.append((source: source, body: body)) + self.loadMediaFileForSourceFilenameReceivedInvocations.append((source: source, filename: filename)) } - if let loadMediaFileForSourceBodyClosure = loadMediaFileForSourceBodyClosure { - return try await loadMediaFileForSourceBodyClosure(source, body) + if let loadMediaFileForSourceFilenameClosure = loadMediaFileForSourceFilenameClosure { + return try await loadMediaFileForSourceFilenameClosure(source, filename) } else { - return loadMediaFileForSourceBodyReturnValue + return loadMediaFileForSourceFilenameReturnValue } } } @@ -9751,7 +10159,413 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = playerForUnderlyingReturnValue + returnValue = playerForUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + playerForUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + playerForUnderlyingReturnValue = newValue + } + } + } + } + var playerForClosure: ((MediaSourceProxy) -> Result)? + + func player(for mediaSource: MediaSourceProxy) -> Result { + playerForCallsCount += 1 + playerForReceivedMediaSource = mediaSource + DispatchQueue.main.async { + self.playerForReceivedInvocations.append(mediaSource) + } + if let playerForClosure = playerForClosure { + return playerForClosure(mediaSource) + } else { + return playerForReturnValue + } + } + //MARK: - playerState + + var playerStateForUnderlyingCallsCount = 0 + var playerStateForCallsCount: Int { + get { + if Thread.isMainThread { + return playerStateForUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = playerStateForUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + playerStateForUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + playerStateForUnderlyingCallsCount = newValue + } + } + } + } + var playerStateForCalled: Bool { + return playerStateForCallsCount > 0 + } + var playerStateForReceivedId: AudioPlayerStateIdentifier? + var playerStateForReceivedInvocations: [AudioPlayerStateIdentifier] = [] + + var playerStateForUnderlyingReturnValue: AudioPlayerState? + var playerStateForReturnValue: AudioPlayerState? { + get { + if Thread.isMainThread { + return playerStateForUnderlyingReturnValue + } else { + var returnValue: AudioPlayerState?? = nil + DispatchQueue.main.sync { + returnValue = playerStateForUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + playerStateForUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + playerStateForUnderlyingReturnValue = newValue + } + } + } + } + var playerStateForClosure: ((AudioPlayerStateIdentifier) -> AudioPlayerState?)? + + func playerState(for id: AudioPlayerStateIdentifier) -> AudioPlayerState? { + playerStateForCallsCount += 1 + playerStateForReceivedId = id + DispatchQueue.main.async { + self.playerStateForReceivedInvocations.append(id) + } + if let playerStateForClosure = playerStateForClosure { + return playerStateForClosure(id) + } else { + return playerStateForReturnValue + } + } + //MARK: - register + + var registerAudioPlayerStateUnderlyingCallsCount = 0 + var registerAudioPlayerStateCallsCount: Int { + get { + if Thread.isMainThread { + return registerAudioPlayerStateUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = registerAudioPlayerStateUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + registerAudioPlayerStateUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + registerAudioPlayerStateUnderlyingCallsCount = newValue + } + } + } + } + var registerAudioPlayerStateCalled: Bool { + return registerAudioPlayerStateCallsCount > 0 + } + var registerAudioPlayerStateReceivedAudioPlayerState: AudioPlayerState? + var registerAudioPlayerStateReceivedInvocations: [AudioPlayerState] = [] + var registerAudioPlayerStateClosure: ((AudioPlayerState) -> Void)? + + func register(audioPlayerState: AudioPlayerState) { + registerAudioPlayerStateCallsCount += 1 + registerAudioPlayerStateReceivedAudioPlayerState = audioPlayerState + DispatchQueue.main.async { + self.registerAudioPlayerStateReceivedInvocations.append(audioPlayerState) + } + registerAudioPlayerStateClosure?(audioPlayerState) + } + //MARK: - unregister + + var unregisterAudioPlayerStateUnderlyingCallsCount = 0 + var unregisterAudioPlayerStateCallsCount: Int { + get { + if Thread.isMainThread { + return unregisterAudioPlayerStateUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = unregisterAudioPlayerStateUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + unregisterAudioPlayerStateUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + unregisterAudioPlayerStateUnderlyingCallsCount = newValue + } + } + } + } + var unregisterAudioPlayerStateCalled: Bool { + return unregisterAudioPlayerStateCallsCount > 0 + } + var unregisterAudioPlayerStateReceivedAudioPlayerState: AudioPlayerState? + var unregisterAudioPlayerStateReceivedInvocations: [AudioPlayerState] = [] + var unregisterAudioPlayerStateClosure: ((AudioPlayerState) -> Void)? + + func unregister(audioPlayerState: AudioPlayerState) { + unregisterAudioPlayerStateCallsCount += 1 + unregisterAudioPlayerStateReceivedAudioPlayerState = audioPlayerState + DispatchQueue.main.async { + self.unregisterAudioPlayerStateReceivedInvocations.append(audioPlayerState) + } + unregisterAudioPlayerStateClosure?(audioPlayerState) + } + //MARK: - detachAllStates + + var detachAllStatesExceptUnderlyingCallsCount = 0 + var detachAllStatesExceptCallsCount: Int { + get { + if Thread.isMainThread { + return detachAllStatesExceptUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = detachAllStatesExceptUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + detachAllStatesExceptUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + detachAllStatesExceptUnderlyingCallsCount = newValue + } + } + } + } + var detachAllStatesExceptCalled: Bool { + return detachAllStatesExceptCallsCount > 0 + } + var detachAllStatesExceptReceivedException: AudioPlayerState? + var detachAllStatesExceptReceivedInvocations: [AudioPlayerState?] = [] + var detachAllStatesExceptClosure: ((AudioPlayerState?) async -> Void)? + + func detachAllStates(except exception: AudioPlayerState?) async { + detachAllStatesExceptCallsCount += 1 + detachAllStatesExceptReceivedException = exception + DispatchQueue.main.async { + self.detachAllStatesExceptReceivedInvocations.append(exception) + } + await detachAllStatesExceptClosure?(exception) + } +} +class MediaProviderMock: MediaProviderProtocol { + + //MARK: - imageFromSource + + var imageFromSourceSizeUnderlyingCallsCount = 0 + var imageFromSourceSizeCallsCount: Int { + get { + if Thread.isMainThread { + return imageFromSourceSizeUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = imageFromSourceSizeUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + imageFromSourceSizeUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + imageFromSourceSizeUnderlyingCallsCount = newValue + } + } + } + } + var imageFromSourceSizeCalled: Bool { + return imageFromSourceSizeCallsCount > 0 + } + var imageFromSourceSizeReceivedArguments: (source: MediaSourceProxy?, size: CGSize?)? + var imageFromSourceSizeReceivedInvocations: [(source: MediaSourceProxy?, size: CGSize?)] = [] + + var imageFromSourceSizeUnderlyingReturnValue: UIImage? + var imageFromSourceSizeReturnValue: UIImage? { + get { + if Thread.isMainThread { + return imageFromSourceSizeUnderlyingReturnValue + } else { + var returnValue: UIImage?? = nil + DispatchQueue.main.sync { + returnValue = imageFromSourceSizeUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + imageFromSourceSizeUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + imageFromSourceSizeUnderlyingReturnValue = newValue + } + } + } + } + var imageFromSourceSizeClosure: ((MediaSourceProxy?, CGSize?) -> UIImage?)? + + func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? { + imageFromSourceSizeCallsCount += 1 + imageFromSourceSizeReceivedArguments = (source: source, size: size) + DispatchQueue.main.async { + self.imageFromSourceSizeReceivedInvocations.append((source: source, size: size)) + } + if let imageFromSourceSizeClosure = imageFromSourceSizeClosure { + return imageFromSourceSizeClosure(source, size) + } else { + return imageFromSourceSizeReturnValue + } + } + //MARK: - loadImageFromSource + + var loadImageFromSourceSizeUnderlyingCallsCount = 0 + var loadImageFromSourceSizeCallsCount: Int { + get { + if Thread.isMainThread { + return loadImageFromSourceSizeUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = loadImageFromSourceSizeUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loadImageFromSourceSizeUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + loadImageFromSourceSizeUnderlyingCallsCount = newValue + } + } + } + } + var loadImageFromSourceSizeCalled: Bool { + return loadImageFromSourceSizeCallsCount > 0 + } + var loadImageFromSourceSizeReceivedArguments: (source: MediaSourceProxy, size: CGSize?)? + var loadImageFromSourceSizeReceivedInvocations: [(source: MediaSourceProxy, size: CGSize?)] = [] + + var loadImageFromSourceSizeUnderlyingReturnValue: Result! + var loadImageFromSourceSizeReturnValue: Result! { + get { + if Thread.isMainThread { + return loadImageFromSourceSizeUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = loadImageFromSourceSizeUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loadImageFromSourceSizeUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + loadImageFromSourceSizeUnderlyingReturnValue = newValue + } + } + } + } + var loadImageFromSourceSizeClosure: ((MediaSourceProxy, CGSize?) async -> Result)? + + func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result { + loadImageFromSourceSizeCallsCount += 1 + loadImageFromSourceSizeReceivedArguments = (source: source, size: size) + DispatchQueue.main.async { + self.loadImageFromSourceSizeReceivedInvocations.append((source: source, size: size)) + } + if let loadImageFromSourceSizeClosure = loadImageFromSourceSizeClosure { + return await loadImageFromSourceSizeClosure(source, size) + } else { + return loadImageFromSourceSizeReturnValue + } + } + //MARK: - loadImageDataFromSource + + var loadImageDataFromSourceUnderlyingCallsCount = 0 + var loadImageDataFromSourceCallsCount: Int { + get { + if Thread.isMainThread { + return loadImageDataFromSourceUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = loadImageDataFromSourceUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loadImageDataFromSourceUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + loadImageDataFromSourceUnderlyingCallsCount = newValue + } + } + } + } + var loadImageDataFromSourceCalled: Bool { + return loadImageDataFromSourceCallsCount > 0 + } + var loadImageDataFromSourceReceivedSource: MediaSourceProxy? + var loadImageDataFromSourceReceivedInvocations: [MediaSourceProxy] = [] + + var loadImageDataFromSourceUnderlyingReturnValue: Result! + var loadImageDataFromSourceReturnValue: Result! { + get { + if Thread.isMainThread { + return loadImageDataFromSourceUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = loadImageDataFromSourceUnderlyingReturnValue } return returnValue! @@ -9759,39 +10573,39 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - playerForUnderlyingReturnValue = newValue + loadImageDataFromSourceUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - playerForUnderlyingReturnValue = newValue + loadImageDataFromSourceUnderlyingReturnValue = newValue } } } } - var playerForClosure: ((MediaSourceProxy) -> Result)? + var loadImageDataFromSourceClosure: ((MediaSourceProxy) async -> Result)? - func player(for mediaSource: MediaSourceProxy) -> Result { - playerForCallsCount += 1 - playerForReceivedMediaSource = mediaSource + func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result { + loadImageDataFromSourceCallsCount += 1 + loadImageDataFromSourceReceivedSource = source DispatchQueue.main.async { - self.playerForReceivedInvocations.append(mediaSource) + self.loadImageDataFromSourceReceivedInvocations.append(source) } - if let playerForClosure = playerForClosure { - return playerForClosure(mediaSource) + if let loadImageDataFromSourceClosure = loadImageDataFromSourceClosure { + return await loadImageDataFromSourceClosure(source) } else { - return playerForReturnValue + return loadImageDataFromSourceReturnValue } } - //MARK: - playerState + //MARK: - loadImageRetryingOnReconnection - var playerStateForUnderlyingCallsCount = 0 - var playerStateForCallsCount: Int { + var loadImageRetryingOnReconnectionSizeUnderlyingCallsCount = 0 + var loadImageRetryingOnReconnectionSizeCallsCount: Int { get { if Thread.isMainThread { - return playerStateForUnderlyingCallsCount + return loadImageRetryingOnReconnectionSizeUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = playerStateForUnderlyingCallsCount + returnValue = loadImageRetryingOnReconnectionSizeUnderlyingCallsCount } return returnValue! @@ -9799,29 +10613,29 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - playerStateForUnderlyingCallsCount = newValue + loadImageRetryingOnReconnectionSizeUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - playerStateForUnderlyingCallsCount = newValue + loadImageRetryingOnReconnectionSizeUnderlyingCallsCount = newValue } } } } - var playerStateForCalled: Bool { - return playerStateForCallsCount > 0 + var loadImageRetryingOnReconnectionSizeCalled: Bool { + return loadImageRetryingOnReconnectionSizeCallsCount > 0 } - var playerStateForReceivedId: AudioPlayerStateIdentifier? - var playerStateForReceivedInvocations: [AudioPlayerStateIdentifier] = [] + var loadImageRetryingOnReconnectionSizeReceivedArguments: (source: MediaSourceProxy, size: CGSize?)? + var loadImageRetryingOnReconnectionSizeReceivedInvocations: [(source: MediaSourceProxy, size: CGSize?)] = [] - var playerStateForUnderlyingReturnValue: AudioPlayerState? - var playerStateForReturnValue: AudioPlayerState? { + var loadImageRetryingOnReconnectionSizeUnderlyingReturnValue: Task! + var loadImageRetryingOnReconnectionSizeReturnValue: Task! { get { if Thread.isMainThread { - return playerStateForUnderlyingReturnValue + return loadImageRetryingOnReconnectionSizeUnderlyingReturnValue } else { - var returnValue: AudioPlayerState?? = nil + var returnValue: Task? = nil DispatchQueue.main.sync { - returnValue = playerStateForUnderlyingReturnValue + returnValue = loadImageRetryingOnReconnectionSizeUnderlyingReturnValue } return returnValue! @@ -9829,39 +10643,39 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - playerStateForUnderlyingReturnValue = newValue + loadImageRetryingOnReconnectionSizeUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - playerStateForUnderlyingReturnValue = newValue + loadImageRetryingOnReconnectionSizeUnderlyingReturnValue = newValue } } } } - var playerStateForClosure: ((AudioPlayerStateIdentifier) -> AudioPlayerState?)? + var loadImageRetryingOnReconnectionSizeClosure: ((MediaSourceProxy, CGSize?) -> Task)? - func playerState(for id: AudioPlayerStateIdentifier) -> AudioPlayerState? { - playerStateForCallsCount += 1 - playerStateForReceivedId = id + func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task { + loadImageRetryingOnReconnectionSizeCallsCount += 1 + loadImageRetryingOnReconnectionSizeReceivedArguments = (source: source, size: size) DispatchQueue.main.async { - self.playerStateForReceivedInvocations.append(id) + self.loadImageRetryingOnReconnectionSizeReceivedInvocations.append((source: source, size: size)) } - if let playerStateForClosure = playerStateForClosure { - return playerStateForClosure(id) + if let loadImageRetryingOnReconnectionSizeClosure = loadImageRetryingOnReconnectionSizeClosure { + return loadImageRetryingOnReconnectionSizeClosure(source, size) } else { - return playerStateForReturnValue + return loadImageRetryingOnReconnectionSizeReturnValue } } - //MARK: - register + //MARK: - loadThumbnailForSource - var registerAudioPlayerStateUnderlyingCallsCount = 0 - var registerAudioPlayerStateCallsCount: Int { + var loadThumbnailForSourceSourceSizeUnderlyingCallsCount = 0 + var loadThumbnailForSourceSourceSizeCallsCount: Int { get { if Thread.isMainThread { - return registerAudioPlayerStateUnderlyingCallsCount + return loadThumbnailForSourceSourceSizeUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = registerAudioPlayerStateUnderlyingCallsCount + returnValue = loadThumbnailForSourceSourceSizeUnderlyingCallsCount } return returnValue! @@ -9869,40 +10683,29 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - registerAudioPlayerStateUnderlyingCallsCount = newValue + loadThumbnailForSourceSourceSizeUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - registerAudioPlayerStateUnderlyingCallsCount = newValue + loadThumbnailForSourceSourceSizeUnderlyingCallsCount = newValue } } } } - var registerAudioPlayerStateCalled: Bool { - return registerAudioPlayerStateCallsCount > 0 - } - var registerAudioPlayerStateReceivedAudioPlayerState: AudioPlayerState? - var registerAudioPlayerStateReceivedInvocations: [AudioPlayerState] = [] - var registerAudioPlayerStateClosure: ((AudioPlayerState) -> Void)? - - func register(audioPlayerState: AudioPlayerState) { - registerAudioPlayerStateCallsCount += 1 - registerAudioPlayerStateReceivedAudioPlayerState = audioPlayerState - DispatchQueue.main.async { - self.registerAudioPlayerStateReceivedInvocations.append(audioPlayerState) - } - registerAudioPlayerStateClosure?(audioPlayerState) + var loadThumbnailForSourceSourceSizeCalled: Bool { + return loadThumbnailForSourceSourceSizeCallsCount > 0 } - //MARK: - unregister + var loadThumbnailForSourceSourceSizeReceivedArguments: (source: MediaSourceProxy, size: CGSize)? + var loadThumbnailForSourceSourceSizeReceivedInvocations: [(source: MediaSourceProxy, size: CGSize)] = [] - var unregisterAudioPlayerStateUnderlyingCallsCount = 0 - var unregisterAudioPlayerStateCallsCount: Int { + var loadThumbnailForSourceSourceSizeUnderlyingReturnValue: Result! + var loadThumbnailForSourceSourceSizeReturnValue: Result! { get { if Thread.isMainThread { - return unregisterAudioPlayerStateUnderlyingCallsCount + return loadThumbnailForSourceSourceSizeUnderlyingReturnValue } else { - var returnValue: Int? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = unregisterAudioPlayerStateUnderlyingCallsCount + returnValue = loadThumbnailForSourceSourceSizeUnderlyingReturnValue } return returnValue! @@ -9910,40 +10713,39 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - unregisterAudioPlayerStateUnderlyingCallsCount = newValue + loadThumbnailForSourceSourceSizeUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - unregisterAudioPlayerStateUnderlyingCallsCount = newValue + loadThumbnailForSourceSourceSizeUnderlyingReturnValue = newValue } } } } - var unregisterAudioPlayerStateCalled: Bool { - return unregisterAudioPlayerStateCallsCount > 0 - } - var unregisterAudioPlayerStateReceivedAudioPlayerState: AudioPlayerState? - var unregisterAudioPlayerStateReceivedInvocations: [AudioPlayerState] = [] - var unregisterAudioPlayerStateClosure: ((AudioPlayerState) -> Void)? + var loadThumbnailForSourceSourceSizeClosure: ((MediaSourceProxy, CGSize) async -> Result)? - func unregister(audioPlayerState: AudioPlayerState) { - unregisterAudioPlayerStateCallsCount += 1 - unregisterAudioPlayerStateReceivedAudioPlayerState = audioPlayerState + func loadThumbnailForSource(source: MediaSourceProxy, size: CGSize) async -> Result { + loadThumbnailForSourceSourceSizeCallsCount += 1 + loadThumbnailForSourceSourceSizeReceivedArguments = (source: source, size: size) DispatchQueue.main.async { - self.unregisterAudioPlayerStateReceivedInvocations.append(audioPlayerState) + self.loadThumbnailForSourceSourceSizeReceivedInvocations.append((source: source, size: size)) + } + if let loadThumbnailForSourceSourceSizeClosure = loadThumbnailForSourceSourceSizeClosure { + return await loadThumbnailForSourceSourceSizeClosure(source, size) + } else { + return loadThumbnailForSourceSourceSizeReturnValue } - unregisterAudioPlayerStateClosure?(audioPlayerState) } - //MARK: - detachAllStates + //MARK: - loadFileFromSource - var detachAllStatesExceptUnderlyingCallsCount = 0 - var detachAllStatesExceptCallsCount: Int { + var loadFileFromSourceFilenameUnderlyingCallsCount = 0 + var loadFileFromSourceFilenameCallsCount: Int { get { if Thread.isMainThread { - return detachAllStatesExceptUnderlyingCallsCount + return loadFileFromSourceFilenameUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = detachAllStatesExceptUnderlyingCallsCount + returnValue = loadFileFromSourceFilenameUnderlyingCallsCount } return returnValue! @@ -9951,28 +10753,57 @@ class MediaPlayerProviderMock: MediaPlayerProviderProtocol { } set { if Thread.isMainThread { - detachAllStatesExceptUnderlyingCallsCount = newValue + loadFileFromSourceFilenameUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - detachAllStatesExceptUnderlyingCallsCount = newValue + loadFileFromSourceFilenameUnderlyingCallsCount = newValue } } } } - var detachAllStatesExceptCalled: Bool { - return detachAllStatesExceptCallsCount > 0 + var loadFileFromSourceFilenameCalled: Bool { + return loadFileFromSourceFilenameCallsCount > 0 } - var detachAllStatesExceptReceivedException: AudioPlayerState? - var detachAllStatesExceptReceivedInvocations: [AudioPlayerState?] = [] - var detachAllStatesExceptClosure: ((AudioPlayerState?) async -> Void)? + var loadFileFromSourceFilenameReceivedArguments: (source: MediaSourceProxy, filename: String?)? + var loadFileFromSourceFilenameReceivedInvocations: [(source: MediaSourceProxy, filename: String?)] = [] - func detachAllStates(except exception: AudioPlayerState?) async { - detachAllStatesExceptCallsCount += 1 - detachAllStatesExceptReceivedException = exception + var loadFileFromSourceFilenameUnderlyingReturnValue: Result! + var loadFileFromSourceFilenameReturnValue: Result! { + get { + if Thread.isMainThread { + return loadFileFromSourceFilenameUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = loadFileFromSourceFilenameUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loadFileFromSourceFilenameUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + loadFileFromSourceFilenameUnderlyingReturnValue = newValue + } + } + } + } + var loadFileFromSourceFilenameClosure: ((MediaSourceProxy, String?) async -> Result)? + + func loadFileFromSource(_ source: MediaSourceProxy, filename: String?) async -> Result { + loadFileFromSourceFilenameCallsCount += 1 + loadFileFromSourceFilenameReceivedArguments = (source: source, filename: filename) DispatchQueue.main.async { - self.detachAllStatesExceptReceivedInvocations.append(exception) + self.loadFileFromSourceFilenameReceivedInvocations.append((source: source, filename: filename)) + } + if let loadFileFromSourceFilenameClosure = loadFileFromSourceFilenameClosure { + return await loadFileFromSourceFilenameClosure(source, filename) + } else { + return loadFileFromSourceFilenameReturnValue } - await detachAllStatesExceptClosure?(exception) } } class NetworkMonitorMock: NetworkMonitorProtocol { @@ -11848,6 +12679,7 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol { } var underlyingUserID: String! var displayName: String? + var disambiguatedDisplayName: String? var avatarURL: URL? var membership: MembershipState { get { return underlyingMembership } @@ -11890,45 +12722,11 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingId = value } } var underlyingId: String! - var canonicalAlias: String? var ownUserID: String { get { return underlyingOwnUserID } set(value) { underlyingOwnUserID = value } } var underlyingOwnUserID: String! - var name: String? - var topic: String? - var avatar: RoomAvatar { - get { return underlyingAvatar } - set(value) { underlyingAvatar = value } - } - var underlyingAvatar: RoomAvatar! - var avatarURL: URL? - var isPublic: Bool { - get { return underlyingIsPublic } - set(value) { underlyingIsPublic = value } - } - var underlyingIsPublic: Bool! - var isDirect: Bool { - get { return underlyingIsDirect } - set(value) { underlyingIsDirect = value } - } - var underlyingIsDirect: Bool! - var isSpace: Bool { - get { return underlyingIsSpace } - set(value) { underlyingIsSpace = value } - } - var underlyingIsSpace: Bool! - var joinedMembersCount: Int { - get { return underlyingJoinedMembersCount } - set(value) { underlyingJoinedMembersCount = value } - } - var underlyingJoinedMembersCount: Int! - var activeMembersCount: Int { - get { return underlyingActiveMembersCount } - set(value) { underlyingActiveMembersCount = value } - } - var underlyingActiveMembersCount: Int! } class RoomSummaryProviderMock: RoomSummaryProviderProtocol { @@ -12412,7 +13210,141 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = generateRecoveryKeyUnderlyingReturnValue + returnValue = generateRecoveryKeyUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + generateRecoveryKeyUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + generateRecoveryKeyUnderlyingReturnValue = newValue + } + } + } + } + var generateRecoveryKeyClosure: (() async -> Result)? + + func generateRecoveryKey() async -> Result { + generateRecoveryKeyCallsCount += 1 + if let generateRecoveryKeyClosure = generateRecoveryKeyClosure { + return await generateRecoveryKeyClosure() + } else { + return generateRecoveryKeyReturnValue + } + } + //MARK: - confirmRecoveryKey + + var confirmRecoveryKeyUnderlyingCallsCount = 0 + var confirmRecoveryKeyCallsCount: Int { + get { + if Thread.isMainThread { + return confirmRecoveryKeyUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = confirmRecoveryKeyUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + confirmRecoveryKeyUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + confirmRecoveryKeyUnderlyingCallsCount = newValue + } + } + } + } + var confirmRecoveryKeyCalled: Bool { + return confirmRecoveryKeyCallsCount > 0 + } + var confirmRecoveryKeyReceivedKey: String? + var confirmRecoveryKeyReceivedInvocations: [String] = [] + + var confirmRecoveryKeyUnderlyingReturnValue: Result! + var confirmRecoveryKeyReturnValue: Result! { + get { + if Thread.isMainThread { + return confirmRecoveryKeyUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = confirmRecoveryKeyUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + confirmRecoveryKeyUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + confirmRecoveryKeyUnderlyingReturnValue = newValue + } + } + } + } + var confirmRecoveryKeyClosure: ((String) async -> Result)? + + func confirmRecoveryKey(_ key: String) async -> Result { + confirmRecoveryKeyCallsCount += 1 + confirmRecoveryKeyReceivedKey = key + DispatchQueue.main.async { + self.confirmRecoveryKeyReceivedInvocations.append(key) + } + if let confirmRecoveryKeyClosure = confirmRecoveryKeyClosure { + return await confirmRecoveryKeyClosure(key) + } else { + return confirmRecoveryKeyReturnValue + } + } + //MARK: - waitForKeyBackupUpload + + var waitForKeyBackupUploadUnderlyingCallsCount = 0 + var waitForKeyBackupUploadCallsCount: Int { + get { + if Thread.isMainThread { + return waitForKeyBackupUploadUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = waitForKeyBackupUploadUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + waitForKeyBackupUploadUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + waitForKeyBackupUploadUnderlyingCallsCount = newValue + } + } + } + } + var waitForKeyBackupUploadCalled: Bool { + return waitForKeyBackupUploadCallsCount > 0 + } + + var waitForKeyBackupUploadUnderlyingReturnValue: Result! + var waitForKeyBackupUploadReturnValue: Result! { + get { + if Thread.isMainThread { + return waitForKeyBackupUploadUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = waitForKeyBackupUploadUnderlyingReturnValue } return returnValue! @@ -12420,35 +13352,43 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } set { if Thread.isMainThread { - generateRecoveryKeyUnderlyingReturnValue = newValue + waitForKeyBackupUploadUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - generateRecoveryKeyUnderlyingReturnValue = newValue + waitForKeyBackupUploadUnderlyingReturnValue = newValue } } } } - var generateRecoveryKeyClosure: (() async -> Result)? + var waitForKeyBackupUploadClosure: (() async -> Result)? - func generateRecoveryKey() async -> Result { - generateRecoveryKeyCallsCount += 1 - if let generateRecoveryKeyClosure = generateRecoveryKeyClosure { - return await generateRecoveryKeyClosure() + func waitForKeyBackupUpload() async -> Result { + waitForKeyBackupUploadCallsCount += 1 + if let waitForKeyBackupUploadClosure = waitForKeyBackupUploadClosure { + return await waitForKeyBackupUploadClosure() } else { - return generateRecoveryKeyReturnValue + return waitForKeyBackupUploadReturnValue } } - //MARK: - confirmRecoveryKey +} +class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { + var actions: PassthroughSubject { + get { return underlyingActions } + set(value) { underlyingActions = value } + } + var underlyingActions: PassthroughSubject! - var confirmRecoveryKeyUnderlyingCallsCount = 0 - var confirmRecoveryKeyCallsCount: Int { + //MARK: - acknowledgeVerificationRequest + + var acknowledgeVerificationRequestDetailsUnderlyingCallsCount = 0 + var acknowledgeVerificationRequestDetailsCallsCount: Int { get { if Thread.isMainThread { - return confirmRecoveryKeyUnderlyingCallsCount + return acknowledgeVerificationRequestDetailsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = confirmRecoveryKeyUnderlyingCallsCount + returnValue = acknowledgeVerificationRequestDetailsUnderlyingCallsCount } return returnValue! @@ -12456,29 +13396,29 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } set { if Thread.isMainThread { - confirmRecoveryKeyUnderlyingCallsCount = newValue + acknowledgeVerificationRequestDetailsUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - confirmRecoveryKeyUnderlyingCallsCount = newValue + acknowledgeVerificationRequestDetailsUnderlyingCallsCount = newValue } } } } - var confirmRecoveryKeyCalled: Bool { - return confirmRecoveryKeyCallsCount > 0 + var acknowledgeVerificationRequestDetailsCalled: Bool { + return acknowledgeVerificationRequestDetailsCallsCount > 0 } - var confirmRecoveryKeyReceivedKey: String? - var confirmRecoveryKeyReceivedInvocations: [String] = [] + var acknowledgeVerificationRequestDetailsReceivedDetails: SessionVerificationRequestDetails? + var acknowledgeVerificationRequestDetailsReceivedInvocations: [SessionVerificationRequestDetails] = [] - var confirmRecoveryKeyUnderlyingReturnValue: Result! - var confirmRecoveryKeyReturnValue: Result! { + var acknowledgeVerificationRequestDetailsUnderlyingReturnValue: Result! + var acknowledgeVerificationRequestDetailsReturnValue: Result! { get { if Thread.isMainThread { - return confirmRecoveryKeyUnderlyingReturnValue + return acknowledgeVerificationRequestDetailsUnderlyingReturnValue } else { - var returnValue: Result? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = confirmRecoveryKeyUnderlyingReturnValue + returnValue = acknowledgeVerificationRequestDetailsUnderlyingReturnValue } return returnValue! @@ -12486,39 +13426,39 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } set { if Thread.isMainThread { - confirmRecoveryKeyUnderlyingReturnValue = newValue + acknowledgeVerificationRequestDetailsUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - confirmRecoveryKeyUnderlyingReturnValue = newValue + acknowledgeVerificationRequestDetailsUnderlyingReturnValue = newValue } } } } - var confirmRecoveryKeyClosure: ((String) async -> Result)? + var acknowledgeVerificationRequestDetailsClosure: ((SessionVerificationRequestDetails) async -> Result)? - func confirmRecoveryKey(_ key: String) async -> Result { - confirmRecoveryKeyCallsCount += 1 - confirmRecoveryKeyReceivedKey = key + func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result { + acknowledgeVerificationRequestDetailsCallsCount += 1 + acknowledgeVerificationRequestDetailsReceivedDetails = details DispatchQueue.main.async { - self.confirmRecoveryKeyReceivedInvocations.append(key) + self.acknowledgeVerificationRequestDetailsReceivedInvocations.append(details) } - if let confirmRecoveryKeyClosure = confirmRecoveryKeyClosure { - return await confirmRecoveryKeyClosure(key) + if let acknowledgeVerificationRequestDetailsClosure = acknowledgeVerificationRequestDetailsClosure { + return await acknowledgeVerificationRequestDetailsClosure(details) } else { - return confirmRecoveryKeyReturnValue + return acknowledgeVerificationRequestDetailsReturnValue } } - //MARK: - waitForKeyBackupUpload + //MARK: - acceptVerificationRequest - var waitForKeyBackupUploadUnderlyingCallsCount = 0 - var waitForKeyBackupUploadCallsCount: Int { + var acceptVerificationRequestUnderlyingCallsCount = 0 + var acceptVerificationRequestCallsCount: Int { get { if Thread.isMainThread { - return waitForKeyBackupUploadUnderlyingCallsCount + return acceptVerificationRequestUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = waitForKeyBackupUploadUnderlyingCallsCount + returnValue = acceptVerificationRequestUnderlyingCallsCount } return returnValue! @@ -12526,27 +13466,27 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } set { if Thread.isMainThread { - waitForKeyBackupUploadUnderlyingCallsCount = newValue + acceptVerificationRequestUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - waitForKeyBackupUploadUnderlyingCallsCount = newValue + acceptVerificationRequestUnderlyingCallsCount = newValue } } } } - var waitForKeyBackupUploadCalled: Bool { - return waitForKeyBackupUploadCallsCount > 0 + var acceptVerificationRequestCalled: Bool { + return acceptVerificationRequestCallsCount > 0 } - var waitForKeyBackupUploadUnderlyingReturnValue: Result! - var waitForKeyBackupUploadReturnValue: Result! { + var acceptVerificationRequestUnderlyingReturnValue: Result! + var acceptVerificationRequestReturnValue: Result! { get { if Thread.isMainThread { - return waitForKeyBackupUploadUnderlyingReturnValue + return acceptVerificationRequestUnderlyingReturnValue } else { - var returnValue: Result? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = waitForKeyBackupUploadUnderlyingReturnValue + returnValue = acceptVerificationRequestUnderlyingReturnValue } return returnValue! @@ -12554,32 +13494,24 @@ class SecureBackupControllerMock: SecureBackupControllerProtocol { } set { if Thread.isMainThread { - waitForKeyBackupUploadUnderlyingReturnValue = newValue + acceptVerificationRequestUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - waitForKeyBackupUploadUnderlyingReturnValue = newValue + acceptVerificationRequestUnderlyingReturnValue = newValue } } } } - var waitForKeyBackupUploadClosure: (() async -> Result)? + var acceptVerificationRequestClosure: (() async -> Result)? - func waitForKeyBackupUpload() async -> Result { - waitForKeyBackupUploadCallsCount += 1 - if let waitForKeyBackupUploadClosure = waitForKeyBackupUploadClosure { - return await waitForKeyBackupUploadClosure() + func acceptVerificationRequest() async -> Result { + acceptVerificationRequestCallsCount += 1 + if let acceptVerificationRequestClosure = acceptVerificationRequestClosure { + return await acceptVerificationRequestClosure() } else { - return waitForKeyBackupUploadReturnValue + return acceptVerificationRequestReturnValue } } -} -class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { - var callbacks: PassthroughSubject { - get { return underlyingCallbacks } - set(value) { underlyingCallbacks = value } - } - var underlyingCallbacks: PassthroughSubject! - //MARK: - requestVerification var requestVerificationUnderlyingCallsCount = 0 @@ -13264,8 +14196,8 @@ class TimelineProxyMock: TimelineProxyProtocol { var editNewContentCalled: Bool { return editNewContentCallsCount > 0 } - var editNewContentReceivedArguments: (timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)? - var editNewContentReceivedInvocations: [(timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)] = [] + var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)? + var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)] = [] var editNewContentUnderlyingReturnValue: Result! var editNewContentReturnValue: Result! { @@ -13291,16 +14223,16 @@ class TimelineProxyMock: TimelineProxyProtocol { } } } - var editNewContentClosure: ((EventTimelineItem, RoomMessageEventContentWithoutRelation) async -> Result)? + var editNewContentClosure: ((EventOrTransactionId, RoomMessageEventContentWithoutRelation) async -> Result)? - func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { editNewContentCallsCount += 1 - editNewContentReceivedArguments = (timelineItem: timelineItem, newContent: newContent) + editNewContentReceivedArguments = (eventOrTransactionID: eventOrTransactionID, newContent: newContent) DispatchQueue.main.async { - self.editNewContentReceivedInvocations.append((timelineItem: timelineItem, newContent: newContent)) + self.editNewContentReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, newContent: newContent)) } if let editNewContentClosure = editNewContentClosure { - return await editNewContentClosure(timelineItem, newContent) + return await editNewContentClosure(eventOrTransactionID, newContent) } else { return editNewContentReturnValue } @@ -13334,8 +14266,8 @@ class TimelineProxyMock: TimelineProxyProtocol { var redactReasonCalled: Bool { return redactReasonCallsCount > 0 } - var redactReasonReceivedArguments: (timelineItemID: TimelineItemIdentifier, reason: String?)? - var redactReasonReceivedInvocations: [(timelineItemID: TimelineItemIdentifier, reason: String?)] = [] + var redactReasonReceivedArguments: (eventOrTransactionID: EventOrTransactionId, reason: String?)? + var redactReasonReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, reason: String?)] = [] var redactReasonUnderlyingReturnValue: Result! var redactReasonReturnValue: Result! { @@ -13361,16 +14293,16 @@ class TimelineProxyMock: TimelineProxyProtocol { } } } - var redactReasonClosure: ((TimelineItemIdentifier, String?) async -> Result)? + var redactReasonClosure: ((EventOrTransactionId, String?) async -> Result)? - func redact(_ timelineItemID: TimelineItemIdentifier, reason: String?) async -> Result { + func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result { redactReasonCallsCount += 1 - redactReasonReceivedArguments = (timelineItemID: timelineItemID, reason: reason) + redactReasonReceivedArguments = (eventOrTransactionID: eventOrTransactionID, reason: reason) DispatchQueue.main.async { - self.redactReasonReceivedInvocations.append((timelineItemID: timelineItemID, reason: reason)) + self.redactReasonReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, reason: reason)) } if let redactReasonClosure = redactReasonClosure { - return await redactReasonClosure(timelineItemID, reason) + return await redactReasonClosure(eventOrTransactionID, reason) } else { return redactReasonReturnValue } @@ -14047,15 +14979,15 @@ class TimelineProxyMock: TimelineProxyProtocol { } //MARK: - sendMessage - var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = 0 - var sendMessageHtmlInReplyToIntentionalMentionsCallsCount: Int { + var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = 0 + var sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount: Int { get { if Thread.isMainThread { - return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount + return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount + returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount } return returnValue! @@ -14063,29 +14995,29 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue + sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue + sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue } } } } - var sendMessageHtmlInReplyToIntentionalMentionsCalled: Bool { - return sendMessageHtmlInReplyToIntentionalMentionsCallsCount > 0 + var sendMessageHtmlInReplyToEventIDIntentionalMentionsCalled: Bool { + return sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount > 0 } - var sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)? - var sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)] = [] + var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments: (message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)? + var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedInvocations: [(message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)] = [] - var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue: Result! - var sendMessageHtmlInReplyToIntentionalMentionsReturnValue: Result! { + var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue: Result! + var sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue: Result! { get { if Thread.isMainThread { - return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue + return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue + returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue } return returnValue! @@ -14093,26 +15025,26 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue = newValue + sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue = newValue + sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue = newValue } } } } - var sendMessageHtmlInReplyToIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result)? + var sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result)? - func sendMessage(_ message: String, html: String?, inReplyTo eventID: String?, intentionalMentions: IntentionalMentions) async -> Result { - sendMessageHtmlInReplyToIntentionalMentionsCallsCount += 1 - sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions) + func sendMessage(_ message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async -> Result { + sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount += 1 + sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments = (message: message, html: html, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions) DispatchQueue.main.async { - self.sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations.append((message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) + self.sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedInvocations.append((message: message, html: html, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions)) } - if let sendMessageHtmlInReplyToIntentionalMentionsClosure = sendMessageHtmlInReplyToIntentionalMentionsClosure { - return await sendMessageHtmlInReplyToIntentionalMentionsClosure(message, html, eventID, intentionalMentions) + if let sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure = sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure { + return await sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure(message, html, inReplyToEventID, intentionalMentions) } else { - return sendMessageHtmlInReplyToIntentionalMentionsReturnValue + return sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue } } //MARK: - toggleReaction @@ -14144,8 +15076,8 @@ class TimelineProxyMock: TimelineProxyProtocol { var toggleReactionToCalled: Bool { return toggleReactionToCallsCount > 0 } - var toggleReactionToReceivedArguments: (reaction: String, itemID: TimelineItemIdentifier)? - var toggleReactionToReceivedInvocations: [(reaction: String, itemID: TimelineItemIdentifier)] = [] + var toggleReactionToReceivedArguments: (reaction: String, eventID: EventOrTransactionId)? + var toggleReactionToReceivedInvocations: [(reaction: String, eventID: EventOrTransactionId)] = [] var toggleReactionToUnderlyingReturnValue: Result! var toggleReactionToReturnValue: Result! { @@ -14171,16 +15103,16 @@ class TimelineProxyMock: TimelineProxyProtocol { } } } - var toggleReactionToClosure: ((String, TimelineItemIdentifier) async -> Result)? + var toggleReactionToClosure: ((String, EventOrTransactionId) async -> Result)? - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result { + func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async -> Result { toggleReactionToCallsCount += 1 - toggleReactionToReceivedArguments = (reaction: reaction, itemID: itemID) + toggleReactionToReceivedArguments = (reaction: reaction, eventID: eventID) DispatchQueue.main.async { - self.toggleReactionToReceivedInvocations.append((reaction: reaction, itemID: itemID)) + self.toggleReactionToReceivedInvocations.append((reaction: reaction, eventID: eventID)) } if let toggleReactionToClosure = toggleReactionToClosure { - return await toggleReactionToClosure(reaction, itemID) + return await toggleReactionToClosure(reaction, eventID) } else { return toggleReactionToReturnValue } @@ -15296,6 +16228,271 @@ class UserSessionMock: UserSessionProtocol { var underlyingCallbacks: PassthroughSubject! } +class UserSessionStoreMock: UserSessionStoreProtocol { + var hasSessions: Bool { + get { return underlyingHasSessions } + set(value) { underlyingHasSessions = value } + } + var underlyingHasSessions: Bool! + var userIDs: [String] = [] + var clientSessionDelegate: ClientSessionDelegate { + get { return underlyingClientSessionDelegate } + set(value) { underlyingClientSessionDelegate = value } + } + var underlyingClientSessionDelegate: ClientSessionDelegate! + + //MARK: - reset + + var resetUnderlyingCallsCount = 0 + var resetCallsCount: Int { + get { + if Thread.isMainThread { + return resetUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = resetUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + resetUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + resetUnderlyingCallsCount = newValue + } + } + } + } + var resetCalled: Bool { + return resetCallsCount > 0 + } + var resetClosure: (() -> Void)? + + func reset() { + resetCallsCount += 1 + resetClosure?() + } + //MARK: - restoreUserSession + + var restoreUserSessionUnderlyingCallsCount = 0 + var restoreUserSessionCallsCount: Int { + get { + if Thread.isMainThread { + return restoreUserSessionUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = restoreUserSessionUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + restoreUserSessionUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + restoreUserSessionUnderlyingCallsCount = newValue + } + } + } + } + var restoreUserSessionCalled: Bool { + return restoreUserSessionCallsCount > 0 + } + + var restoreUserSessionUnderlyingReturnValue: Result! + var restoreUserSessionReturnValue: Result! { + get { + if Thread.isMainThread { + return restoreUserSessionUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = restoreUserSessionUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + restoreUserSessionUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + restoreUserSessionUnderlyingReturnValue = newValue + } + } + } + } + var restoreUserSessionClosure: (() async -> Result)? + + func restoreUserSession() async -> Result { + restoreUserSessionCallsCount += 1 + if let restoreUserSessionClosure = restoreUserSessionClosure { + return await restoreUserSessionClosure() + } else { + return restoreUserSessionReturnValue + } + } + //MARK: - userSession + + var userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = 0 + var userSessionForSessionDirectoriesPassphraseCallsCount: Int { + get { + if Thread.isMainThread { + return userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + userSessionForSessionDirectoriesPassphraseUnderlyingCallsCount = newValue + } + } + } + } + var userSessionForSessionDirectoriesPassphraseCalled: Bool { + return userSessionForSessionDirectoriesPassphraseCallsCount > 0 + } + var userSessionForSessionDirectoriesPassphraseReceivedArguments: (client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)? + var userSessionForSessionDirectoriesPassphraseReceivedInvocations: [(client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?)] = [] + + var userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue: Result! + var userSessionForSessionDirectoriesPassphraseReturnValue: Result! { + get { + if Thread.isMainThread { + return userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + userSessionForSessionDirectoriesPassphraseUnderlyingReturnValue = newValue + } + } + } + } + var userSessionForSessionDirectoriesPassphraseClosure: ((ClientProtocol, SessionDirectories, String?) async -> Result)? + + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { + userSessionForSessionDirectoriesPassphraseCallsCount += 1 + userSessionForSessionDirectoriesPassphraseReceivedArguments = (client: client, sessionDirectories: sessionDirectories, passphrase: passphrase) + DispatchQueue.main.async { + self.userSessionForSessionDirectoriesPassphraseReceivedInvocations.append((client: client, sessionDirectories: sessionDirectories, passphrase: passphrase)) + } + if let userSessionForSessionDirectoriesPassphraseClosure = userSessionForSessionDirectoriesPassphraseClosure { + return await userSessionForSessionDirectoriesPassphraseClosure(client, sessionDirectories, passphrase) + } else { + return userSessionForSessionDirectoriesPassphraseReturnValue + } + } + //MARK: - logout + + var logoutUserSessionUnderlyingCallsCount = 0 + var logoutUserSessionCallsCount: Int { + get { + if Thread.isMainThread { + return logoutUserSessionUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = logoutUserSessionUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + logoutUserSessionUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + logoutUserSessionUnderlyingCallsCount = newValue + } + } + } + } + var logoutUserSessionCalled: Bool { + return logoutUserSessionCallsCount > 0 + } + var logoutUserSessionReceivedUserSession: UserSessionProtocol? + var logoutUserSessionReceivedInvocations: [UserSessionProtocol] = [] + var logoutUserSessionClosure: ((UserSessionProtocol) -> Void)? + + func logout(userSession: UserSessionProtocol) { + logoutUserSessionCallsCount += 1 + logoutUserSessionReceivedUserSession = userSession + DispatchQueue.main.async { + self.logoutUserSessionReceivedInvocations.append(userSession) + } + logoutUserSessionClosure?(userSession) + } + //MARK: - clearCache + + var clearCacheForUnderlyingCallsCount = 0 + var clearCacheForCallsCount: Int { + get { + if Thread.isMainThread { + return clearCacheForUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = clearCacheForUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + clearCacheForUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + clearCacheForUnderlyingCallsCount = newValue + } + } + } + } + var clearCacheForCalled: Bool { + return clearCacheForCallsCount > 0 + } + var clearCacheForReceivedUserID: String? + var clearCacheForReceivedInvocations: [String] = [] + var clearCacheForClosure: ((String) -> Void)? + + func clearCache(for userID: String) { + clearCacheForCallsCount += 1 + clearCacheForReceivedUserID = userID + DispatchQueue.main.async { + self.clearCacheForReceivedInvocations.append(userID) + } + clearCacheForClosure?(userID) + } +} class VoiceMessageCacheMock: VoiceMessageCacheProtocol { var urlForRecording: URL { get { return underlyingUrlForRecording } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 152c0940c9..43a24c5c6f 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -15,17 +15,17 @@ open class ClientSDKMock: MatrixRustSDK.Client { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - abortOidcLogin + //MARK: - abortOidcAuth - var abortOidcLoginAuthorizationDataUnderlyingCallsCount = 0 - open var abortOidcLoginAuthorizationDataCallsCount: Int { + var abortOidcAuthAuthorizationDataUnderlyingCallsCount = 0 + open var abortOidcAuthAuthorizationDataCallsCount: Int { get { if Thread.isMainThread { - return abortOidcLoginAuthorizationDataUnderlyingCallsCount + return abortOidcAuthAuthorizationDataUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = abortOidcLoginAuthorizationDataUnderlyingCallsCount + returnValue = abortOidcAuthAuthorizationDataUnderlyingCallsCount } return returnValue! @@ -33,28 +33,28 @@ open class ClientSDKMock: MatrixRustSDK.Client { } set { if Thread.isMainThread { - abortOidcLoginAuthorizationDataUnderlyingCallsCount = newValue + abortOidcAuthAuthorizationDataUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - abortOidcLoginAuthorizationDataUnderlyingCallsCount = newValue + abortOidcAuthAuthorizationDataUnderlyingCallsCount = newValue } } } } - open var abortOidcLoginAuthorizationDataCalled: Bool { - return abortOidcLoginAuthorizationDataCallsCount > 0 + open var abortOidcAuthAuthorizationDataCalled: Bool { + return abortOidcAuthAuthorizationDataCallsCount > 0 } - open var abortOidcLoginAuthorizationDataReceivedAuthorizationData: OidcAuthorizationData? - open var abortOidcLoginAuthorizationDataReceivedInvocations: [OidcAuthorizationData] = [] - open var abortOidcLoginAuthorizationDataClosure: ((OidcAuthorizationData) async -> Void)? + open var abortOidcAuthAuthorizationDataReceivedAuthorizationData: OidcAuthorizationData? + open var abortOidcAuthAuthorizationDataReceivedInvocations: [OidcAuthorizationData] = [] + open var abortOidcAuthAuthorizationDataClosure: ((OidcAuthorizationData) async -> Void)? - open override func abortOidcLogin(authorizationData: OidcAuthorizationData) async { - abortOidcLoginAuthorizationDataCallsCount += 1 - abortOidcLoginAuthorizationDataReceivedAuthorizationData = authorizationData + open override func abortOidcAuth(authorizationData: OidcAuthorizationData) async { + abortOidcAuthAuthorizationDataCallsCount += 1 + abortOidcAuthAuthorizationDataReceivedAuthorizationData = authorizationData DispatchQueue.main.async { - self.abortOidcLoginAuthorizationDataReceivedInvocations.append(authorizationData) + self.abortOidcAuthAuthorizationDataReceivedInvocations.append(authorizationData) } - await abortOidcLoginAuthorizationDataClosure?(authorizationData) + await abortOidcAuthAuthorizationDataClosure?(authorizationData) } //MARK: - accountData @@ -625,6 +625,52 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - customLoginWithJwt + + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError: Error? + var customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = 0 + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount: Int { + get { + if Thread.isMainThread { + return customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue + } + } + } + } + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCalled: Bool { + return customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount > 0 + } + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments: (jwt: String, initialDeviceName: String?, deviceId: String?)? + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations: [(jwt: String, initialDeviceName: String?, deviceId: String?)] = [] + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure: ((String, String?, String?) async throws -> Void)? + + open override func customLoginWithJwt(jwt: String, initialDeviceName: String?, deviceId: String?) async throws { + if let error = customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError { + throw error + } + customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount += 1 + customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments = (jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId) + DispatchQueue.main.async { + self.customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations.append((jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId)) + } + try await customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure?(jwt, initialDeviceName, deviceId) + } + //MARK: - deactivateAccount open var deactivateAccountAuthDataEraseDataThrowableError: Error? @@ -1114,16 +1160,16 @@ open class ClientSDKMock: MatrixRustSDK.Client { //MARK: - getMediaFile - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirThrowableError: Error? - var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingCallsCount = 0 - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount: Int { + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirThrowableError: Error? + var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingCallsCount = 0 + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirCallsCount: Int { get { if Thread.isMainThread { - return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingCallsCount + return getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingCallsCount + returnValue = getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingCallsCount } return returnValue! @@ -1131,29 +1177,29 @@ open class ClientSDKMock: MatrixRustSDK.Client { } set { if Thread.isMainThread { - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingCallsCount = newValue + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingCallsCount = newValue + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingCallsCount = newValue } } } } - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCalled: Bool { - return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount > 0 + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirCalled: Bool { + return getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirCallsCount > 0 } - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedArguments: (mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?)? - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedInvocations: [(mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?)] = [] + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReceivedArguments: (mediaSource: MediaSource, filename: String?, mimeType: String, useCache: Bool, tempDir: String?)? + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReceivedInvocations: [(mediaSource: MediaSource, filename: String?, mimeType: String, useCache: Bool, tempDir: String?)] = [] - var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingReturnValue: MediaFileHandle! - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReturnValue: MediaFileHandle! { + var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingReturnValue: MediaFileHandle! + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReturnValue: MediaFileHandle! { get { if Thread.isMainThread { - return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingReturnValue + return getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingReturnValue } else { var returnValue: MediaFileHandle? = nil DispatchQueue.main.sync { - returnValue = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingReturnValue + returnValue = getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingReturnValue } return returnValue! @@ -1161,29 +1207,29 @@ open class ClientSDKMock: MatrixRustSDK.Client { } set { if Thread.isMainThread { - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingReturnValue = newValue + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirUnderlyingReturnValue = newValue + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirUnderlyingReturnValue = newValue } } } } - open var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure: ((MediaSource, String?, String, Bool, String?) async throws -> MediaFileHandle)? + open var getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirClosure: ((MediaSource, String?, String, Bool, String?) async throws -> MediaFileHandle)? - open override func getMediaFile(mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?) async throws -> MediaFileHandle { - if let error = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirThrowableError { + open override func getMediaFile(mediaSource: MediaSource, filename: String?, mimeType: String, useCache: Bool, tempDir: String?) async throws -> MediaFileHandle { + if let error = getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirThrowableError { throw error } - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount += 1 - getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedArguments = (mediaSource: mediaSource, body: body, mimeType: mimeType, useCache: useCache, tempDir: tempDir) + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirCallsCount += 1 + getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReceivedArguments = (mediaSource: mediaSource, filename: filename, mimeType: mimeType, useCache: useCache, tempDir: tempDir) DispatchQueue.main.async { - self.getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedInvocations.append((mediaSource: mediaSource, body: body, mimeType: mimeType, useCache: useCache, tempDir: tempDir)) + self.getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReceivedInvocations.append((mediaSource: mediaSource, filename: filename, mimeType: mimeType, useCache: useCache, tempDir: tempDir)) } - if let getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure { - return try await getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure(mediaSource, body, mimeType, useCache, tempDir) + if let getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirClosure = getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirClosure { + return try await getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirClosure(mediaSource, filename, mimeType, useCache, tempDir) } else { - return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReturnValue + return getMediaFileMediaSourceFilenameMimeTypeUseCacheTempDirReturnValue } } @@ -2010,6 +2056,81 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - isRoomAliasAvailable + + open var isRoomAliasAvailableAliasThrowableError: Error? + var isRoomAliasAvailableAliasUnderlyingCallsCount = 0 + open var isRoomAliasAvailableAliasCallsCount: Int { + get { + if Thread.isMainThread { + return isRoomAliasAvailableAliasUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = isRoomAliasAvailableAliasUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isRoomAliasAvailableAliasUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + isRoomAliasAvailableAliasUnderlyingCallsCount = newValue + } + } + } + } + open var isRoomAliasAvailableAliasCalled: Bool { + return isRoomAliasAvailableAliasCallsCount > 0 + } + open var isRoomAliasAvailableAliasReceivedAlias: String? + open var isRoomAliasAvailableAliasReceivedInvocations: [String] = [] + + var isRoomAliasAvailableAliasUnderlyingReturnValue: Bool! + open var isRoomAliasAvailableAliasReturnValue: Bool! { + get { + if Thread.isMainThread { + return isRoomAliasAvailableAliasUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = isRoomAliasAvailableAliasUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isRoomAliasAvailableAliasUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + isRoomAliasAvailableAliasUnderlyingReturnValue = newValue + } + } + } + } + open var isRoomAliasAvailableAliasClosure: ((String) async throws -> Bool)? + + open override func isRoomAliasAvailable(alias: String) async throws -> Bool { + if let error = isRoomAliasAvailableAliasThrowableError { + throw error + } + isRoomAliasAvailableAliasCallsCount += 1 + isRoomAliasAvailableAliasReceivedAlias = alias + DispatchQueue.main.async { + self.isRoomAliasAvailableAliasReceivedInvocations.append(alias) + } + if let isRoomAliasAvailableAliasClosure = isRoomAliasAvailableAliasClosure { + return try await isRoomAliasAvailableAliasClosure(alias) + } else { + return isRoomAliasAvailableAliasReturnValue + } + } + //MARK: - joinRoomById open var joinRoomByIdRoomIdThrowableError: Error? @@ -2160,6 +2281,81 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - knock + + open var knockRoomIdOrAliasReasonServerNamesThrowableError: Error? + var knockRoomIdOrAliasReasonServerNamesUnderlyingCallsCount = 0 + open var knockRoomIdOrAliasReasonServerNamesCallsCount: Int { + get { + if Thread.isMainThread { + return knockRoomIdOrAliasReasonServerNamesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = knockRoomIdOrAliasReasonServerNamesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomIdOrAliasReasonServerNamesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + knockRoomIdOrAliasReasonServerNamesUnderlyingCallsCount = newValue + } + } + } + } + open var knockRoomIdOrAliasReasonServerNamesCalled: Bool { + return knockRoomIdOrAliasReasonServerNamesCallsCount > 0 + } + open var knockRoomIdOrAliasReasonServerNamesReceivedArguments: (roomIdOrAlias: String, reason: String?, serverNames: [String])? + open var knockRoomIdOrAliasReasonServerNamesReceivedInvocations: [(roomIdOrAlias: String, reason: String?, serverNames: [String])] = [] + + var knockRoomIdOrAliasReasonServerNamesUnderlyingReturnValue: Room! + open var knockRoomIdOrAliasReasonServerNamesReturnValue: Room! { + get { + if Thread.isMainThread { + return knockRoomIdOrAliasReasonServerNamesUnderlyingReturnValue + } else { + var returnValue: Room? = nil + DispatchQueue.main.sync { + returnValue = knockRoomIdOrAliasReasonServerNamesUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + knockRoomIdOrAliasReasonServerNamesUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + knockRoomIdOrAliasReasonServerNamesUnderlyingReturnValue = newValue + } + } + } + } + open var knockRoomIdOrAliasReasonServerNamesClosure: ((String, String?, [String]) async throws -> Room)? + + open override func knock(roomIdOrAlias: String, reason: String?, serverNames: [String]) async throws -> Room { + if let error = knockRoomIdOrAliasReasonServerNamesThrowableError { + throw error + } + knockRoomIdOrAliasReasonServerNamesCallsCount += 1 + knockRoomIdOrAliasReasonServerNamesReceivedArguments = (roomIdOrAlias: roomIdOrAlias, reason: reason, serverNames: serverNames) + DispatchQueue.main.async { + self.knockRoomIdOrAliasReasonServerNamesReceivedInvocations.append((roomIdOrAlias: roomIdOrAlias, reason: reason, serverNames: serverNames)) + } + if let knockRoomIdOrAliasReasonServerNamesClosure = knockRoomIdOrAliasReasonServerNamesClosure { + return try await knockRoomIdOrAliasReasonServerNamesClosure(roomIdOrAlias, reason, serverNames) + } else { + return knockRoomIdOrAliasReasonServerNamesReturnValue + } + } + //MARK: - login open var loginUsernamePasswordInitialDeviceNameDeviceIdThrowableError: Error? @@ -2555,13 +2751,13 @@ open class ClientSDKMock: MatrixRustSDK.Client { open var resolveRoomAliasRoomAliasReceivedRoomAlias: String? open var resolveRoomAliasRoomAliasReceivedInvocations: [String] = [] - var resolveRoomAliasRoomAliasUnderlyingReturnValue: ResolvedRoomAlias! - open var resolveRoomAliasRoomAliasReturnValue: ResolvedRoomAlias! { + var resolveRoomAliasRoomAliasUnderlyingReturnValue: ResolvedRoomAlias? + open var resolveRoomAliasRoomAliasReturnValue: ResolvedRoomAlias? { get { if Thread.isMainThread { return resolveRoomAliasRoomAliasUnderlyingReturnValue } else { - var returnValue: ResolvedRoomAlias? = nil + var returnValue: ResolvedRoomAlias?? = nil DispatchQueue.main.sync { returnValue = resolveRoomAliasRoomAliasUnderlyingReturnValue } @@ -2579,9 +2775,9 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } } - open var resolveRoomAliasRoomAliasClosure: ((String) async throws -> ResolvedRoomAlias)? + open var resolveRoomAliasRoomAliasClosure: ((String) async throws -> ResolvedRoomAlias?)? - open override func resolveRoomAlias(roomAlias: String) async throws -> ResolvedRoomAlias { + open override func resolveRoomAlias(roomAlias: String) async throws -> ResolvedRoomAlias? { if let error = resolveRoomAliasRoomAliasThrowableError { throw error } @@ -2643,6 +2839,81 @@ open class ClientSDKMock: MatrixRustSDK.Client { try await restoreSessionSessionClosure?(session) } + //MARK: - roomAliasExists + + open var roomAliasExistsRoomAliasThrowableError: Error? + var roomAliasExistsRoomAliasUnderlyingCallsCount = 0 + open var roomAliasExistsRoomAliasCallsCount: Int { + get { + if Thread.isMainThread { + return roomAliasExistsRoomAliasUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = roomAliasExistsRoomAliasUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomAliasExistsRoomAliasUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + roomAliasExistsRoomAliasUnderlyingCallsCount = newValue + } + } + } + } + open var roomAliasExistsRoomAliasCalled: Bool { + return roomAliasExistsRoomAliasCallsCount > 0 + } + open var roomAliasExistsRoomAliasReceivedRoomAlias: String? + open var roomAliasExistsRoomAliasReceivedInvocations: [String] = [] + + var roomAliasExistsRoomAliasUnderlyingReturnValue: Bool! + open var roomAliasExistsRoomAliasReturnValue: Bool! { + get { + if Thread.isMainThread { + return roomAliasExistsRoomAliasUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = roomAliasExistsRoomAliasUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomAliasExistsRoomAliasUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + roomAliasExistsRoomAliasUnderlyingReturnValue = newValue + } + } + } + } + open var roomAliasExistsRoomAliasClosure: ((String) async throws -> Bool)? + + open override func roomAliasExists(roomAlias: String) async throws -> Bool { + if let error = roomAliasExistsRoomAliasThrowableError { + throw error + } + roomAliasExistsRoomAliasCallsCount += 1 + roomAliasExistsRoomAliasReceivedRoomAlias = roomAlias + DispatchQueue.main.async { + self.roomAliasExistsRoomAliasReceivedInvocations.append(roomAlias) + } + if let roomAliasExistsRoomAliasClosure = roomAliasExistsRoomAliasClosure { + return try await roomAliasExistsRoomAliasClosure(roomAlias) + } else { + return roomAliasExistsRoomAliasReturnValue + } + } + //MARK: - roomDirectorySearch var roomDirectorySearchUnderlyingCallsCount = 0 @@ -3751,18 +4022,18 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } - //MARK: - urlForOidcLogin + //MARK: - urlForOidc - open var urlForOidcLoginOidcConfigurationThrowableError: Error? - var urlForOidcLoginOidcConfigurationUnderlyingCallsCount = 0 - open var urlForOidcLoginOidcConfigurationCallsCount: Int { + open var urlForOidcOidcConfigurationPromptThrowableError: Error? + var urlForOidcOidcConfigurationPromptUnderlyingCallsCount = 0 + open var urlForOidcOidcConfigurationPromptCallsCount: Int { get { if Thread.isMainThread { - return urlForOidcLoginOidcConfigurationUnderlyingCallsCount + return urlForOidcOidcConfigurationPromptUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = urlForOidcLoginOidcConfigurationUnderlyingCallsCount + returnValue = urlForOidcOidcConfigurationPromptUnderlyingCallsCount } return returnValue! @@ -3770,29 +4041,29 @@ open class ClientSDKMock: MatrixRustSDK.Client { } set { if Thread.isMainThread { - urlForOidcLoginOidcConfigurationUnderlyingCallsCount = newValue + urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - urlForOidcLoginOidcConfigurationUnderlyingCallsCount = newValue + urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue } } } } - open var urlForOidcLoginOidcConfigurationCalled: Bool { - return urlForOidcLoginOidcConfigurationCallsCount > 0 + open var urlForOidcOidcConfigurationPromptCalled: Bool { + return urlForOidcOidcConfigurationPromptCallsCount > 0 } - open var urlForOidcLoginOidcConfigurationReceivedOidcConfiguration: OidcConfiguration? - open var urlForOidcLoginOidcConfigurationReceivedInvocations: [OidcConfiguration] = [] + open var urlForOidcOidcConfigurationPromptReceivedArguments: (oidcConfiguration: OidcConfiguration, prompt: OidcPrompt)? + open var urlForOidcOidcConfigurationPromptReceivedInvocations: [(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt)] = [] - var urlForOidcLoginOidcConfigurationUnderlyingReturnValue: OidcAuthorizationData! - open var urlForOidcLoginOidcConfigurationReturnValue: OidcAuthorizationData! { + var urlForOidcOidcConfigurationPromptUnderlyingReturnValue: OidcAuthorizationData! + open var urlForOidcOidcConfigurationPromptReturnValue: OidcAuthorizationData! { get { if Thread.isMainThread { - return urlForOidcLoginOidcConfigurationUnderlyingReturnValue + return urlForOidcOidcConfigurationPromptUnderlyingReturnValue } else { var returnValue: OidcAuthorizationData? = nil DispatchQueue.main.sync { - returnValue = urlForOidcLoginOidcConfigurationUnderlyingReturnValue + returnValue = urlForOidcOidcConfigurationPromptUnderlyingReturnValue } return returnValue! @@ -3800,29 +4071,29 @@ open class ClientSDKMock: MatrixRustSDK.Client { } set { if Thread.isMainThread { - urlForOidcLoginOidcConfigurationUnderlyingReturnValue = newValue + urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - urlForOidcLoginOidcConfigurationUnderlyingReturnValue = newValue + urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue } } } } - open var urlForOidcLoginOidcConfigurationClosure: ((OidcConfiguration) async throws -> OidcAuthorizationData)? + open var urlForOidcOidcConfigurationPromptClosure: ((OidcConfiguration, OidcPrompt) async throws -> OidcAuthorizationData)? - open override func urlForOidcLogin(oidcConfiguration: OidcConfiguration) async throws -> OidcAuthorizationData { - if let error = urlForOidcLoginOidcConfigurationThrowableError { + open override func urlForOidc(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt) async throws -> OidcAuthorizationData { + if let error = urlForOidcOidcConfigurationPromptThrowableError { throw error } - urlForOidcLoginOidcConfigurationCallsCount += 1 - urlForOidcLoginOidcConfigurationReceivedOidcConfiguration = oidcConfiguration + urlForOidcOidcConfigurationPromptCallsCount += 1 + urlForOidcOidcConfigurationPromptReceivedArguments = (oidcConfiguration: oidcConfiguration, prompt: prompt) DispatchQueue.main.async { - self.urlForOidcLoginOidcConfigurationReceivedInvocations.append(oidcConfiguration) + self.urlForOidcOidcConfigurationPromptReceivedInvocations.append((oidcConfiguration: oidcConfiguration, prompt: prompt)) } - if let urlForOidcLoginOidcConfigurationClosure = urlForOidcLoginOidcConfigurationClosure { - return try await urlForOidcLoginOidcConfigurationClosure(oidcConfiguration) + if let urlForOidcOidcConfigurationPromptClosure = urlForOidcOidcConfigurationPromptClosure { + return try await urlForOidcOidcConfigurationPromptClosure(oidcConfiguration, prompt) } else { - return urlForOidcLoginOidcConfigurationReturnValue + return urlForOidcOidcConfigurationPromptReturnValue } } @@ -4953,6 +5224,77 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } } + //MARK: - roomDecryptionTrustRequirement + + var roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = 0 + open var roomDecryptionTrustRequirementTrustRequirementCallsCount: Int { + get { + if Thread.isMainThread { + return roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue + } + } + } + } + open var roomDecryptionTrustRequirementTrustRequirementCalled: Bool { + return roomDecryptionTrustRequirementTrustRequirementCallsCount > 0 + } + open var roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement: TrustRequirement? + open var roomDecryptionTrustRequirementTrustRequirementReceivedInvocations: [TrustRequirement] = [] + + var roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue: ClientBuilder! + open var roomDecryptionTrustRequirementTrustRequirementReturnValue: ClientBuilder! { + get { + if Thread.isMainThread { + return roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue + } else { + var returnValue: ClientBuilder? = nil + DispatchQueue.main.sync { + returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue + } + } + } + } + open var roomDecryptionTrustRequirementTrustRequirementClosure: ((TrustRequirement) -> ClientBuilder)? + + open override func roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) -> ClientBuilder { + roomDecryptionTrustRequirementTrustRequirementCallsCount += 1 + roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement = trustRequirement + DispatchQueue.main.async { + self.roomDecryptionTrustRequirementTrustRequirementReceivedInvocations.append(trustRequirement) + } + if let roomDecryptionTrustRequirementTrustRequirementClosure = roomDecryptionTrustRequirementTrustRequirementClosure { + return roomDecryptionTrustRequirementTrustRequirementClosure(trustRequirement) + } else { + return roomDecryptionTrustRequirementTrustRequirementReturnValue + } + } + //MARK: - roomKeyRecipientStrategy var roomKeyRecipientStrategyStrategyUnderlyingCallsCount = 0 @@ -5949,16 +6291,16 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { //MARK: - enableRecovery - open var enableRecoveryWaitForBackupsToUploadProgressListenerThrowableError: Error? - var enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingCallsCount = 0 - open var enableRecoveryWaitForBackupsToUploadProgressListenerCallsCount: Int { + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerThrowableError: Error? + var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingCallsCount = 0 + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerCallsCount: Int { get { if Thread.isMainThread { - return enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingCallsCount + return enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingCallsCount + returnValue = enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingCallsCount } return returnValue! @@ -5966,29 +6308,29 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } set { if Thread.isMainThread { - enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingCallsCount = newValue + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingCallsCount = newValue + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingCallsCount = newValue } } } } - open var enableRecoveryWaitForBackupsToUploadProgressListenerCalled: Bool { - return enableRecoveryWaitForBackupsToUploadProgressListenerCallsCount > 0 + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerCalled: Bool { + return enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerCallsCount > 0 } - open var enableRecoveryWaitForBackupsToUploadProgressListenerReceivedArguments: (waitForBackupsToUpload: Bool, progressListener: EnableRecoveryProgressListener)? - open var enableRecoveryWaitForBackupsToUploadProgressListenerReceivedInvocations: [(waitForBackupsToUpload: Bool, progressListener: EnableRecoveryProgressListener)] = [] + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReceivedArguments: (waitForBackupsToUpload: Bool, passphrase: String?, progressListener: EnableRecoveryProgressListener)? + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReceivedInvocations: [(waitForBackupsToUpload: Bool, passphrase: String?, progressListener: EnableRecoveryProgressListener)] = [] - var enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingReturnValue: String! - open var enableRecoveryWaitForBackupsToUploadProgressListenerReturnValue: String! { + var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingReturnValue: String! + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReturnValue: String! { get { if Thread.isMainThread { - return enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingReturnValue + return enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingReturnValue } else { var returnValue: String? = nil DispatchQueue.main.sync { - returnValue = enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingReturnValue + returnValue = enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingReturnValue } return returnValue! @@ -5996,29 +6338,29 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } set { if Thread.isMainThread { - enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingReturnValue = newValue + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - enableRecoveryWaitForBackupsToUploadProgressListenerUnderlyingReturnValue = newValue + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerUnderlyingReturnValue = newValue } } } } - open var enableRecoveryWaitForBackupsToUploadProgressListenerClosure: ((Bool, EnableRecoveryProgressListener) async throws -> String)? + open var enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerClosure: ((Bool, String?, EnableRecoveryProgressListener) async throws -> String)? - open override func enableRecovery(waitForBackupsToUpload: Bool, progressListener: EnableRecoveryProgressListener) async throws -> String { - if let error = enableRecoveryWaitForBackupsToUploadProgressListenerThrowableError { + open override func enableRecovery(waitForBackupsToUpload: Bool, passphrase: String?, progressListener: EnableRecoveryProgressListener) async throws -> String { + if let error = enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerThrowableError { throw error } - enableRecoveryWaitForBackupsToUploadProgressListenerCallsCount += 1 - enableRecoveryWaitForBackupsToUploadProgressListenerReceivedArguments = (waitForBackupsToUpload: waitForBackupsToUpload, progressListener: progressListener) + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerCallsCount += 1 + enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReceivedArguments = (waitForBackupsToUpload: waitForBackupsToUpload, passphrase: passphrase, progressListener: progressListener) DispatchQueue.main.async { - self.enableRecoveryWaitForBackupsToUploadProgressListenerReceivedInvocations.append((waitForBackupsToUpload: waitForBackupsToUpload, progressListener: progressListener)) + self.enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReceivedInvocations.append((waitForBackupsToUpload: waitForBackupsToUpload, passphrase: passphrase, progressListener: progressListener)) } - if let enableRecoveryWaitForBackupsToUploadProgressListenerClosure = enableRecoveryWaitForBackupsToUploadProgressListenerClosure { - return try await enableRecoveryWaitForBackupsToUploadProgressListenerClosure(waitForBackupsToUpload, progressListener) + if let enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerClosure = enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerClosure { + return try await enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerClosure(waitForBackupsToUpload, passphrase, progressListener) } else { - return enableRecoveryWaitForBackupsToUploadProgressListenerReturnValue + return enableRecoveryWaitForBackupsToUploadPassphraseProgressListenerReturnValue } } @@ -6486,17 +6828,18 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } } - //MARK: - verificationState + //MARK: - userIdentity - var verificationStateUnderlyingCallsCount = 0 - open var verificationStateCallsCount: Int { + open var userIdentityUserIdThrowableError: Error? + var userIdentityUserIdUnderlyingCallsCount = 0 + open var userIdentityUserIdCallsCount: Int { get { if Thread.isMainThread { - return verificationStateUnderlyingCallsCount + return userIdentityUserIdUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = verificationStateUnderlyingCallsCount + returnValue = userIdentityUserIdUnderlyingCallsCount } return returnValue! @@ -6504,27 +6847,29 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } set { if Thread.isMainThread { - verificationStateUnderlyingCallsCount = newValue + userIdentityUserIdUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - verificationStateUnderlyingCallsCount = newValue + userIdentityUserIdUnderlyingCallsCount = newValue } } } } - open var verificationStateCalled: Bool { - return verificationStateCallsCount > 0 + open var userIdentityUserIdCalled: Bool { + return userIdentityUserIdCallsCount > 0 } + open var userIdentityUserIdReceivedUserId: String? + open var userIdentityUserIdReceivedInvocations: [String] = [] - var verificationStateUnderlyingReturnValue: VerificationState! - open var verificationStateReturnValue: VerificationState! { + var userIdentityUserIdUnderlyingReturnValue: UserIdentity? + open var userIdentityUserIdReturnValue: UserIdentity? { get { if Thread.isMainThread { - return verificationStateUnderlyingReturnValue + return userIdentityUserIdUnderlyingReturnValue } else { - var returnValue: VerificationState? = nil + var returnValue: UserIdentity?? = nil DispatchQueue.main.sync { - returnValue = verificationStateUnderlyingReturnValue + returnValue = userIdentityUserIdUnderlyingReturnValue } return returnValue! @@ -6532,36 +6877,43 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } set { if Thread.isMainThread { - verificationStateUnderlyingReturnValue = newValue + userIdentityUserIdUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - verificationStateUnderlyingReturnValue = newValue + userIdentityUserIdUnderlyingReturnValue = newValue } } } } - open var verificationStateClosure: (() -> VerificationState)? + open var userIdentityUserIdClosure: ((String) async throws -> UserIdentity?)? - open override func verificationState() -> VerificationState { - verificationStateCallsCount += 1 - if let verificationStateClosure = verificationStateClosure { - return verificationStateClosure() + open override func userIdentity(userId: String) async throws -> UserIdentity? { + if let error = userIdentityUserIdThrowableError { + throw error + } + userIdentityUserIdCallsCount += 1 + userIdentityUserIdReceivedUserId = userId + DispatchQueue.main.async { + self.userIdentityUserIdReceivedInvocations.append(userId) + } + if let userIdentityUserIdClosure = userIdentityUserIdClosure { + return try await userIdentityUserIdClosure(userId) } else { - return verificationStateReturnValue + return userIdentityUserIdReturnValue } } - //MARK: - verificationStateListener + //MARK: - verificationState - var verificationStateListenerListenerUnderlyingCallsCount = 0 - open var verificationStateListenerListenerCallsCount: Int { + var verificationStateUnderlyingCallsCount = 0 + open var verificationStateCallsCount: Int { get { if Thread.isMainThread { - return verificationStateListenerListenerUnderlyingCallsCount + return verificationStateUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = verificationStateListenerListenerUnderlyingCallsCount + returnValue = verificationStateUnderlyingCallsCount } return returnValue! @@ -6569,10 +6921,75 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { } set { if Thread.isMainThread { - verificationStateListenerListenerUnderlyingCallsCount = newValue + verificationStateUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - verificationStateListenerListenerUnderlyingCallsCount = newValue + verificationStateUnderlyingCallsCount = newValue + } + } + } + } + open var verificationStateCalled: Bool { + return verificationStateCallsCount > 0 + } + + var verificationStateUnderlyingReturnValue: VerificationState! + open var verificationStateReturnValue: VerificationState! { + get { + if Thread.isMainThread { + return verificationStateUnderlyingReturnValue + } else { + var returnValue: VerificationState? = nil + DispatchQueue.main.sync { + returnValue = verificationStateUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + verificationStateUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + verificationStateUnderlyingReturnValue = newValue + } + } + } + } + open var verificationStateClosure: (() -> VerificationState)? + + open override func verificationState() -> VerificationState { + verificationStateCallsCount += 1 + if let verificationStateClosure = verificationStateClosure { + return verificationStateClosure() + } else { + return verificationStateReturnValue + } + } + + //MARK: - verificationStateListener + + var verificationStateListenerListenerUnderlyingCallsCount = 0 + open var verificationStateListenerListenerCallsCount: Int { + get { + if Thread.isMainThread { + return verificationStateListenerListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = verificationStateListenerListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + verificationStateListenerListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + verificationStateListenerListenerUnderlyingCallsCount = newValue } } } @@ -6704,7 +7121,7 @@ open class EncryptionSDKMock: MatrixRustSDK.Encryption { await waitForE2eeInitializationTasksClosure?() } } -open class EventTimelineItemSDKMock: MatrixRustSDK.EventTimelineItem { +open class HomeserverLoginDetailsSDKMock: MatrixRustSDK.HomeserverLoginDetails { init() { super.init(noPointer: .init()) } @@ -6715,147 +7132,17 @@ open class EventTimelineItemSDKMock: MatrixRustSDK.EventTimelineItem { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - canBeRepliedTo - - var canBeRepliedToUnderlyingCallsCount = 0 - open var canBeRepliedToCallsCount: Int { - get { - if Thread.isMainThread { - return canBeRepliedToUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = canBeRepliedToUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - canBeRepliedToUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - canBeRepliedToUnderlyingCallsCount = newValue - } - } - } - } - open var canBeRepliedToCalled: Bool { - return canBeRepliedToCallsCount > 0 - } - - var canBeRepliedToUnderlyingReturnValue: Bool! - open var canBeRepliedToReturnValue: Bool! { - get { - if Thread.isMainThread { - return canBeRepliedToUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = canBeRepliedToUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - canBeRepliedToUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - canBeRepliedToUnderlyingReturnValue = newValue - } - } - } - } - open var canBeRepliedToClosure: (() -> Bool)? - - open override func canBeRepliedTo() -> Bool { - canBeRepliedToCallsCount += 1 - if let canBeRepliedToClosure = canBeRepliedToClosure { - return canBeRepliedToClosure() - } else { - return canBeRepliedToReturnValue - } - } - - //MARK: - content - - var contentUnderlyingCallsCount = 0 - open var contentCallsCount: Int { - get { - if Thread.isMainThread { - return contentUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = contentUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - contentUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - contentUnderlyingCallsCount = newValue - } - } - } - } - open var contentCalled: Bool { - return contentCallsCount > 0 - } - - var contentUnderlyingReturnValue: TimelineItemContent! - open var contentReturnValue: TimelineItemContent! { - get { - if Thread.isMainThread { - return contentUnderlyingReturnValue - } else { - var returnValue: TimelineItemContent? = nil - DispatchQueue.main.sync { - returnValue = contentUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - contentUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - contentUnderlyingReturnValue = newValue - } - } - } - } - open var contentClosure: (() -> TimelineItemContent)? - - open override func content() -> TimelineItemContent { - contentCallsCount += 1 - if let contentClosure = contentClosure { - return contentClosure() - } else { - return contentReturnValue - } - } - - //MARK: - debugInfo + //MARK: - slidingSyncVersion - var debugInfoUnderlyingCallsCount = 0 - open var debugInfoCallsCount: Int { + var slidingSyncVersionUnderlyingCallsCount = 0 + open var slidingSyncVersionCallsCount: Int { get { if Thread.isMainThread { - return debugInfoUnderlyingCallsCount + return slidingSyncVersionUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = debugInfoUnderlyingCallsCount + returnValue = slidingSyncVersionUnderlyingCallsCount } return returnValue! @@ -6863,27 +7150,27 @@ open class EventTimelineItemSDKMock: MatrixRustSDK.EventTimelineItem { } set { if Thread.isMainThread { - debugInfoUnderlyingCallsCount = newValue + slidingSyncVersionUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - debugInfoUnderlyingCallsCount = newValue + slidingSyncVersionUnderlyingCallsCount = newValue } } } } - open var debugInfoCalled: Bool { - return debugInfoCallsCount > 0 + open var slidingSyncVersionCalled: Bool { + return slidingSyncVersionCallsCount > 0 } - var debugInfoUnderlyingReturnValue: EventTimelineItemDebugInfo! - open var debugInfoReturnValue: EventTimelineItemDebugInfo! { + var slidingSyncVersionUnderlyingReturnValue: SlidingSyncVersion! + open var slidingSyncVersionReturnValue: SlidingSyncVersion! { get { if Thread.isMainThread { - return debugInfoUnderlyingReturnValue + return slidingSyncVersionUnderlyingReturnValue } else { - var returnValue: EventTimelineItemDebugInfo? = nil + var returnValue: SlidingSyncVersion? = nil DispatchQueue.main.sync { - returnValue = debugInfoUnderlyingReturnValue + returnValue = slidingSyncVersionUnderlyingReturnValue } return returnValue! @@ -6891,64 +7178,36 @@ open class EventTimelineItemSDKMock: MatrixRustSDK.EventTimelineItem { } set { if Thread.isMainThread { - debugInfoUnderlyingReturnValue = newValue + slidingSyncVersionUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - debugInfoUnderlyingReturnValue = newValue + slidingSyncVersionUnderlyingReturnValue = newValue } } } } - open var debugInfoClosure: (() -> EventTimelineItemDebugInfo)? + open var slidingSyncVersionClosure: (() -> SlidingSyncVersion)? - open override func debugInfo() -> EventTimelineItemDebugInfo { - debugInfoCallsCount += 1 - if let debugInfoClosure = debugInfoClosure { - return debugInfoClosure() + open override func slidingSyncVersion() -> SlidingSyncVersion { + slidingSyncVersionCallsCount += 1 + if let slidingSyncVersionClosure = slidingSyncVersionClosure { + return slidingSyncVersionClosure() } else { - return debugInfoReturnValue + return slidingSyncVersionReturnValue } } - //MARK: - eventId + //MARK: - supportedOidcPrompts - var eventIdUnderlyingCallsCount = 0 - open var eventIdCallsCount: Int { + var supportedOidcPromptsUnderlyingCallsCount = 0 + open var supportedOidcPromptsCallsCount: Int { get { if Thread.isMainThread { - return eventIdUnderlyingCallsCount + return supportedOidcPromptsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = eventIdUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - eventIdUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - eventIdUnderlyingCallsCount = newValue - } - } - } - } - open var eventIdCalled: Bool { - return eventIdCallsCount > 0 - } - - var eventIdUnderlyingReturnValue: String? - open var eventIdReturnValue: String? { - get { - if Thread.isMainThread { - return eventIdUnderlyingReturnValue - } else { - var returnValue: String?? = nil - DispatchQueue.main.sync { - returnValue = eventIdUnderlyingReturnValue + returnValue = supportedOidcPromptsUnderlyingCallsCount } return returnValue! @@ -6956,926 +7215,27 @@ open class EventTimelineItemSDKMock: MatrixRustSDK.EventTimelineItem { } set { if Thread.isMainThread { - eventIdUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - eventIdUnderlyingReturnValue = newValue - } - } - } - } - open var eventIdClosure: (() -> String?)? - - open override func eventId() -> String? { - eventIdCallsCount += 1 - if let eventIdClosure = eventIdClosure { - return eventIdClosure() - } else { - return eventIdReturnValue - } - } - - //MARK: - getShield - - var getShieldStrictUnderlyingCallsCount = 0 - open var getShieldStrictCallsCount: Int { - get { - if Thread.isMainThread { - return getShieldStrictUnderlyingCallsCount + supportedOidcPromptsUnderlyingCallsCount = newValue } else { - var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = getShieldStrictUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - getShieldStrictUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - getShieldStrictUnderlyingCallsCount = newValue - } - } - } - } - open var getShieldStrictCalled: Bool { - return getShieldStrictCallsCount > 0 - } - open var getShieldStrictReceivedStrict: Bool? - open var getShieldStrictReceivedInvocations: [Bool] = [] - - var getShieldStrictUnderlyingReturnValue: ShieldState? - open var getShieldStrictReturnValue: ShieldState? { - get { - if Thread.isMainThread { - return getShieldStrictUnderlyingReturnValue - } else { - var returnValue: ShieldState?? = nil - DispatchQueue.main.sync { - returnValue = getShieldStrictUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - getShieldStrictUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - getShieldStrictUnderlyingReturnValue = newValue - } - } - } - } - open var getShieldStrictClosure: ((Bool) -> ShieldState?)? - - open override func getShield(strict: Bool) -> ShieldState? { - getShieldStrictCallsCount += 1 - getShieldStrictReceivedStrict = strict - DispatchQueue.main.async { - self.getShieldStrictReceivedInvocations.append(strict) - } - if let getShieldStrictClosure = getShieldStrictClosure { - return getShieldStrictClosure(strict) - } else { - return getShieldStrictReturnValue - } - } - - //MARK: - isEditable - - var isEditableUnderlyingCallsCount = 0 - open var isEditableCallsCount: Int { - get { - if Thread.isMainThread { - return isEditableUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = isEditableUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isEditableUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - isEditableUnderlyingCallsCount = newValue - } - } - } - } - open var isEditableCalled: Bool { - return isEditableCallsCount > 0 - } - - var isEditableUnderlyingReturnValue: Bool! - open var isEditableReturnValue: Bool! { - get { - if Thread.isMainThread { - return isEditableUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = isEditableUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isEditableUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - isEditableUnderlyingReturnValue = newValue - } - } - } - } - open var isEditableClosure: (() -> Bool)? - - open override func isEditable() -> Bool { - isEditableCallsCount += 1 - if let isEditableClosure = isEditableClosure { - return isEditableClosure() - } else { - return isEditableReturnValue - } - } - - //MARK: - isLocal - - var isLocalUnderlyingCallsCount = 0 - open var isLocalCallsCount: Int { - get { - if Thread.isMainThread { - return isLocalUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = isLocalUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isLocalUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - isLocalUnderlyingCallsCount = newValue - } - } - } - } - open var isLocalCalled: Bool { - return isLocalCallsCount > 0 - } - - var isLocalUnderlyingReturnValue: Bool! - open var isLocalReturnValue: Bool! { - get { - if Thread.isMainThread { - return isLocalUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = isLocalUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isLocalUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - isLocalUnderlyingReturnValue = newValue - } - } - } - } - open var isLocalClosure: (() -> Bool)? - - open override func isLocal() -> Bool { - isLocalCallsCount += 1 - if let isLocalClosure = isLocalClosure { - return isLocalClosure() - } else { - return isLocalReturnValue - } - } - - //MARK: - isOwn - - var isOwnUnderlyingCallsCount = 0 - open var isOwnCallsCount: Int { - get { - if Thread.isMainThread { - return isOwnUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = isOwnUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isOwnUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - isOwnUnderlyingCallsCount = newValue - } - } - } - } - open var isOwnCalled: Bool { - return isOwnCallsCount > 0 - } - - var isOwnUnderlyingReturnValue: Bool! - open var isOwnReturnValue: Bool! { - get { - if Thread.isMainThread { - return isOwnUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = isOwnUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isOwnUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - isOwnUnderlyingReturnValue = newValue - } - } - } - } - open var isOwnClosure: (() -> Bool)? - - open override func isOwn() -> Bool { - isOwnCallsCount += 1 - if let isOwnClosure = isOwnClosure { - return isOwnClosure() - } else { - return isOwnReturnValue - } - } - - //MARK: - isRemote - - var isRemoteUnderlyingCallsCount = 0 - open var isRemoteCallsCount: Int { - get { - if Thread.isMainThread { - return isRemoteUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = isRemoteUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isRemoteUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - isRemoteUnderlyingCallsCount = newValue - } - } - } - } - open var isRemoteCalled: Bool { - return isRemoteCallsCount > 0 - } - - var isRemoteUnderlyingReturnValue: Bool! - open var isRemoteReturnValue: Bool! { - get { - if Thread.isMainThread { - return isRemoteUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = isRemoteUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isRemoteUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - isRemoteUnderlyingReturnValue = newValue - } - } - } - } - open var isRemoteClosure: (() -> Bool)? - - open override func isRemote() -> Bool { - isRemoteCallsCount += 1 - if let isRemoteClosure = isRemoteClosure { - return isRemoteClosure() - } else { - return isRemoteReturnValue - } - } - - //MARK: - localSendState - - var localSendStateUnderlyingCallsCount = 0 - open var localSendStateCallsCount: Int { - get { - if Thread.isMainThread { - return localSendStateUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = localSendStateUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - localSendStateUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - localSendStateUnderlyingCallsCount = newValue - } - } - } - } - open var localSendStateCalled: Bool { - return localSendStateCallsCount > 0 - } - - var localSendStateUnderlyingReturnValue: EventSendState? - open var localSendStateReturnValue: EventSendState? { - get { - if Thread.isMainThread { - return localSendStateUnderlyingReturnValue - } else { - var returnValue: EventSendState?? = nil - DispatchQueue.main.sync { - returnValue = localSendStateUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - localSendStateUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - localSendStateUnderlyingReturnValue = newValue - } - } - } - } - open var localSendStateClosure: (() -> EventSendState?)? - - open override func localSendState() -> EventSendState? { - localSendStateCallsCount += 1 - if let localSendStateClosure = localSendStateClosure { - return localSendStateClosure() - } else { - return localSendStateReturnValue - } - } - - //MARK: - origin - - var originUnderlyingCallsCount = 0 - open var originCallsCount: Int { - get { - if Thread.isMainThread { - return originUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = originUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - originUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - originUnderlyingCallsCount = newValue - } - } - } - } - open var originCalled: Bool { - return originCallsCount > 0 - } - - var originUnderlyingReturnValue: EventItemOrigin? - open var originReturnValue: EventItemOrigin? { - get { - if Thread.isMainThread { - return originUnderlyingReturnValue - } else { - var returnValue: EventItemOrigin?? = nil - DispatchQueue.main.sync { - returnValue = originUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - originUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - originUnderlyingReturnValue = newValue - } - } - } - } - open var originClosure: (() -> EventItemOrigin?)? - - open override func origin() -> EventItemOrigin? { - originCallsCount += 1 - if let originClosure = originClosure { - return originClosure() - } else { - return originReturnValue - } - } - - //MARK: - reactions - - var reactionsUnderlyingCallsCount = 0 - open var reactionsCallsCount: Int { - get { - if Thread.isMainThread { - return reactionsUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = reactionsUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - reactionsUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - reactionsUnderlyingCallsCount = newValue - } - } - } - } - open var reactionsCalled: Bool { - return reactionsCallsCount > 0 - } - - var reactionsUnderlyingReturnValue: [Reaction]! - open var reactionsReturnValue: [Reaction]! { - get { - if Thread.isMainThread { - return reactionsUnderlyingReturnValue - } else { - var returnValue: [Reaction]? = nil - DispatchQueue.main.sync { - returnValue = reactionsUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - reactionsUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - reactionsUnderlyingReturnValue = newValue - } - } - } - } - open var reactionsClosure: (() -> [Reaction])? - - open override func reactions() -> [Reaction] { - reactionsCallsCount += 1 - if let reactionsClosure = reactionsClosure { - return reactionsClosure() - } else { - return reactionsReturnValue - } - } - - //MARK: - readReceipts - - var readReceiptsUnderlyingCallsCount = 0 - open var readReceiptsCallsCount: Int { - get { - if Thread.isMainThread { - return readReceiptsUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = readReceiptsUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - readReceiptsUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - readReceiptsUnderlyingCallsCount = newValue - } - } - } - } - open var readReceiptsCalled: Bool { - return readReceiptsCallsCount > 0 - } - - var readReceiptsUnderlyingReturnValue: [String: Receipt]! - open var readReceiptsReturnValue: [String: Receipt]! { - get { - if Thread.isMainThread { - return readReceiptsUnderlyingReturnValue - } else { - var returnValue: [String: Receipt]? = nil - DispatchQueue.main.sync { - returnValue = readReceiptsUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - readReceiptsUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - readReceiptsUnderlyingReturnValue = newValue - } - } - } - } - open var readReceiptsClosure: (() -> [String: Receipt])? - - open override func readReceipts() -> [String: Receipt] { - readReceiptsCallsCount += 1 - if let readReceiptsClosure = readReceiptsClosure { - return readReceiptsClosure() - } else { - return readReceiptsReturnValue - } - } - - //MARK: - sender - - var senderUnderlyingCallsCount = 0 - open var senderCallsCount: Int { - get { - if Thread.isMainThread { - return senderUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = senderUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - senderUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - senderUnderlyingCallsCount = newValue - } - } - } - } - open var senderCalled: Bool { - return senderCallsCount > 0 - } - - var senderUnderlyingReturnValue: String! - open var senderReturnValue: String! { - get { - if Thread.isMainThread { - return senderUnderlyingReturnValue - } else { - var returnValue: String? = nil - DispatchQueue.main.sync { - returnValue = senderUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - senderUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - senderUnderlyingReturnValue = newValue - } - } - } - } - open var senderClosure: (() -> String)? - - open override func sender() -> String { - senderCallsCount += 1 - if let senderClosure = senderClosure { - return senderClosure() - } else { - return senderReturnValue - } - } - - //MARK: - senderProfile - - var senderProfileUnderlyingCallsCount = 0 - open var senderProfileCallsCount: Int { - get { - if Thread.isMainThread { - return senderProfileUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = senderProfileUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - senderProfileUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - senderProfileUnderlyingCallsCount = newValue - } - } - } - } - open var senderProfileCalled: Bool { - return senderProfileCallsCount > 0 - } - - var senderProfileUnderlyingReturnValue: ProfileDetails! - open var senderProfileReturnValue: ProfileDetails! { - get { - if Thread.isMainThread { - return senderProfileUnderlyingReturnValue - } else { - var returnValue: ProfileDetails? = nil - DispatchQueue.main.sync { - returnValue = senderProfileUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - senderProfileUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - senderProfileUnderlyingReturnValue = newValue - } - } - } - } - open var senderProfileClosure: (() -> ProfileDetails)? - - open override func senderProfile() -> ProfileDetails { - senderProfileCallsCount += 1 - if let senderProfileClosure = senderProfileClosure { - return senderProfileClosure() - } else { - return senderProfileReturnValue - } - } - - //MARK: - timestamp - - var timestampUnderlyingCallsCount = 0 - open var timestampCallsCount: Int { - get { - if Thread.isMainThread { - return timestampUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = timestampUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - timestampUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - timestampUnderlyingCallsCount = newValue - } - } - } - } - open var timestampCalled: Bool { - return timestampCallsCount > 0 - } - - var timestampUnderlyingReturnValue: UInt64! - open var timestampReturnValue: UInt64! { - get { - if Thread.isMainThread { - return timestampUnderlyingReturnValue - } else { - var returnValue: UInt64? = nil - DispatchQueue.main.sync { - returnValue = timestampUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - timestampUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - timestampUnderlyingReturnValue = newValue - } - } - } - } - open var timestampClosure: (() -> UInt64)? - - open override func timestamp() -> UInt64 { - timestampCallsCount += 1 - if let timestampClosure = timestampClosure { - return timestampClosure() - } else { - return timestampReturnValue - } - } - - //MARK: - transactionId - - var transactionIdUnderlyingCallsCount = 0 - open var transactionIdCallsCount: Int { - get { - if Thread.isMainThread { - return transactionIdUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = transactionIdUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - transactionIdUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - transactionIdUnderlyingCallsCount = newValue - } - } - } - } - open var transactionIdCalled: Bool { - return transactionIdCallsCount > 0 - } - - var transactionIdUnderlyingReturnValue: String? - open var transactionIdReturnValue: String? { - get { - if Thread.isMainThread { - return transactionIdUnderlyingReturnValue - } else { - var returnValue: String?? = nil - DispatchQueue.main.sync { - returnValue = transactionIdUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - transactionIdUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - transactionIdUnderlyingReturnValue = newValue - } - } - } - } - open var transactionIdClosure: (() -> String?)? - - open override func transactionId() -> String? { - transactionIdCallsCount += 1 - if let transactionIdClosure = transactionIdClosure { - return transactionIdClosure() - } else { - return transactionIdReturnValue - } - } -} -open class HomeserverLoginDetailsSDKMock: MatrixRustSDK.HomeserverLoginDetails { - init() { - super.init(noPointer: .init()) - } - - public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - fatalError("init(unsafeFromRawPointer:) has not been implemented") - } - - fileprivate var pointer: UnsafeMutableRawPointer! - - //MARK: - slidingSyncVersion - - var slidingSyncVersionUnderlyingCallsCount = 0 - open var slidingSyncVersionCallsCount: Int { - get { - if Thread.isMainThread { - return slidingSyncVersionUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = slidingSyncVersionUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - slidingSyncVersionUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - slidingSyncVersionUnderlyingCallsCount = newValue + supportedOidcPromptsUnderlyingCallsCount = newValue } } } } - open var slidingSyncVersionCalled: Bool { - return slidingSyncVersionCallsCount > 0 + open var supportedOidcPromptsCalled: Bool { + return supportedOidcPromptsCallsCount > 0 } - var slidingSyncVersionUnderlyingReturnValue: SlidingSyncVersion! - open var slidingSyncVersionReturnValue: SlidingSyncVersion! { + var supportedOidcPromptsUnderlyingReturnValue: [OidcPrompt]! + open var supportedOidcPromptsReturnValue: [OidcPrompt]! { get { if Thread.isMainThread { - return slidingSyncVersionUnderlyingReturnValue + return supportedOidcPromptsUnderlyingReturnValue } else { - var returnValue: SlidingSyncVersion? = nil + var returnValue: [OidcPrompt]? = nil DispatchQueue.main.sync { - returnValue = slidingSyncVersionUnderlyingReturnValue + returnValue = supportedOidcPromptsUnderlyingReturnValue } return returnValue! @@ -7883,22 +7243,22 @@ open class HomeserverLoginDetailsSDKMock: MatrixRustSDK.HomeserverLoginDetails { } set { if Thread.isMainThread { - slidingSyncVersionUnderlyingReturnValue = newValue + supportedOidcPromptsUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - slidingSyncVersionUnderlyingReturnValue = newValue + supportedOidcPromptsUnderlyingReturnValue = newValue } } } } - open var slidingSyncVersionClosure: (() -> SlidingSyncVersion)? + open var supportedOidcPromptsClosure: (() -> [OidcPrompt])? - open override func slidingSyncVersion() -> SlidingSyncVersion { - slidingSyncVersionCallsCount += 1 - if let slidingSyncVersionClosure = slidingSyncVersionClosure { - return slidingSyncVersionClosure() + open override func supportedOidcPrompts() -> [OidcPrompt] { + supportedOidcPromptsCallsCount += 1 + if let supportedOidcPromptsClosure = supportedOidcPromptsClosure { + return supportedOidcPromptsClosure() } else { - return slidingSyncVersionReturnValue + return supportedOidcPromptsReturnValue } } @@ -8255,162 +7615,7 @@ open class IdentityResetHandleSDKMock: MatrixRustSDK.IdentityResetHandle { try await resetAuthClosure?(auth) } } -open class MediaFileHandleSDKMock: MatrixRustSDK.MediaFileHandle { - init() { - super.init(noPointer: .init()) - } - - public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - fatalError("init(unsafeFromRawPointer:) has not been implemented") - } - - fileprivate var pointer: UnsafeMutableRawPointer! - - //MARK: - path - - open var pathThrowableError: Error? - var pathUnderlyingCallsCount = 0 - open var pathCallsCount: Int { - get { - if Thread.isMainThread { - return pathUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = pathUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - pathUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - pathUnderlyingCallsCount = newValue - } - } - } - } - open var pathCalled: Bool { - return pathCallsCount > 0 - } - - var pathUnderlyingReturnValue: String! - open var pathReturnValue: String! { - get { - if Thread.isMainThread { - return pathUnderlyingReturnValue - } else { - var returnValue: String? = nil - DispatchQueue.main.sync { - returnValue = pathUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - pathUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - pathUnderlyingReturnValue = newValue - } - } - } - } - open var pathClosure: (() throws -> String)? - - open override func path() throws -> String { - if let error = pathThrowableError { - throw error - } - pathCallsCount += 1 - if let pathClosure = pathClosure { - return try pathClosure() - } else { - return pathReturnValue - } - } - - //MARK: - persist - - open var persistPathThrowableError: Error? - var persistPathUnderlyingCallsCount = 0 - open var persistPathCallsCount: Int { - get { - if Thread.isMainThread { - return persistPathUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = persistPathUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - persistPathUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - persistPathUnderlyingCallsCount = newValue - } - } - } - } - open var persistPathCalled: Bool { - return persistPathCallsCount > 0 - } - open var persistPathReceivedPath: String? - open var persistPathReceivedInvocations: [String] = [] - - var persistPathUnderlyingReturnValue: Bool! - open var persistPathReturnValue: Bool! { - get { - if Thread.isMainThread { - return persistPathUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = persistPathUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - persistPathUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - persistPathUnderlyingReturnValue = newValue - } - } - } - } - open var persistPathClosure: ((String) throws -> Bool)? - - open override func persist(path: String) throws -> Bool { - if let error = persistPathThrowableError { - throw error - } - persistPathCallsCount += 1 - persistPathReceivedPath = path - DispatchQueue.main.async { - self.persistPathReceivedInvocations.append(path) - } - if let persistPathClosure = persistPathClosure { - return try persistPathClosure(path) - } else { - return persistPathReturnValue - } - } -} -open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { +open class InReplyToDetailsSDKMock: MatrixRustSDK.InReplyToDetails { init() { super.init(noPointer: .init()) } @@ -8420,21 +7625,18 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { } fileprivate var pointer: UnsafeMutableRawPointer! - static func reset() - { - } - //MARK: - toJson + //MARK: - event - var toJsonUnderlyingCallsCount = 0 - open var toJsonCallsCount: Int { + var eventUnderlyingCallsCount = 0 + open var eventCallsCount: Int { get { if Thread.isMainThread { - return toJsonUnderlyingCallsCount + return eventUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = toJsonUnderlyingCallsCount + returnValue = eventUnderlyingCallsCount } return returnValue! @@ -8442,27 +7644,27 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { } set { if Thread.isMainThread { - toJsonUnderlyingCallsCount = newValue + eventUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - toJsonUnderlyingCallsCount = newValue + eventUnderlyingCallsCount = newValue } } } } - open var toJsonCalled: Bool { - return toJsonCallsCount > 0 + open var eventCalled: Bool { + return eventCallsCount > 0 } - var toJsonUnderlyingReturnValue: String! - open var toJsonReturnValue: String! { + var eventUnderlyingReturnValue: RepliedToEventDetails! + open var eventReturnValue: RepliedToEventDetails! { get { if Thread.isMainThread { - return toJsonUnderlyingReturnValue + return eventUnderlyingReturnValue } else { - var returnValue: String? = nil + var returnValue: RepliedToEventDetails? = nil DispatchQueue.main.sync { - returnValue = toJsonUnderlyingReturnValue + returnValue = eventUnderlyingReturnValue } return returnValue! @@ -8470,36 +7672,36 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { } set { if Thread.isMainThread { - toJsonUnderlyingReturnValue = newValue + eventUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - toJsonUnderlyingReturnValue = newValue + eventUnderlyingReturnValue = newValue } } } } - open var toJsonClosure: (() -> String)? + open var eventClosure: (() -> RepliedToEventDetails)? - open override func toJson() -> String { - toJsonCallsCount += 1 - if let toJsonClosure = toJsonClosure { - return toJsonClosure() + open override func event() -> RepliedToEventDetails { + eventCallsCount += 1 + if let eventClosure = eventClosure { + return eventClosure() } else { - return toJsonReturnValue + return eventReturnValue } } - //MARK: - url + //MARK: - eventId - var urlUnderlyingCallsCount = 0 - open var urlCallsCount: Int { + var eventIdUnderlyingCallsCount = 0 + open var eventIdCallsCount: Int { get { if Thread.isMainThread { - return urlUnderlyingCallsCount + return eventIdUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = urlUnderlyingCallsCount + returnValue = eventIdUnderlyingCallsCount } return returnValue! @@ -8507,27 +7709,27 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { } set { if Thread.isMainThread { - urlUnderlyingCallsCount = newValue + eventIdUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - urlUnderlyingCallsCount = newValue + eventIdUnderlyingCallsCount = newValue } } } } - open var urlCalled: Bool { - return urlCallsCount > 0 + open var eventIdCalled: Bool { + return eventIdCallsCount > 0 } - var urlUnderlyingReturnValue: String! - open var urlReturnValue: String! { + var eventIdUnderlyingReturnValue: String! + open var eventIdReturnValue: String! { get { if Thread.isMainThread { - return urlUnderlyingReturnValue + return eventIdUnderlyingReturnValue } else { var returnValue: String? = nil DispatchQueue.main.sync { - returnValue = urlUnderlyingReturnValue + returnValue = eventIdUnderlyingReturnValue } return returnValue! @@ -8535,26 +7737,26 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { } set { if Thread.isMainThread { - urlUnderlyingReturnValue = newValue + eventIdUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - urlUnderlyingReturnValue = newValue + eventIdUnderlyingReturnValue = newValue } } } } - open var urlClosure: (() -> String)? + open var eventIdClosure: (() -> String)? - open override func url() -> String { - urlCallsCount += 1 - if let urlClosure = urlClosure { - return urlClosure() + open override func eventId() -> String { + eventIdCallsCount += 1 + if let eventIdClosure = eventIdClosure { + return eventIdClosure() } else { - return urlReturnValue + return eventIdReturnValue } } } -open class MessageSDKMock: MatrixRustSDK.Message { +open class LazyTimelineItemProviderSDKMock: MatrixRustSDK.LazyTimelineItemProvider { init() { super.init(noPointer: .init()) } @@ -8565,17 +7767,17 @@ open class MessageSDKMock: MatrixRustSDK.Message { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - body + //MARK: - debugInfo - var bodyUnderlyingCallsCount = 0 - open var bodyCallsCount: Int { + var debugInfoUnderlyingCallsCount = 0 + open var debugInfoCallsCount: Int { get { if Thread.isMainThread { - return bodyUnderlyingCallsCount + return debugInfoUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = bodyUnderlyingCallsCount + returnValue = debugInfoUnderlyingCallsCount } return returnValue! @@ -8583,27 +7785,27 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - bodyUnderlyingCallsCount = newValue + debugInfoUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - bodyUnderlyingCallsCount = newValue + debugInfoUnderlyingCallsCount = newValue } } } } - open var bodyCalled: Bool { - return bodyCallsCount > 0 + open var debugInfoCalled: Bool { + return debugInfoCallsCount > 0 } - var bodyUnderlyingReturnValue: String! - open var bodyReturnValue: String! { + var debugInfoUnderlyingReturnValue: EventTimelineItemDebugInfo! + open var debugInfoReturnValue: EventTimelineItemDebugInfo! { get { if Thread.isMainThread { - return bodyUnderlyingReturnValue + return debugInfoUnderlyingReturnValue } else { - var returnValue: String? = nil + var returnValue: EventTimelineItemDebugInfo? = nil DispatchQueue.main.sync { - returnValue = bodyUnderlyingReturnValue + returnValue = debugInfoUnderlyingReturnValue } return returnValue! @@ -8611,36 +7813,36 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - bodyUnderlyingReturnValue = newValue + debugInfoUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - bodyUnderlyingReturnValue = newValue + debugInfoUnderlyingReturnValue = newValue } } } } - open var bodyClosure: (() -> String)? + open var debugInfoClosure: (() -> EventTimelineItemDebugInfo)? - open override func body() -> String { - bodyCallsCount += 1 - if let bodyClosure = bodyClosure { - return bodyClosure() + open override func debugInfo() -> EventTimelineItemDebugInfo { + debugInfoCallsCount += 1 + if let debugInfoClosure = debugInfoClosure { + return debugInfoClosure() } else { - return bodyReturnValue + return debugInfoReturnValue } } - //MARK: - content + //MARK: - getShields - var contentUnderlyingCallsCount = 0 - open var contentCallsCount: Int { + var getShieldsStrictUnderlyingCallsCount = 0 + open var getShieldsStrictCallsCount: Int { get { if Thread.isMainThread { - return contentUnderlyingCallsCount + return getShieldsStrictUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = contentUnderlyingCallsCount + returnValue = getShieldsStrictUnderlyingCallsCount } return returnValue! @@ -8648,27 +7850,29 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - contentUnderlyingCallsCount = newValue + getShieldsStrictUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - contentUnderlyingCallsCount = newValue + getShieldsStrictUnderlyingCallsCount = newValue } } } } - open var contentCalled: Bool { - return contentCallsCount > 0 + open var getShieldsStrictCalled: Bool { + return getShieldsStrictCallsCount > 0 } + open var getShieldsStrictReceivedStrict: Bool? + open var getShieldsStrictReceivedInvocations: [Bool] = [] - var contentUnderlyingReturnValue: RoomMessageEventContentWithoutRelation! - open var contentReturnValue: RoomMessageEventContentWithoutRelation! { + var getShieldsStrictUnderlyingReturnValue: ShieldState? + open var getShieldsStrictReturnValue: ShieldState? { get { if Thread.isMainThread { - return contentUnderlyingReturnValue + return getShieldsStrictUnderlyingReturnValue } else { - var returnValue: RoomMessageEventContentWithoutRelation? = nil + var returnValue: ShieldState?? = nil DispatchQueue.main.sync { - returnValue = contentUnderlyingReturnValue + returnValue = getShieldsStrictUnderlyingReturnValue } return returnValue! @@ -8676,36 +7880,52 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - contentUnderlyingReturnValue = newValue + getShieldsStrictUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - contentUnderlyingReturnValue = newValue + getShieldsStrictUnderlyingReturnValue = newValue } } } } - open var contentClosure: (() -> RoomMessageEventContentWithoutRelation)? + open var getShieldsStrictClosure: ((Bool) -> ShieldState?)? - open override func content() -> RoomMessageEventContentWithoutRelation { - contentCallsCount += 1 - if let contentClosure = contentClosure { - return contentClosure() + open override func getShields(strict: Bool) -> ShieldState? { + getShieldsStrictCallsCount += 1 + getShieldsStrictReceivedStrict = strict + DispatchQueue.main.async { + self.getShieldsStrictReceivedInvocations.append(strict) + } + if let getShieldsStrictClosure = getShieldsStrictClosure { + return getShieldsStrictClosure(strict) } else { - return contentReturnValue + return getShieldsStrictReturnValue } } +} +open class MediaFileHandleSDKMock: MatrixRustSDK.MediaFileHandle { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } + + fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - inReplyTo + //MARK: - path - var inReplyToUnderlyingCallsCount = 0 - open var inReplyToCallsCount: Int { + open var pathThrowableError: Error? + var pathUnderlyingCallsCount = 0 + open var pathCallsCount: Int { get { if Thread.isMainThread { - return inReplyToUnderlyingCallsCount + return pathUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = inReplyToUnderlyingCallsCount + returnValue = pathUnderlyingCallsCount } return returnValue! @@ -8713,27 +7933,27 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - inReplyToUnderlyingCallsCount = newValue + pathUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - inReplyToUnderlyingCallsCount = newValue + pathUnderlyingCallsCount = newValue } } } } - open var inReplyToCalled: Bool { - return inReplyToCallsCount > 0 + open var pathCalled: Bool { + return pathCallsCount > 0 } - var inReplyToUnderlyingReturnValue: InReplyToDetails? - open var inReplyToReturnValue: InReplyToDetails? { + var pathUnderlyingReturnValue: String! + open var pathReturnValue: String! { get { if Thread.isMainThread { - return inReplyToUnderlyingReturnValue + return pathUnderlyingReturnValue } else { - var returnValue: InReplyToDetails?? = nil + var returnValue: String? = nil DispatchQueue.main.sync { - returnValue = inReplyToUnderlyingReturnValue + returnValue = pathUnderlyingReturnValue } return returnValue! @@ -8741,36 +7961,40 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - inReplyToUnderlyingReturnValue = newValue + pathUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - inReplyToUnderlyingReturnValue = newValue + pathUnderlyingReturnValue = newValue } } } } - open var inReplyToClosure: (() -> InReplyToDetails?)? + open var pathClosure: (() throws -> String)? - open override func inReplyTo() -> InReplyToDetails? { - inReplyToCallsCount += 1 - if let inReplyToClosure = inReplyToClosure { - return inReplyToClosure() + open override func path() throws -> String { + if let error = pathThrowableError { + throw error + } + pathCallsCount += 1 + if let pathClosure = pathClosure { + return try pathClosure() } else { - return inReplyToReturnValue + return pathReturnValue } } - //MARK: - isEdited + //MARK: - persist - var isEditedUnderlyingCallsCount = 0 - open var isEditedCallsCount: Int { + open var persistPathThrowableError: Error? + var persistPathUnderlyingCallsCount = 0 + open var persistPathCallsCount: Int { get { if Thread.isMainThread { - return isEditedUnderlyingCallsCount + return persistPathUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = isEditedUnderlyingCallsCount + returnValue = persistPathUnderlyingCallsCount } return returnValue! @@ -8778,27 +8002,29 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - isEditedUnderlyingCallsCount = newValue + persistPathUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - isEditedUnderlyingCallsCount = newValue + persistPathUnderlyingCallsCount = newValue } } } } - open var isEditedCalled: Bool { - return isEditedCallsCount > 0 + open var persistPathCalled: Bool { + return persistPathCallsCount > 0 } + open var persistPathReceivedPath: String? + open var persistPathReceivedInvocations: [String] = [] - var isEditedUnderlyingReturnValue: Bool! - open var isEditedReturnValue: Bool! { + var persistPathUnderlyingReturnValue: Bool! + open var persistPathReturnValue: Bool! { get { if Thread.isMainThread { - return isEditedUnderlyingReturnValue + return persistPathUnderlyingReturnValue } else { var returnValue: Bool? = nil DispatchQueue.main.sync { - returnValue = isEditedUnderlyingReturnValue + returnValue = persistPathUnderlyingReturnValue } return returnValue! @@ -8806,36 +8032,57 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - isEditedUnderlyingReturnValue = newValue + persistPathUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - isEditedUnderlyingReturnValue = newValue + persistPathUnderlyingReturnValue = newValue } } } } - open var isEditedClosure: (() -> Bool)? + open var persistPathClosure: ((String) throws -> Bool)? - open override func isEdited() -> Bool { - isEditedCallsCount += 1 - if let isEditedClosure = isEditedClosure { - return isEditedClosure() + open override func persist(path: String) throws -> Bool { + if let error = persistPathThrowableError { + throw error + } + persistPathCallsCount += 1 + persistPathReceivedPath = path + DispatchQueue.main.async { + self.persistPathReceivedInvocations.append(path) + } + if let persistPathClosure = persistPathClosure { + return try persistPathClosure(path) } else { - return isEditedReturnValue + return persistPathReturnValue } } +} +open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } + + fileprivate var pointer: UnsafeMutableRawPointer! + static func reset() + { + } - //MARK: - isThreaded + //MARK: - toJson - var isThreadedUnderlyingCallsCount = 0 - open var isThreadedCallsCount: Int { + var toJsonUnderlyingCallsCount = 0 + open var toJsonCallsCount: Int { get { if Thread.isMainThread { - return isThreadedUnderlyingCallsCount + return toJsonUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = isThreadedUnderlyingCallsCount + returnValue = toJsonUnderlyingCallsCount } return returnValue! @@ -8843,27 +8090,27 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - isThreadedUnderlyingCallsCount = newValue + toJsonUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - isThreadedUnderlyingCallsCount = newValue + toJsonUnderlyingCallsCount = newValue } } } } - open var isThreadedCalled: Bool { - return isThreadedCallsCount > 0 + open var toJsonCalled: Bool { + return toJsonCallsCount > 0 } - var isThreadedUnderlyingReturnValue: Bool! - open var isThreadedReturnValue: Bool! { + var toJsonUnderlyingReturnValue: String! + open var toJsonReturnValue: String! { get { if Thread.isMainThread { - return isThreadedUnderlyingReturnValue + return toJsonUnderlyingReturnValue } else { - var returnValue: Bool? = nil + var returnValue: String? = nil DispatchQueue.main.sync { - returnValue = isThreadedUnderlyingReturnValue + returnValue = toJsonUnderlyingReturnValue } return returnValue! @@ -8871,36 +8118,36 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - isThreadedUnderlyingReturnValue = newValue + toJsonUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - isThreadedUnderlyingReturnValue = newValue + toJsonUnderlyingReturnValue = newValue } } } } - open var isThreadedClosure: (() -> Bool)? + open var toJsonClosure: (() -> String)? - open override func isThreaded() -> Bool { - isThreadedCallsCount += 1 - if let isThreadedClosure = isThreadedClosure { - return isThreadedClosure() + open override func toJson() -> String { + toJsonCallsCount += 1 + if let toJsonClosure = toJsonClosure { + return toJsonClosure() } else { - return isThreadedReturnValue + return toJsonReturnValue } } - //MARK: - msgtype + //MARK: - url - var msgtypeUnderlyingCallsCount = 0 - open var msgtypeCallsCount: Int { + var urlUnderlyingCallsCount = 0 + open var urlCallsCount: Int { get { if Thread.isMainThread { - return msgtypeUnderlyingCallsCount + return urlUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = msgtypeUnderlyingCallsCount + returnValue = urlUnderlyingCallsCount } return returnValue! @@ -8908,27 +8155,27 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - msgtypeUnderlyingCallsCount = newValue + urlUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - msgtypeUnderlyingCallsCount = newValue + urlUnderlyingCallsCount = newValue } } } } - open var msgtypeCalled: Bool { - return msgtypeCallsCount > 0 + open var urlCalled: Bool { + return urlCallsCount > 0 } - var msgtypeUnderlyingReturnValue: MessageType! - open var msgtypeReturnValue: MessageType! { + var urlUnderlyingReturnValue: String! + open var urlReturnValue: String! { get { if Thread.isMainThread { - return msgtypeUnderlyingReturnValue + return urlUnderlyingReturnValue } else { - var returnValue: MessageType? = nil + var returnValue: String? = nil DispatchQueue.main.sync { - returnValue = msgtypeUnderlyingReturnValue + returnValue = urlUnderlyingReturnValue } return returnValue! @@ -8936,22 +8183,22 @@ open class MessageSDKMock: MatrixRustSDK.Message { } set { if Thread.isMainThread { - msgtypeUnderlyingReturnValue = newValue + urlUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - msgtypeUnderlyingReturnValue = newValue + urlUnderlyingReturnValue = newValue } } } } - open var msgtypeClosure: (() -> MessageType)? + open var urlClosure: (() -> String)? - open override func msgtype() -> MessageType { - msgtypeCallsCount += 1 - if let msgtypeClosure = msgtypeClosure { - return msgtypeClosure() + open override func url() -> String { + urlCallsCount += 1 + if let urlClosure = urlClosure { + return urlClosure() } else { - return msgtypeReturnValue + return urlReturnValue } } } @@ -14292,6 +13539,77 @@ open class RoomSDKMock: MatrixRustSDK.Room { try await setUnreadFlagNewValueClosure?(newValue) } + //MARK: - subscribeToIdentityStatusChanges + + var subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = 0 + open var subscribeToIdentityStatusChangesListenerCallsCount: Int { + get { + if Thread.isMainThread { + return subscribeToIdentityStatusChangesListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = subscribeToIdentityStatusChangesListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue + } + } + } + } + open var subscribeToIdentityStatusChangesListenerCalled: Bool { + return subscribeToIdentityStatusChangesListenerCallsCount > 0 + } + open var subscribeToIdentityStatusChangesListenerReceivedListener: IdentityStatusChangeListener? + open var subscribeToIdentityStatusChangesListenerReceivedInvocations: [IdentityStatusChangeListener] = [] + + var subscribeToIdentityStatusChangesListenerUnderlyingReturnValue: TaskHandle! + open var subscribeToIdentityStatusChangesListenerReturnValue: TaskHandle! { + get { + if Thread.isMainThread { + return subscribeToIdentityStatusChangesListenerUnderlyingReturnValue + } else { + var returnValue: TaskHandle? = nil + DispatchQueue.main.sync { + returnValue = subscribeToIdentityStatusChangesListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue + } + } + } + } + open var subscribeToIdentityStatusChangesListenerClosure: ((IdentityStatusChangeListener) -> TaskHandle)? + + open override func subscribeToIdentityStatusChanges(listener: IdentityStatusChangeListener) -> TaskHandle { + subscribeToIdentityStatusChangesListenerCallsCount += 1 + subscribeToIdentityStatusChangesListenerReceivedListener = listener + DispatchQueue.main.async { + self.subscribeToIdentityStatusChangesListenerReceivedInvocations.append(listener) + } + if let subscribeToIdentityStatusChangesListenerClosure = subscribeToIdentityStatusChangesListenerClosure { + return subscribeToIdentityStatusChangesListenerClosure(listener) + } else { + return subscribeToIdentityStatusChangesListenerReturnValue + } + } + //MARK: - subscribeToRoomInfoUpdates var subscribeToRoomInfoUpdatesListenerUnderlyingCallsCount = 0 @@ -15234,124 +14552,38 @@ open class RoomDirectorySearchSDKMock: MatrixRustSDK.RoomDirectorySearch { resultsListenerUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - resultsListenerUnderlyingReturnValue = newValue - } - } - } - } - open var resultsListenerClosure: ((RoomDirectorySearchEntriesListener) async -> TaskHandle)? - - open override func results(listener: RoomDirectorySearchEntriesListener) async -> TaskHandle { - resultsListenerCallsCount += 1 - resultsListenerReceivedListener = listener - DispatchQueue.main.async { - self.resultsListenerReceivedInvocations.append(listener) - } - if let resultsListenerClosure = resultsListenerClosure { - return await resultsListenerClosure(listener) - } else { - return resultsListenerReturnValue - } - } - - //MARK: - search - - open var searchFilterBatchSizeThrowableError: Error? - var searchFilterBatchSizeUnderlyingCallsCount = 0 - open var searchFilterBatchSizeCallsCount: Int { - get { - if Thread.isMainThread { - return searchFilterBatchSizeUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = searchFilterBatchSizeUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - searchFilterBatchSizeUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - searchFilterBatchSizeUnderlyingCallsCount = newValue - } - } - } - } - open var searchFilterBatchSizeCalled: Bool { - return searchFilterBatchSizeCallsCount > 0 - } - open var searchFilterBatchSizeReceivedArguments: (filter: String?, batchSize: UInt32)? - open var searchFilterBatchSizeReceivedInvocations: [(filter: String?, batchSize: UInt32)] = [] - open var searchFilterBatchSizeClosure: ((String?, UInt32) async throws -> Void)? - - open override func search(filter: String?, batchSize: UInt32) async throws { - if let error = searchFilterBatchSizeThrowableError { - throw error - } - searchFilterBatchSizeCallsCount += 1 - searchFilterBatchSizeReceivedArguments = (filter: filter, batchSize: batchSize) - DispatchQueue.main.async { - self.searchFilterBatchSizeReceivedInvocations.append((filter: filter, batchSize: batchSize)) - } - try await searchFilterBatchSizeClosure?(filter, batchSize) - } -} -open class RoomListSDKMock: MatrixRustSDK.RoomList { - init() { - super.init(noPointer: .init()) - } - - public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - fatalError("init(unsafeFromRawPointer:) has not been implemented") - } - - fileprivate var pointer: UnsafeMutableRawPointer! - - //MARK: - entries - - var entriesListenerUnderlyingCallsCount = 0 - open var entriesListenerCallsCount: Int { - get { - if Thread.isMainThread { - return entriesListenerUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = entriesListenerUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - entriesListenerUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - entriesListenerUnderlyingCallsCount = newValue + resultsListenerUnderlyingReturnValue = newValue } } } } - open var entriesListenerCalled: Bool { - return entriesListenerCallsCount > 0 + open var resultsListenerClosure: ((RoomDirectorySearchEntriesListener) async -> TaskHandle)? + + open override func results(listener: RoomDirectorySearchEntriesListener) async -> TaskHandle { + resultsListenerCallsCount += 1 + resultsListenerReceivedListener = listener + DispatchQueue.main.async { + self.resultsListenerReceivedInvocations.append(listener) + } + if let resultsListenerClosure = resultsListenerClosure { + return await resultsListenerClosure(listener) + } else { + return resultsListenerReturnValue + } } - open var entriesListenerReceivedListener: RoomListEntriesListener? - open var entriesListenerReceivedInvocations: [RoomListEntriesListener] = [] - var entriesListenerUnderlyingReturnValue: TaskHandle! - open var entriesListenerReturnValue: TaskHandle! { + //MARK: - search + + open var searchFilterBatchSizeViaServerNameThrowableError: Error? + var searchFilterBatchSizeViaServerNameUnderlyingCallsCount = 0 + open var searchFilterBatchSizeViaServerNameCallsCount: Int { get { if Thread.isMainThread { - return entriesListenerUnderlyingReturnValue + return searchFilterBatchSizeViaServerNameUnderlyingCallsCount } else { - var returnValue: TaskHandle? = nil + var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = entriesListenerUnderlyingReturnValue + returnValue = searchFilterBatchSizeViaServerNameUnderlyingCallsCount } return returnValue! @@ -15359,29 +14591,44 @@ open class RoomListSDKMock: MatrixRustSDK.RoomList { } set { if Thread.isMainThread { - entriesListenerUnderlyingReturnValue = newValue + searchFilterBatchSizeViaServerNameUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - entriesListenerUnderlyingReturnValue = newValue + searchFilterBatchSizeViaServerNameUnderlyingCallsCount = newValue } } } } - open var entriesListenerClosure: ((RoomListEntriesListener) -> TaskHandle)? + open var searchFilterBatchSizeViaServerNameCalled: Bool { + return searchFilterBatchSizeViaServerNameCallsCount > 0 + } + open var searchFilterBatchSizeViaServerNameReceivedArguments: (filter: String?, batchSize: UInt32, viaServerName: String?)? + open var searchFilterBatchSizeViaServerNameReceivedInvocations: [(filter: String?, batchSize: UInt32, viaServerName: String?)] = [] + open var searchFilterBatchSizeViaServerNameClosure: ((String?, UInt32, String?) async throws -> Void)? - open override func entries(listener: RoomListEntriesListener) -> TaskHandle { - entriesListenerCallsCount += 1 - entriesListenerReceivedListener = listener - DispatchQueue.main.async { - self.entriesListenerReceivedInvocations.append(listener) + open override func search(filter: String?, batchSize: UInt32, viaServerName: String?) async throws { + if let error = searchFilterBatchSizeViaServerNameThrowableError { + throw error } - if let entriesListenerClosure = entriesListenerClosure { - return entriesListenerClosure(listener) - } else { - return entriesListenerReturnValue + searchFilterBatchSizeViaServerNameCallsCount += 1 + searchFilterBatchSizeViaServerNameReceivedArguments = (filter: filter, batchSize: batchSize, viaServerName: viaServerName) + DispatchQueue.main.async { + self.searchFilterBatchSizeViaServerNameReceivedInvocations.append((filter: filter, batchSize: batchSize, viaServerName: viaServerName)) } + try await searchFilterBatchSizeViaServerNameClosure?(filter, batchSize, viaServerName) + } +} +open class RoomListSDKMock: MatrixRustSDK.RoomList { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") } + fileprivate var pointer: UnsafeMutableRawPointer! + //MARK: - entriesWithDynamicAdapters var entriesWithDynamicAdaptersPageSizeListenerUnderlyingCallsCount = 0 @@ -16678,6 +15925,81 @@ open class RoomListItemSDKMock: MatrixRustSDK.RoomListItem { } } + //MARK: - previewRoom + + open var previewRoomViaThrowableError: Error? + var previewRoomViaUnderlyingCallsCount = 0 + open var previewRoomViaCallsCount: Int { + get { + if Thread.isMainThread { + return previewRoomViaUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = previewRoomViaUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + previewRoomViaUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + previewRoomViaUnderlyingCallsCount = newValue + } + } + } + } + open var previewRoomViaCalled: Bool { + return previewRoomViaCallsCount > 0 + } + open var previewRoomViaReceivedVia: [String]? + open var previewRoomViaReceivedInvocations: [[String]] = [] + + var previewRoomViaUnderlyingReturnValue: RoomPreview! + open var previewRoomViaReturnValue: RoomPreview! { + get { + if Thread.isMainThread { + return previewRoomViaUnderlyingReturnValue + } else { + var returnValue: RoomPreview? = nil + DispatchQueue.main.sync { + returnValue = previewRoomViaUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + previewRoomViaUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + previewRoomViaUnderlyingReturnValue = newValue + } + } + } + } + open var previewRoomViaClosure: (([String]) async throws -> RoomPreview)? + + open override func previewRoom(via: [String]) async throws -> RoomPreview { + if let error = previewRoomViaThrowableError { + throw error + } + previewRoomViaCallsCount += 1 + previewRoomViaReceivedVia = via + DispatchQueue.main.async { + self.previewRoomViaReceivedInvocations.append(via) + } + if let previewRoomViaClosure = previewRoomViaClosure { + return try await previewRoomViaClosure(via) + } else { + return previewRoomViaReturnValue + } + } + //MARK: - roomInfo open var roomInfoThrowableError: Error? @@ -16975,16 +16297,16 @@ open class RoomListServiceSDKMock: MatrixRustSDK.RoomListService { //MARK: - subscribeToRooms - open var subscribeToRoomsRoomIdsSettingsThrowableError: Error? - var subscribeToRoomsRoomIdsSettingsUnderlyingCallsCount = 0 - open var subscribeToRoomsRoomIdsSettingsCallsCount: Int { + open var subscribeToRoomsRoomIdsThrowableError: Error? + var subscribeToRoomsRoomIdsUnderlyingCallsCount = 0 + open var subscribeToRoomsRoomIdsCallsCount: Int { get { if Thread.isMainThread { - return subscribeToRoomsRoomIdsSettingsUnderlyingCallsCount + return subscribeToRoomsRoomIdsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = subscribeToRoomsRoomIdsSettingsUnderlyingCallsCount + returnValue = subscribeToRoomsRoomIdsUnderlyingCallsCount } return returnValue! @@ -16992,31 +16314,31 @@ open class RoomListServiceSDKMock: MatrixRustSDK.RoomListService { } set { if Thread.isMainThread { - subscribeToRoomsRoomIdsSettingsUnderlyingCallsCount = newValue + subscribeToRoomsRoomIdsUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - subscribeToRoomsRoomIdsSettingsUnderlyingCallsCount = newValue + subscribeToRoomsRoomIdsUnderlyingCallsCount = newValue } } } } - open var subscribeToRoomsRoomIdsSettingsCalled: Bool { - return subscribeToRoomsRoomIdsSettingsCallsCount > 0 + open var subscribeToRoomsRoomIdsCalled: Bool { + return subscribeToRoomsRoomIdsCallsCount > 0 } - open var subscribeToRoomsRoomIdsSettingsReceivedArguments: (roomIds: [String], settings: RoomSubscription?)? - open var subscribeToRoomsRoomIdsSettingsReceivedInvocations: [(roomIds: [String], settings: RoomSubscription?)] = [] - open var subscribeToRoomsRoomIdsSettingsClosure: (([String], RoomSubscription?) throws -> Void)? + open var subscribeToRoomsRoomIdsReceivedRoomIds: [String]? + open var subscribeToRoomsRoomIdsReceivedInvocations: [[String]] = [] + open var subscribeToRoomsRoomIdsClosure: (([String]) throws -> Void)? - open override func subscribeToRooms(roomIds: [String], settings: RoomSubscription?) throws { - if let error = subscribeToRoomsRoomIdsSettingsThrowableError { + open override func subscribeToRooms(roomIds: [String]) throws { + if let error = subscribeToRoomsRoomIdsThrowableError { throw error } - subscribeToRoomsRoomIdsSettingsCallsCount += 1 - subscribeToRoomsRoomIdsSettingsReceivedArguments = (roomIds: roomIds, settings: settings) + subscribeToRoomsRoomIdsCallsCount += 1 + subscribeToRoomsRoomIdsReceivedRoomIds = roomIds DispatchQueue.main.async { - self.subscribeToRoomsRoomIdsSettingsReceivedInvocations.append((roomIds: roomIds, settings: settings)) + self.subscribeToRoomsRoomIdsReceivedInvocations.append(roomIds) } - try subscribeToRoomsRoomIdsSettingsClosure?(roomIds, settings) + try subscribeToRoomsRoomIdsClosure?(roomIds) } //MARK: - syncIndicator @@ -17319,6 +16641,126 @@ open class RoomMessageEventContentWithoutRelationSDKMock: MatrixRustSDK.RoomMess } } } +open class RoomPreviewSDKMock: MatrixRustSDK.RoomPreview { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } + + fileprivate var pointer: UnsafeMutableRawPointer! + + //MARK: - info + + open var infoThrowableError: Error? + var infoUnderlyingCallsCount = 0 + open var infoCallsCount: Int { + get { + if Thread.isMainThread { + return infoUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = infoUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + infoUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + infoUnderlyingCallsCount = newValue + } + } + } + } + open var infoCalled: Bool { + return infoCallsCount > 0 + } + + var infoUnderlyingReturnValue: RoomPreviewInfo! + open var infoReturnValue: RoomPreviewInfo! { + get { + if Thread.isMainThread { + return infoUnderlyingReturnValue + } else { + var returnValue: RoomPreviewInfo? = nil + DispatchQueue.main.sync { + returnValue = infoUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + infoUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + infoUnderlyingReturnValue = newValue + } + } + } + } + open var infoClosure: (() throws -> RoomPreviewInfo)? + + open override func info() throws -> RoomPreviewInfo { + if let error = infoThrowableError { + throw error + } + infoCallsCount += 1 + if let infoClosure = infoClosure { + return try infoClosure() + } else { + return infoReturnValue + } + } + + //MARK: - leave + + open var leaveThrowableError: Error? + var leaveUnderlyingCallsCount = 0 + open var leaveCallsCount: Int { + get { + if Thread.isMainThread { + return leaveUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = leaveUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + leaveUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + leaveUnderlyingCallsCount = newValue + } + } + } + } + open var leaveCalled: Bool { + return leaveCallsCount > 0 + } + open var leaveClosure: (() async throws -> Void)? + + open override func leave() async throws { + if let error = leaveThrowableError { + throw error + } + leaveCallsCount += 1 + try await leaveClosure?() + } +} open class SendAttachmentJoinHandleSDKMock: MatrixRustSDK.SendAttachmentJoinHandle { init() { super.init(noPointer: .init()) @@ -17402,11 +16844,91 @@ open class SendAttachmentJoinHandleSDKMock: MatrixRustSDK.SendAttachmentJoinHand if let error = joinThrowableError { throw error } - joinCallsCount += 1 - try await joinClosure?() + joinCallsCount += 1 + try await joinClosure?() + } +} +open class SendHandleSDKMock: MatrixRustSDK.SendHandle { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } + + fileprivate var pointer: UnsafeMutableRawPointer! + + //MARK: - abort + + open var abortThrowableError: Error? + var abortUnderlyingCallsCount = 0 + open var abortCallsCount: Int { + get { + if Thread.isMainThread { + return abortUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = abortUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + abortUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + abortUnderlyingCallsCount = newValue + } + } + } + } + open var abortCalled: Bool { + return abortCallsCount > 0 + } + + var abortUnderlyingReturnValue: Bool! + open var abortReturnValue: Bool! { + get { + if Thread.isMainThread { + return abortUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = abortUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + abortUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + abortUnderlyingReturnValue = newValue + } + } + } + } + open var abortClosure: (() async throws -> Bool)? + + open override func abort() async throws -> Bool { + if let error = abortThrowableError { + throw error + } + abortCallsCount += 1 + if let abortClosure = abortClosure { + return try await abortClosure() + } else { + return abortReturnValue + } } } -open class SendHandleSDKMock: MatrixRustSDK.SendHandle { +open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificationController { init() { super.init(noPointer: .init()) } @@ -17417,18 +16939,18 @@ open class SendHandleSDKMock: MatrixRustSDK.SendHandle { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - abort + //MARK: - acceptVerificationRequest - open var abortThrowableError: Error? - var abortUnderlyingCallsCount = 0 - open var abortCallsCount: Int { + open var acceptVerificationRequestThrowableError: Error? + var acceptVerificationRequestUnderlyingCallsCount = 0 + open var acceptVerificationRequestCallsCount: Int { get { if Thread.isMainThread { - return abortUnderlyingCallsCount + return acceptVerificationRequestUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = abortUnderlyingCallsCount + returnValue = acceptVerificationRequestUnderlyingCallsCount } return returnValue! @@ -17436,27 +16958,39 @@ open class SendHandleSDKMock: MatrixRustSDK.SendHandle { } set { if Thread.isMainThread { - abortUnderlyingCallsCount = newValue + acceptVerificationRequestUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - abortUnderlyingCallsCount = newValue + acceptVerificationRequestUnderlyingCallsCount = newValue } } } } - open var abortCalled: Bool { - return abortCallsCount > 0 + open var acceptVerificationRequestCalled: Bool { + return acceptVerificationRequestCallsCount > 0 } + open var acceptVerificationRequestClosure: (() async throws -> Void)? - var abortUnderlyingReturnValue: Bool! - open var abortReturnValue: Bool! { + open override func acceptVerificationRequest() async throws { + if let error = acceptVerificationRequestThrowableError { + throw error + } + acceptVerificationRequestCallsCount += 1 + try await acceptVerificationRequestClosure?() + } + + //MARK: - acknowledgeVerificationRequest + + open var acknowledgeVerificationRequestSenderIdFlowIdThrowableError: Error? + var acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = 0 + open var acknowledgeVerificationRequestSenderIdFlowIdCallsCount: Int { get { if Thread.isMainThread { - return abortUnderlyingReturnValue + return acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount } else { - var returnValue: Bool? = nil + var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = abortUnderlyingReturnValue + returnValue = acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount } return returnValue! @@ -17464,38 +16998,32 @@ open class SendHandleSDKMock: MatrixRustSDK.SendHandle { } set { if Thread.isMainThread { - abortUnderlyingReturnValue = newValue + acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - abortUnderlyingReturnValue = newValue + acknowledgeVerificationRequestSenderIdFlowIdUnderlyingCallsCount = newValue } } } } - open var abortClosure: (() async throws -> Bool)? + open var acknowledgeVerificationRequestSenderIdFlowIdCalled: Bool { + return acknowledgeVerificationRequestSenderIdFlowIdCallsCount > 0 + } + open var acknowledgeVerificationRequestSenderIdFlowIdReceivedArguments: (senderId: String, flowId: String)? + open var acknowledgeVerificationRequestSenderIdFlowIdReceivedInvocations: [(senderId: String, flowId: String)] = [] + open var acknowledgeVerificationRequestSenderIdFlowIdClosure: ((String, String) async throws -> Void)? - open override func abort() async throws -> Bool { - if let error = abortThrowableError { + open override func acknowledgeVerificationRequest(senderId: String, flowId: String) async throws { + if let error = acknowledgeVerificationRequestSenderIdFlowIdThrowableError { throw error } - abortCallsCount += 1 - if let abortClosure = abortClosure { - return try await abortClosure() - } else { - return abortReturnValue + acknowledgeVerificationRequestSenderIdFlowIdCallsCount += 1 + acknowledgeVerificationRequestSenderIdFlowIdReceivedArguments = (senderId: senderId, flowId: flowId) + DispatchQueue.main.async { + self.acknowledgeVerificationRequestSenderIdFlowIdReceivedInvocations.append((senderId: senderId, flowId: flowId)) } + try await acknowledgeVerificationRequestSenderIdFlowIdClosure?(senderId, flowId) } -} -open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificationController { - init() { - super.init(noPointer: .init()) - } - - public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - fatalError("init(unsafeFromRawPointer:) has not been implemented") - } - - fileprivate var pointer: UnsafeMutableRawPointer! //MARK: - approveVerification @@ -17617,75 +17145,6 @@ open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificati try await declineVerificationClosure?() } - //MARK: - isVerified - - open var isVerifiedThrowableError: Error? - var isVerifiedUnderlyingCallsCount = 0 - open var isVerifiedCallsCount: Int { - get { - if Thread.isMainThread { - return isVerifiedUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = isVerifiedUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isVerifiedUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - isVerifiedUnderlyingCallsCount = newValue - } - } - } - } - open var isVerifiedCalled: Bool { - return isVerifiedCallsCount > 0 - } - - var isVerifiedUnderlyingReturnValue: Bool! - open var isVerifiedReturnValue: Bool! { - get { - if Thread.isMainThread { - return isVerifiedUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = isVerifiedUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - isVerifiedUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - isVerifiedUnderlyingReturnValue = newValue - } - } - } - } - open var isVerifiedClosure: (() async throws -> Bool)? - - open override func isVerified() async throws -> Bool { - if let error = isVerifiedThrowableError { - throw error - } - isVerifiedCallsCount += 1 - if let isVerifiedClosure = isVerifiedClosure { - return try await isVerifiedClosure() - } else { - return isVerifiedReturnValue - } - } - //MARK: - requestVerification open var requestVerificationThrowableError: Error? @@ -18861,6 +18320,77 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } } + //MARK: - createMessageContent + + var createMessageContentMsgTypeUnderlyingCallsCount = 0 + open var createMessageContentMsgTypeCallsCount: Int { + get { + if Thread.isMainThread { + return createMessageContentMsgTypeUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = createMessageContentMsgTypeUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + createMessageContentMsgTypeUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + createMessageContentMsgTypeUnderlyingCallsCount = newValue + } + } + } + } + open var createMessageContentMsgTypeCalled: Bool { + return createMessageContentMsgTypeCallsCount > 0 + } + open var createMessageContentMsgTypeReceivedMsgType: MessageType? + open var createMessageContentMsgTypeReceivedInvocations: [MessageType] = [] + + var createMessageContentMsgTypeUnderlyingReturnValue: RoomMessageEventContentWithoutRelation? + open var createMessageContentMsgTypeReturnValue: RoomMessageEventContentWithoutRelation? { + get { + if Thread.isMainThread { + return createMessageContentMsgTypeUnderlyingReturnValue + } else { + var returnValue: RoomMessageEventContentWithoutRelation?? = nil + DispatchQueue.main.sync { + returnValue = createMessageContentMsgTypeUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + createMessageContentMsgTypeUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + createMessageContentMsgTypeUnderlyingReturnValue = newValue + } + } + } + } + open var createMessageContentMsgTypeClosure: ((MessageType) -> RoomMessageEventContentWithoutRelation?)? + + open override func createMessageContent(msgType: MessageType) -> RoomMessageEventContentWithoutRelation? { + createMessageContentMsgTypeCallsCount += 1 + createMessageContentMsgTypeReceivedMsgType = msgType + DispatchQueue.main.async { + self.createMessageContentMsgTypeReceivedInvocations.append(msgType) + } + if let createMessageContentMsgTypeClosure = createMessageContentMsgTypeClosure { + return createMessageContentMsgTypeClosure(msgType) + } else { + return createMessageContentMsgTypeReturnValue + } + } + //MARK: - createPoll open var createPollQuestionAnswersMaxSelectionsPollKindThrowableError: Error? @@ -18909,16 +18439,16 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - edit - open var editItemNewContentThrowableError: Error? - var editItemNewContentUnderlyingCallsCount = 0 - open var editItemNewContentCallsCount: Int { + open var editEventOrTransactionIdNewContentThrowableError: Error? + var editEventOrTransactionIdNewContentUnderlyingCallsCount = 0 + open var editEventOrTransactionIdNewContentCallsCount: Int { get { if Thread.isMainThread { - return editItemNewContentUnderlyingCallsCount + return editEventOrTransactionIdNewContentUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = editItemNewContentUnderlyingCallsCount + returnValue = editEventOrTransactionIdNewContentUnderlyingCallsCount } return returnValue! @@ -18926,74 +18456,45 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - editItemNewContentUnderlyingCallsCount = newValue + editEventOrTransactionIdNewContentUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - editItemNewContentUnderlyingCallsCount = newValue + editEventOrTransactionIdNewContentUnderlyingCallsCount = newValue } } } } - open var editItemNewContentCalled: Bool { - return editItemNewContentCallsCount > 0 + open var editEventOrTransactionIdNewContentCalled: Bool { + return editEventOrTransactionIdNewContentCallsCount > 0 } - open var editItemNewContentReceivedArguments: (item: EventTimelineItem, newContent: EditedContent)? - open var editItemNewContentReceivedInvocations: [(item: EventTimelineItem, newContent: EditedContent)] = [] + open var editEventOrTransactionIdNewContentReceivedArguments: (eventOrTransactionId: EventOrTransactionId, newContent: EditedContent)? + open var editEventOrTransactionIdNewContentReceivedInvocations: [(eventOrTransactionId: EventOrTransactionId, newContent: EditedContent)] = [] + open var editEventOrTransactionIdNewContentClosure: ((EventOrTransactionId, EditedContent) async throws -> Void)? - var editItemNewContentUnderlyingReturnValue: Bool! - open var editItemNewContentReturnValue: Bool! { - get { - if Thread.isMainThread { - return editItemNewContentUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = editItemNewContentUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - editItemNewContentUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - editItemNewContentUnderlyingReturnValue = newValue - } - } - } - } - open var editItemNewContentClosure: ((EventTimelineItem, EditedContent) async throws -> Bool)? - - open override func edit(item: EventTimelineItem, newContent: EditedContent) async throws -> Bool { - if let error = editItemNewContentThrowableError { + open override func edit(eventOrTransactionId: EventOrTransactionId, newContent: EditedContent) async throws { + if let error = editEventOrTransactionIdNewContentThrowableError { throw error } - editItemNewContentCallsCount += 1 - editItemNewContentReceivedArguments = (item: item, newContent: newContent) + editEventOrTransactionIdNewContentCallsCount += 1 + editEventOrTransactionIdNewContentReceivedArguments = (eventOrTransactionId: eventOrTransactionId, newContent: newContent) DispatchQueue.main.async { - self.editItemNewContentReceivedInvocations.append((item: item, newContent: newContent)) - } - if let editItemNewContentClosure = editItemNewContentClosure { - return try await editItemNewContentClosure(item, newContent) - } else { - return editItemNewContentReturnValue + self.editEventOrTransactionIdNewContentReceivedInvocations.append((eventOrTransactionId: eventOrTransactionId, newContent: newContent)) } + try await editEventOrTransactionIdNewContentClosure?(eventOrTransactionId, newContent) } //MARK: - endPoll - open var endPollPollStartIdTextThrowableError: Error? - var endPollPollStartIdTextUnderlyingCallsCount = 0 - open var endPollPollStartIdTextCallsCount: Int { + open var endPollPollStartEventIdTextThrowableError: Error? + var endPollPollStartEventIdTextUnderlyingCallsCount = 0 + open var endPollPollStartEventIdTextCallsCount: Int { get { if Thread.isMainThread { - return endPollPollStartIdTextUnderlyingCallsCount + return endPollPollStartEventIdTextUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = endPollPollStartIdTextUnderlyingCallsCount + returnValue = endPollPollStartEventIdTextUnderlyingCallsCount } return returnValue! @@ -19001,31 +18502,31 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - endPollPollStartIdTextUnderlyingCallsCount = newValue + endPollPollStartEventIdTextUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - endPollPollStartIdTextUnderlyingCallsCount = newValue + endPollPollStartEventIdTextUnderlyingCallsCount = newValue } } } } - open var endPollPollStartIdTextCalled: Bool { - return endPollPollStartIdTextCallsCount > 0 + open var endPollPollStartEventIdTextCalled: Bool { + return endPollPollStartEventIdTextCallsCount > 0 } - open var endPollPollStartIdTextReceivedArguments: (pollStartId: String, text: String)? - open var endPollPollStartIdTextReceivedInvocations: [(pollStartId: String, text: String)] = [] - open var endPollPollStartIdTextClosure: ((String, String) throws -> Void)? + open var endPollPollStartEventIdTextReceivedArguments: (pollStartEventId: String, text: String)? + open var endPollPollStartEventIdTextReceivedInvocations: [(pollStartEventId: String, text: String)] = [] + open var endPollPollStartEventIdTextClosure: ((String, String) throws -> Void)? - open override func endPoll(pollStartId: String, text: String) throws { - if let error = endPollPollStartIdTextThrowableError { + open override func endPoll(pollStartEventId: String, text: String) throws { + if let error = endPollPollStartEventIdTextThrowableError { throw error } - endPollPollStartIdTextCallsCount += 1 - endPollPollStartIdTextReceivedArguments = (pollStartId: pollStartId, text: text) + endPollPollStartEventIdTextCallsCount += 1 + endPollPollStartEventIdTextReceivedArguments = (pollStartEventId: pollStartEventId, text: text) DispatchQueue.main.async { - self.endPollPollStartIdTextReceivedInvocations.append((pollStartId: pollStartId, text: text)) + self.endPollPollStartEventIdTextReceivedInvocations.append((pollStartEventId: pollStartEventId, text: text)) } - try endPollPollStartIdTextClosure?(pollStartId, text) + try endPollPollStartEventIdTextClosure?(pollStartEventId, text) } //MARK: - fetchDetailsForEvent @@ -19176,102 +18677,27 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { focusedPaginateForwardsNumEventsCallsCount += 1 focusedPaginateForwardsNumEventsReceivedNumEvents = numEvents DispatchQueue.main.async { - self.focusedPaginateForwardsNumEventsReceivedInvocations.append(numEvents) - } - if let focusedPaginateForwardsNumEventsClosure = focusedPaginateForwardsNumEventsClosure { - return try await focusedPaginateForwardsNumEventsClosure(numEvents) - } else { - return focusedPaginateForwardsNumEventsReturnValue - } - } - - //MARK: - getEventTimelineItemByEventId - - open var getEventTimelineItemByEventIdEventIdThrowableError: Error? - var getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = 0 - open var getEventTimelineItemByEventIdEventIdCallsCount: Int { - get { - if Thread.isMainThread { - return getEventTimelineItemByEventIdEventIdUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = getEventTimelineItemByEventIdEventIdUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = newValue - } - } - } - } - open var getEventTimelineItemByEventIdEventIdCalled: Bool { - return getEventTimelineItemByEventIdEventIdCallsCount > 0 - } - open var getEventTimelineItemByEventIdEventIdReceivedEventId: String? - open var getEventTimelineItemByEventIdEventIdReceivedInvocations: [String] = [] - - var getEventTimelineItemByEventIdEventIdUnderlyingReturnValue: EventTimelineItem! - open var getEventTimelineItemByEventIdEventIdReturnValue: EventTimelineItem! { - get { - if Thread.isMainThread { - return getEventTimelineItemByEventIdEventIdUnderlyingReturnValue - } else { - var returnValue: EventTimelineItem? = nil - DispatchQueue.main.sync { - returnValue = getEventTimelineItemByEventIdEventIdUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - getEventTimelineItemByEventIdEventIdUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - getEventTimelineItemByEventIdEventIdUnderlyingReturnValue = newValue - } - } - } - } - open var getEventTimelineItemByEventIdEventIdClosure: ((String) async throws -> EventTimelineItem)? - - open override func getEventTimelineItemByEventId(eventId: String) async throws -> EventTimelineItem { - if let error = getEventTimelineItemByEventIdEventIdThrowableError { - throw error - } - getEventTimelineItemByEventIdEventIdCallsCount += 1 - getEventTimelineItemByEventIdEventIdReceivedEventId = eventId - DispatchQueue.main.async { - self.getEventTimelineItemByEventIdEventIdReceivedInvocations.append(eventId) + self.focusedPaginateForwardsNumEventsReceivedInvocations.append(numEvents) } - if let getEventTimelineItemByEventIdEventIdClosure = getEventTimelineItemByEventIdEventIdClosure { - return try await getEventTimelineItemByEventIdEventIdClosure(eventId) + if let focusedPaginateForwardsNumEventsClosure = focusedPaginateForwardsNumEventsClosure { + return try await focusedPaginateForwardsNumEventsClosure(numEvents) } else { - return getEventTimelineItemByEventIdEventIdReturnValue + return focusedPaginateForwardsNumEventsReturnValue } } - //MARK: - getEventTimelineItemByTransactionId + //MARK: - getEventTimelineItemByEventId - open var getEventTimelineItemByTransactionIdTransactionIdThrowableError: Error? - var getEventTimelineItemByTransactionIdTransactionIdUnderlyingCallsCount = 0 - open var getEventTimelineItemByTransactionIdTransactionIdCallsCount: Int { + open var getEventTimelineItemByEventIdEventIdThrowableError: Error? + var getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = 0 + open var getEventTimelineItemByEventIdEventIdCallsCount: Int { get { if Thread.isMainThread { - return getEventTimelineItemByTransactionIdTransactionIdUnderlyingCallsCount + return getEventTimelineItemByEventIdEventIdUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = getEventTimelineItemByTransactionIdTransactionIdUnderlyingCallsCount + returnValue = getEventTimelineItemByEventIdEventIdUnderlyingCallsCount } return returnValue! @@ -19279,29 +18705,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - getEventTimelineItemByTransactionIdTransactionIdUnderlyingCallsCount = newValue + getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - getEventTimelineItemByTransactionIdTransactionIdUnderlyingCallsCount = newValue + getEventTimelineItemByEventIdEventIdUnderlyingCallsCount = newValue } } } } - open var getEventTimelineItemByTransactionIdTransactionIdCalled: Bool { - return getEventTimelineItemByTransactionIdTransactionIdCallsCount > 0 + open var getEventTimelineItemByEventIdEventIdCalled: Bool { + return getEventTimelineItemByEventIdEventIdCallsCount > 0 } - open var getEventTimelineItemByTransactionIdTransactionIdReceivedTransactionId: String? - open var getEventTimelineItemByTransactionIdTransactionIdReceivedInvocations: [String] = [] + open var getEventTimelineItemByEventIdEventIdReceivedEventId: String? + open var getEventTimelineItemByEventIdEventIdReceivedInvocations: [String] = [] - var getEventTimelineItemByTransactionIdTransactionIdUnderlyingReturnValue: EventTimelineItem! - open var getEventTimelineItemByTransactionIdTransactionIdReturnValue: EventTimelineItem! { + var getEventTimelineItemByEventIdEventIdUnderlyingReturnValue: EventTimelineItem! + open var getEventTimelineItemByEventIdEventIdReturnValue: EventTimelineItem! { get { if Thread.isMainThread { - return getEventTimelineItemByTransactionIdTransactionIdUnderlyingReturnValue + return getEventTimelineItemByEventIdEventIdUnderlyingReturnValue } else { var returnValue: EventTimelineItem? = nil DispatchQueue.main.sync { - returnValue = getEventTimelineItemByTransactionIdTransactionIdUnderlyingReturnValue + returnValue = getEventTimelineItemByEventIdEventIdUnderlyingReturnValue } return returnValue! @@ -19309,29 +18735,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - getEventTimelineItemByTransactionIdTransactionIdUnderlyingReturnValue = newValue + getEventTimelineItemByEventIdEventIdUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - getEventTimelineItemByTransactionIdTransactionIdUnderlyingReturnValue = newValue + getEventTimelineItemByEventIdEventIdUnderlyingReturnValue = newValue } } } } - open var getEventTimelineItemByTransactionIdTransactionIdClosure: ((String) async throws -> EventTimelineItem)? + open var getEventTimelineItemByEventIdEventIdClosure: ((String) async throws -> EventTimelineItem)? - open override func getEventTimelineItemByTransactionId(transactionId: String) async throws -> EventTimelineItem { - if let error = getEventTimelineItemByTransactionIdTransactionIdThrowableError { + open override func getEventTimelineItemByEventId(eventId: String) async throws -> EventTimelineItem { + if let error = getEventTimelineItemByEventIdEventIdThrowableError { throw error } - getEventTimelineItemByTransactionIdTransactionIdCallsCount += 1 - getEventTimelineItemByTransactionIdTransactionIdReceivedTransactionId = transactionId + getEventTimelineItemByEventIdEventIdCallsCount += 1 + getEventTimelineItemByEventIdEventIdReceivedEventId = eventId DispatchQueue.main.async { - self.getEventTimelineItemByTransactionIdTransactionIdReceivedInvocations.append(transactionId) + self.getEventTimelineItemByEventIdEventIdReceivedInvocations.append(eventId) } - if let getEventTimelineItemByTransactionIdTransactionIdClosure = getEventTimelineItemByTransactionIdTransactionIdClosure { - return try await getEventTimelineItemByTransactionIdTransactionIdClosure(transactionId) + if let getEventTimelineItemByEventIdEventIdClosure = getEventTimelineItemByEventIdEventIdClosure { + return try await getEventTimelineItemByEventIdEventIdClosure(eventId) } else { - return getEventTimelineItemByTransactionIdTransactionIdReturnValue + return getEventTimelineItemByEventIdEventIdReturnValue } } @@ -19608,16 +19034,16 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - redactEvent - open var redactEventItemReasonThrowableError: Error? - var redactEventItemReasonUnderlyingCallsCount = 0 - open var redactEventItemReasonCallsCount: Int { + open var redactEventEventOrTransactionIdReasonThrowableError: Error? + var redactEventEventOrTransactionIdReasonUnderlyingCallsCount = 0 + open var redactEventEventOrTransactionIdReasonCallsCount: Int { get { if Thread.isMainThread { - return redactEventItemReasonUnderlyingCallsCount + return redactEventEventOrTransactionIdReasonUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = redactEventItemReasonUnderlyingCallsCount + returnValue = redactEventEventOrTransactionIdReasonUnderlyingCallsCount } return returnValue! @@ -19625,60 +19051,31 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - redactEventItemReasonUnderlyingCallsCount = newValue + redactEventEventOrTransactionIdReasonUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - redactEventItemReasonUnderlyingCallsCount = newValue + redactEventEventOrTransactionIdReasonUnderlyingCallsCount = newValue } } } } - open var redactEventItemReasonCalled: Bool { - return redactEventItemReasonCallsCount > 0 - } - open var redactEventItemReasonReceivedArguments: (item: EventTimelineItem, reason: String?)? - open var redactEventItemReasonReceivedInvocations: [(item: EventTimelineItem, reason: String?)] = [] - - var redactEventItemReasonUnderlyingReturnValue: Bool! - open var redactEventItemReasonReturnValue: Bool! { - get { - if Thread.isMainThread { - return redactEventItemReasonUnderlyingReturnValue - } else { - var returnValue: Bool? = nil - DispatchQueue.main.sync { - returnValue = redactEventItemReasonUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - redactEventItemReasonUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - redactEventItemReasonUnderlyingReturnValue = newValue - } - } - } + open var redactEventEventOrTransactionIdReasonCalled: Bool { + return redactEventEventOrTransactionIdReasonCallsCount > 0 } - open var redactEventItemReasonClosure: ((EventTimelineItem, String?) async throws -> Bool)? + open var redactEventEventOrTransactionIdReasonReceivedArguments: (eventOrTransactionId: EventOrTransactionId, reason: String?)? + open var redactEventEventOrTransactionIdReasonReceivedInvocations: [(eventOrTransactionId: EventOrTransactionId, reason: String?)] = [] + open var redactEventEventOrTransactionIdReasonClosure: ((EventOrTransactionId, String?) async throws -> Void)? - open override func redactEvent(item: EventTimelineItem, reason: String?) async throws -> Bool { - if let error = redactEventItemReasonThrowableError { + open override func redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?) async throws { + if let error = redactEventEventOrTransactionIdReasonThrowableError { throw error } - redactEventItemReasonCallsCount += 1 - redactEventItemReasonReceivedArguments = (item: item, reason: reason) + redactEventEventOrTransactionIdReasonCallsCount += 1 + redactEventEventOrTransactionIdReasonReceivedArguments = (eventOrTransactionId: eventOrTransactionId, reason: reason) DispatchQueue.main.async { - self.redactEventItemReasonReceivedInvocations.append((item: item, reason: reason)) - } - if let redactEventItemReasonClosure = redactEventItemReasonClosure { - return try await redactEventItemReasonClosure(item, reason) - } else { - return redactEventItemReasonReturnValue + self.redactEventEventOrTransactionIdReasonReceivedInvocations.append((eventOrTransactionId: eventOrTransactionId, reason: reason)) } + try await redactEventEventOrTransactionIdReasonClosure?(eventOrTransactionId, reason) } //MARK: - retryDecryption @@ -19800,15 +19197,15 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - sendAudio - var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = 0 - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherCallsCount: Int { + var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + returnValue = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -19816,29 +19213,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherCalled: Bool { - return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherCallsCount > 0 + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCalled: Bool { + return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount > 0 } - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReceivedArguments: (url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)? - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations: [(url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)] = [] + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments: (url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations: [(url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReturnValue: SendAttachmentJoinHandle! { + var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + returnValue = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -19846,40 +19243,40 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherClosure: ((String, AudioInfo, String?, FormattedBody?, ProgressWatcher?) -> SendAttachmentJoinHandle)? + open var sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure: ((String, AudioInfo, String?, FormattedBody?, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendAudio(url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?) -> SendAttachmentJoinHandle { - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherCallsCount += 1 - sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReceivedArguments = (url: url, audioInfo: audioInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher) + open override func sendAudio(url: String, audioInfo: AudioInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount += 1 + sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments = (url: url, audioInfo: audioInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations.append((url: url, audioInfo: audioInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher)) + self.sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations.append((url: url, audioInfo: audioInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherClosure = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherClosure { - return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherClosure(url, audioInfo, caption, formattedCaption, progressWatcher) + if let sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure = sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure { + return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure(url, audioInfo, caption, formattedCaption, progressWatcher, useSendQueue) } else { - return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherReturnValue + return sendAudioUrlAudioInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue } } //MARK: - sendFile - var sendFileUrlFileInfoProgressWatcherUnderlyingCallsCount = 0 - open var sendFileUrlFileInfoProgressWatcherCallsCount: Int { + var sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressWatcherUnderlyingCallsCount + return sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressWatcherUnderlyingCallsCount + returnValue = sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -19887,29 +19284,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressWatcherUnderlyingCallsCount = newValue + sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressWatcherUnderlyingCallsCount = newValue + sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendFileUrlFileInfoProgressWatcherCalled: Bool { - return sendFileUrlFileInfoProgressWatcherCallsCount > 0 + open var sendFileUrlFileInfoProgressWatcherUseSendQueueCalled: Bool { + return sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount > 0 } - open var sendFileUrlFileInfoProgressWatcherReceivedArguments: (url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?)? - open var sendFileUrlFileInfoProgressWatcherReceivedInvocations: [(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?)] = [] + open var sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedArguments: (url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedInvocations: [(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendFileUrlFileInfoProgressWatcherUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendFileUrlFileInfoProgressWatcherReturnValue: SendAttachmentJoinHandle! { + var sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendFileUrlFileInfoProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressWatcherUnderlyingReturnValue + return sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressWatcherUnderlyingReturnValue + returnValue = sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -19917,40 +19314,40 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressWatcherUnderlyingReturnValue = newValue + sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressWatcherUnderlyingReturnValue = newValue + sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendFileUrlFileInfoProgressWatcherClosure: ((String, FileInfo, ProgressWatcher?) -> SendAttachmentJoinHandle)? + open var sendFileUrlFileInfoProgressWatcherUseSendQueueClosure: ((String, FileInfo, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendFile(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?) -> SendAttachmentJoinHandle { - sendFileUrlFileInfoProgressWatcherCallsCount += 1 - sendFileUrlFileInfoProgressWatcherReceivedArguments = (url: url, fileInfo: fileInfo, progressWatcher: progressWatcher) + open override func sendFile(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount += 1 + sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedArguments = (url: url, fileInfo: fileInfo, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendFileUrlFileInfoProgressWatcherReceivedInvocations.append((url: url, fileInfo: fileInfo, progressWatcher: progressWatcher)) + self.sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedInvocations.append((url: url, fileInfo: fileInfo, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendFileUrlFileInfoProgressWatcherClosure = sendFileUrlFileInfoProgressWatcherClosure { - return sendFileUrlFileInfoProgressWatcherClosure(url, fileInfo, progressWatcher) + if let sendFileUrlFileInfoProgressWatcherUseSendQueueClosure = sendFileUrlFileInfoProgressWatcherUseSendQueueClosure { + return sendFileUrlFileInfoProgressWatcherUseSendQueueClosure(url, fileInfo, progressWatcher, useSendQueue) } else { - return sendFileUrlFileInfoProgressWatcherReturnValue + return sendFileUrlFileInfoProgressWatcherUseSendQueueReturnValue } } //MARK: - sendImage - var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = 0 - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherCallsCount: Int { + var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + returnValue = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -19958,29 +19355,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherCalled: Bool { - return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherCallsCount > 0 + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCalled: Bool { + return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount > 0 } - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReceivedArguments: (url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)? - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations: [(url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)] = [] + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments: (url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations: [(url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReturnValue: SendAttachmentJoinHandle! { + var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + returnValue = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -19988,26 +19385,26 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherClosure: ((String, String?, ImageInfo, String?, FormattedBody?, ProgressWatcher?) -> SendAttachmentJoinHandle)? + open var sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure: ((String, String?, ImageInfo, String?, FormattedBody?, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendImage(url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?) -> SendAttachmentJoinHandle { - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherCallsCount += 1 - sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReceivedArguments = (url: url, thumbnailUrl: thumbnailUrl, imageInfo: imageInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher) + open override func sendImage(url: String, thumbnailUrl: String?, imageInfo: ImageInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount += 1 + sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments = (url: url, thumbnailUrl: thumbnailUrl, imageInfo: imageInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations.append((url: url, thumbnailUrl: thumbnailUrl, imageInfo: imageInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher)) + self.sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations.append((url: url, thumbnailUrl: thumbnailUrl, imageInfo: imageInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherClosure = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherClosure { - return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherClosure(url, thumbnailUrl, imageInfo, caption, formattedCaption, progressWatcher) + if let sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure = sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure { + return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure(url, thumbnailUrl, imageInfo, caption, formattedCaption, progressWatcher, useSendQueue) } else { - return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherReturnValue + return sendImageUrlThumbnailUrlImageInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue } } @@ -20055,16 +19452,16 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - sendPollResponse - open var sendPollResponsePollStartIdAnswersThrowableError: Error? - var sendPollResponsePollStartIdAnswersUnderlyingCallsCount = 0 - open var sendPollResponsePollStartIdAnswersCallsCount: Int { + open var sendPollResponsePollStartEventIdAnswersThrowableError: Error? + var sendPollResponsePollStartEventIdAnswersUnderlyingCallsCount = 0 + open var sendPollResponsePollStartEventIdAnswersCallsCount: Int { get { if Thread.isMainThread { - return sendPollResponsePollStartIdAnswersUnderlyingCallsCount + return sendPollResponsePollStartEventIdAnswersUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendPollResponsePollStartIdAnswersUnderlyingCallsCount + returnValue = sendPollResponsePollStartEventIdAnswersUnderlyingCallsCount } return returnValue! @@ -20072,31 +19469,31 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendPollResponsePollStartIdAnswersUnderlyingCallsCount = newValue + sendPollResponsePollStartEventIdAnswersUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendPollResponsePollStartIdAnswersUnderlyingCallsCount = newValue + sendPollResponsePollStartEventIdAnswersUnderlyingCallsCount = newValue } } } } - open var sendPollResponsePollStartIdAnswersCalled: Bool { - return sendPollResponsePollStartIdAnswersCallsCount > 0 + open var sendPollResponsePollStartEventIdAnswersCalled: Bool { + return sendPollResponsePollStartEventIdAnswersCallsCount > 0 } - open var sendPollResponsePollStartIdAnswersReceivedArguments: (pollStartId: String, answers: [String])? - open var sendPollResponsePollStartIdAnswersReceivedInvocations: [(pollStartId: String, answers: [String])] = [] - open var sendPollResponsePollStartIdAnswersClosure: ((String, [String]) async throws -> Void)? + open var sendPollResponsePollStartEventIdAnswersReceivedArguments: (pollStartEventId: String, answers: [String])? + open var sendPollResponsePollStartEventIdAnswersReceivedInvocations: [(pollStartEventId: String, answers: [String])] = [] + open var sendPollResponsePollStartEventIdAnswersClosure: ((String, [String]) async throws -> Void)? - open override func sendPollResponse(pollStartId: String, answers: [String]) async throws { - if let error = sendPollResponsePollStartIdAnswersThrowableError { + open override func sendPollResponse(pollStartEventId: String, answers: [String]) async throws { + if let error = sendPollResponsePollStartEventIdAnswersThrowableError { throw error } - sendPollResponsePollStartIdAnswersCallsCount += 1 - sendPollResponsePollStartIdAnswersReceivedArguments = (pollStartId: pollStartId, answers: answers) + sendPollResponsePollStartEventIdAnswersCallsCount += 1 + sendPollResponsePollStartEventIdAnswersReceivedArguments = (pollStartEventId: pollStartEventId, answers: answers) DispatchQueue.main.async { - self.sendPollResponsePollStartIdAnswersReceivedInvocations.append((pollStartId: pollStartId, answers: answers)) + self.sendPollResponsePollStartEventIdAnswersReceivedInvocations.append((pollStartEventId: pollStartEventId, answers: answers)) } - try await sendPollResponsePollStartIdAnswersClosure?(pollStartId, answers) + try await sendPollResponsePollStartEventIdAnswersClosure?(pollStartEventId, answers) } //MARK: - sendReadReceipt @@ -20193,15 +19590,15 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - sendVideo - var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = 0 - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherCallsCount: Int { + var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + returnValue = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -20209,29 +19606,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherCalled: Bool { - return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherCallsCount > 0 + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCalled: Bool { + return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount > 0 } - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReceivedArguments: (url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)? - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations: [(url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)] = [] + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments: (url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations: [(url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReturnValue: SendAttachmentJoinHandle! { + var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + returnValue = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -20239,40 +19636,40 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherClosure: ((String, String?, VideoInfo, String?, FormattedBody?, ProgressWatcher?) -> SendAttachmentJoinHandle)? + open var sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure: ((String, String?, VideoInfo, String?, FormattedBody?, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendVideo(url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?) -> SendAttachmentJoinHandle { - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherCallsCount += 1 - sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReceivedArguments = (url: url, thumbnailUrl: thumbnailUrl, videoInfo: videoInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher) + open override func sendVideo(url: String, thumbnailUrl: String?, videoInfo: VideoInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount += 1 + sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments = (url: url, thumbnailUrl: thumbnailUrl, videoInfo: videoInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReceivedInvocations.append((url: url, thumbnailUrl: thumbnailUrl, videoInfo: videoInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher)) + self.sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations.append((url: url, thumbnailUrl: thumbnailUrl, videoInfo: videoInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherClosure = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherClosure { - return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherClosure(url, thumbnailUrl, videoInfo, caption, formattedCaption, progressWatcher) + if let sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure = sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure { + return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure(url, thumbnailUrl, videoInfo, caption, formattedCaption, progressWatcher, useSendQueue) } else { - return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherReturnValue + return sendVideoUrlThumbnailUrlVideoInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue } } //MARK: - sendVoiceMessage - var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = 0 - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherCallsCount: Int { + var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount + returnValue = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -20280,29 +19677,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingCallsCount = newValue + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherCalled: Bool { - return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherCallsCount > 0 + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueCalled: Bool { + return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount > 0 } - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReceivedArguments: (url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)? - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReceivedInvocations: [(url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?)] = [] + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments: (url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations: [(url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReturnValue: SendAttachmentJoinHandle! { + var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue + returnValue = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -20310,26 +19707,26 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUnderlyingReturnValue = newValue + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherClosure: ((String, AudioInfo, [UInt16], String?, FormattedBody?, ProgressWatcher?) -> SendAttachmentJoinHandle)? + open var sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueClosure: ((String, AudioInfo, [UInt16], String?, FormattedBody?, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendVoiceMessage(url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?) -> SendAttachmentJoinHandle { - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherCallsCount += 1 - sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReceivedArguments = (url: url, audioInfo: audioInfo, waveform: waveform, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher) + open override func sendVoiceMessage(url: String, audioInfo: AudioInfo, waveform: [UInt16], caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount += 1 + sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments = (url: url, audioInfo: audioInfo, waveform: waveform, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReceivedInvocations.append((url: url, audioInfo: audioInfo, waveform: waveform, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher)) + self.sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations.append((url: url, audioInfo: audioInfo, waveform: waveform, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherClosure = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherClosure { - return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherClosure(url, audioInfo, waveform, caption, formattedCaption, progressWatcher) + if let sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueClosure = sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueClosure { + return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueClosure(url, audioInfo, waveform, caption, formattedCaption, progressWatcher, useSendQueue) } else { - return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherReturnValue + return sendVoiceMessageUrlAudioInfoWaveformCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue } } @@ -20410,16 +19807,16 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - toggleReaction - open var toggleReactionUniqueIdKeyThrowableError: Error? - var toggleReactionUniqueIdKeyUnderlyingCallsCount = 0 - open var toggleReactionUniqueIdKeyCallsCount: Int { + open var toggleReactionItemIdKeyThrowableError: Error? + var toggleReactionItemIdKeyUnderlyingCallsCount = 0 + open var toggleReactionItemIdKeyCallsCount: Int { get { if Thread.isMainThread { - return toggleReactionUniqueIdKeyUnderlyingCallsCount + return toggleReactionItemIdKeyUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = toggleReactionUniqueIdKeyUnderlyingCallsCount + returnValue = toggleReactionItemIdKeyUnderlyingCallsCount } return returnValue! @@ -20427,31 +19824,31 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - toggleReactionUniqueIdKeyUnderlyingCallsCount = newValue + toggleReactionItemIdKeyUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - toggleReactionUniqueIdKeyUnderlyingCallsCount = newValue + toggleReactionItemIdKeyUnderlyingCallsCount = newValue } } } } - open var toggleReactionUniqueIdKeyCalled: Bool { - return toggleReactionUniqueIdKeyCallsCount > 0 + open var toggleReactionItemIdKeyCalled: Bool { + return toggleReactionItemIdKeyCallsCount > 0 } - open var toggleReactionUniqueIdKeyReceivedArguments: (uniqueId: String, key: String)? - open var toggleReactionUniqueIdKeyReceivedInvocations: [(uniqueId: String, key: String)] = [] - open var toggleReactionUniqueIdKeyClosure: ((String, String) async throws -> Void)? + open var toggleReactionItemIdKeyReceivedArguments: (itemId: EventOrTransactionId, key: String)? + open var toggleReactionItemIdKeyReceivedInvocations: [(itemId: EventOrTransactionId, key: String)] = [] + open var toggleReactionItemIdKeyClosure: ((EventOrTransactionId, String) async throws -> Void)? - open override func toggleReaction(uniqueId: String, key: String) async throws { - if let error = toggleReactionUniqueIdKeyThrowableError { + open override func toggleReaction(itemId: EventOrTransactionId, key: String) async throws { + if let error = toggleReactionItemIdKeyThrowableError { throw error } - toggleReactionUniqueIdKeyCallsCount += 1 - toggleReactionUniqueIdKeyReceivedArguments = (uniqueId: uniqueId, key: key) + toggleReactionItemIdKeyCallsCount += 1 + toggleReactionItemIdKeyReceivedArguments = (itemId: itemId, key: key) DispatchQueue.main.async { - self.toggleReactionUniqueIdKeyReceivedInvocations.append((uniqueId: uniqueId, key: key)) + self.toggleReactionItemIdKeyReceivedInvocations.append((itemId: itemId, key: key)) } - try await toggleReactionUniqueIdKeyClosure?(uniqueId, key) + try await toggleReactionItemIdKeyClosure?(itemId, key) } //MARK: - unpinEvent @@ -21650,13 +21047,13 @@ open class TimelineItemSDKMock: MatrixRustSDK.TimelineItem { return uniqueIdCallsCount > 0 } - var uniqueIdUnderlyingReturnValue: String! - open var uniqueIdReturnValue: String! { + var uniqueIdUnderlyingReturnValue: TimelineUniqueId! + open var uniqueIdReturnValue: TimelineUniqueId! { get { if Thread.isMainThread { return uniqueIdUnderlyingReturnValue } else { - var returnValue: String? = nil + var returnValue: TimelineUniqueId? = nil DispatchQueue.main.sync { returnValue = uniqueIdUnderlyingReturnValue } @@ -21674,9 +21071,9 @@ open class TimelineItemSDKMock: MatrixRustSDK.TimelineItem { } } } - open var uniqueIdClosure: (() -> String)? + open var uniqueIdClosure: (() -> TimelineUniqueId)? - open override func uniqueId() -> String { + open override func uniqueId() -> TimelineUniqueId { uniqueIdCallsCount += 1 if let uniqueIdClosure = uniqueIdClosure { return uniqueIdClosure() @@ -21685,7 +21082,7 @@ open class TimelineItemSDKMock: MatrixRustSDK.TimelineItem { } } } -open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { +open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCount { init() { super.init(noPointer: .init()) } @@ -21696,17 +21093,17 @@ open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - asMessage + //MARK: - hasNotifications - var asMessageUnderlyingCallsCount = 0 - open var asMessageCallsCount: Int { + var hasNotificationsUnderlyingCallsCount = 0 + open var hasNotificationsCallsCount: Int { get { if Thread.isMainThread { - return asMessageUnderlyingCallsCount + return hasNotificationsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = asMessageUnderlyingCallsCount + returnValue = hasNotificationsUnderlyingCallsCount } return returnValue! @@ -21714,27 +21111,27 @@ open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { } set { if Thread.isMainThread { - asMessageUnderlyingCallsCount = newValue + hasNotificationsUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - asMessageUnderlyingCallsCount = newValue + hasNotificationsUnderlyingCallsCount = newValue } } } } - open var asMessageCalled: Bool { - return asMessageCallsCount > 0 + open var hasNotificationsCalled: Bool { + return hasNotificationsCallsCount > 0 } - var asMessageUnderlyingReturnValue: Message? - open var asMessageReturnValue: Message? { + var hasNotificationsUnderlyingReturnValue: Bool! + open var hasNotificationsReturnValue: Bool! { get { if Thread.isMainThread { - return asMessageUnderlyingReturnValue + return hasNotificationsUnderlyingReturnValue } else { - var returnValue: Message?? = nil + var returnValue: Bool? = nil DispatchQueue.main.sync { - returnValue = asMessageUnderlyingReturnValue + returnValue = hasNotificationsUnderlyingReturnValue } return returnValue! @@ -21742,36 +21139,36 @@ open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { } set { if Thread.isMainThread { - asMessageUnderlyingReturnValue = newValue + hasNotificationsUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - asMessageUnderlyingReturnValue = newValue + hasNotificationsUnderlyingReturnValue = newValue } } } } - open var asMessageClosure: (() -> Message?)? + open var hasNotificationsClosure: (() -> Bool)? - open override func asMessage() -> Message? { - asMessageCallsCount += 1 - if let asMessageClosure = asMessageClosure { - return asMessageClosure() + open override func hasNotifications() -> Bool { + hasNotificationsCallsCount += 1 + if let hasNotificationsClosure = hasNotificationsClosure { + return hasNotificationsClosure() } else { - return asMessageReturnValue + return hasNotificationsReturnValue } } - //MARK: - kind + //MARK: - highlightCount - var kindUnderlyingCallsCount = 0 - open var kindCallsCount: Int { + var highlightCountUnderlyingCallsCount = 0 + open var highlightCountCallsCount: Int { get { if Thread.isMainThread { - return kindUnderlyingCallsCount + return highlightCountUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = kindUnderlyingCallsCount + returnValue = highlightCountUnderlyingCallsCount } return returnValue! @@ -21779,27 +21176,27 @@ open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { } set { if Thread.isMainThread { - kindUnderlyingCallsCount = newValue + highlightCountUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - kindUnderlyingCallsCount = newValue + highlightCountUnderlyingCallsCount = newValue } } } } - open var kindCalled: Bool { - return kindCallsCount > 0 + open var highlightCountCalled: Bool { + return highlightCountCallsCount > 0 } - var kindUnderlyingReturnValue: TimelineItemContentKind! - open var kindReturnValue: TimelineItemContentKind! { + var highlightCountUnderlyingReturnValue: UInt32! + open var highlightCountReturnValue: UInt32! { get { if Thread.isMainThread { - return kindUnderlyingReturnValue + return highlightCountUnderlyingReturnValue } else { - var returnValue: TimelineItemContentKind? = nil + var returnValue: UInt32? = nil DispatchQueue.main.sync { - returnValue = kindUnderlyingReturnValue + returnValue = highlightCountUnderlyingReturnValue } return returnValue! @@ -21807,47 +21204,36 @@ open class TimelineItemContentSDKMock: MatrixRustSDK.TimelineItemContent { } set { if Thread.isMainThread { - kindUnderlyingReturnValue = newValue + highlightCountUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - kindUnderlyingReturnValue = newValue + highlightCountUnderlyingReturnValue = newValue } } } } - open var kindClosure: (() -> TimelineItemContentKind)? + open var highlightCountClosure: (() -> UInt32)? - open override func kind() -> TimelineItemContentKind { - kindCallsCount += 1 - if let kindClosure = kindClosure { - return kindClosure() + open override func highlightCount() -> UInt32 { + highlightCountCallsCount += 1 + if let highlightCountClosure = highlightCountClosure { + return highlightCountClosure() } else { - return kindReturnValue + return highlightCountReturnValue } } -} -open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCount { - init() { - super.init(noPointer: .init()) - } - - public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - fatalError("init(unsafeFromRawPointer:) has not been implemented") - } - - fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - hasNotifications + //MARK: - notificationCount - var hasNotificationsUnderlyingCallsCount = 0 - open var hasNotificationsCallsCount: Int { + var notificationCountUnderlyingCallsCount = 0 + open var notificationCountCallsCount: Int { get { if Thread.isMainThread { - return hasNotificationsUnderlyingCallsCount + return notificationCountUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = hasNotificationsUnderlyingCallsCount + returnValue = notificationCountUnderlyingCallsCount } return returnValue! @@ -21855,27 +21241,27 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - hasNotificationsUnderlyingCallsCount = newValue + notificationCountUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - hasNotificationsUnderlyingCallsCount = newValue + notificationCountUnderlyingCallsCount = newValue } } } } - open var hasNotificationsCalled: Bool { - return hasNotificationsCallsCount > 0 + open var notificationCountCalled: Bool { + return notificationCountCallsCount > 0 } - var hasNotificationsUnderlyingReturnValue: Bool! - open var hasNotificationsReturnValue: Bool! { + var notificationCountUnderlyingReturnValue: UInt32! + open var notificationCountReturnValue: UInt32! { get { if Thread.isMainThread { - return hasNotificationsUnderlyingReturnValue + return notificationCountUnderlyingReturnValue } else { - var returnValue: Bool? = nil + var returnValue: UInt32? = nil DispatchQueue.main.sync { - returnValue = hasNotificationsUnderlyingReturnValue + returnValue = notificationCountUnderlyingReturnValue } return returnValue! @@ -21883,36 +21269,47 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - hasNotificationsUnderlyingReturnValue = newValue + notificationCountUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - hasNotificationsUnderlyingReturnValue = newValue + notificationCountUnderlyingReturnValue = newValue } } } } - open var hasNotificationsClosure: (() -> Bool)? + open var notificationCountClosure: (() -> UInt32)? - open override func hasNotifications() -> Bool { - hasNotificationsCallsCount += 1 - if let hasNotificationsClosure = hasNotificationsClosure { - return hasNotificationsClosure() + open override func notificationCount() -> UInt32 { + notificationCountCallsCount += 1 + if let notificationCountClosure = notificationCountClosure { + return notificationCountClosure() } else { - return hasNotificationsReturnValue + return notificationCountReturnValue } } +} +open class UserIdentitySDKMock: MatrixRustSDK.UserIdentity { + init() { + super.init(noPointer: .init()) + } - //MARK: - highlightCount + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } - var highlightCountUnderlyingCallsCount = 0 - open var highlightCountCallsCount: Int { + fileprivate var pointer: UnsafeMutableRawPointer! + + //MARK: - isVerified + + var isVerifiedUnderlyingCallsCount = 0 + open var isVerifiedCallsCount: Int { get { if Thread.isMainThread { - return highlightCountUnderlyingCallsCount + return isVerifiedUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = highlightCountUnderlyingCallsCount + returnValue = isVerifiedUnderlyingCallsCount } return returnValue! @@ -21920,27 +21317,27 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - highlightCountUnderlyingCallsCount = newValue + isVerifiedUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - highlightCountUnderlyingCallsCount = newValue + isVerifiedUnderlyingCallsCount = newValue } } } } - open var highlightCountCalled: Bool { - return highlightCountCallsCount > 0 + open var isVerifiedCalled: Bool { + return isVerifiedCallsCount > 0 } - var highlightCountUnderlyingReturnValue: UInt32! - open var highlightCountReturnValue: UInt32! { + var isVerifiedUnderlyingReturnValue: Bool! + open var isVerifiedReturnValue: Bool! { get { if Thread.isMainThread { - return highlightCountUnderlyingReturnValue + return isVerifiedUnderlyingReturnValue } else { - var returnValue: UInt32? = nil + var returnValue: Bool? = nil DispatchQueue.main.sync { - returnValue = highlightCountUnderlyingReturnValue + returnValue = isVerifiedUnderlyingReturnValue } return returnValue! @@ -21948,36 +21345,36 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - highlightCountUnderlyingReturnValue = newValue + isVerifiedUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - highlightCountUnderlyingReturnValue = newValue + isVerifiedUnderlyingReturnValue = newValue } } } } - open var highlightCountClosure: (() -> UInt32)? + open var isVerifiedClosure: (() -> Bool)? - open override func highlightCount() -> UInt32 { - highlightCountCallsCount += 1 - if let highlightCountClosure = highlightCountClosure { - return highlightCountClosure() + open override func isVerified() -> Bool { + isVerifiedCallsCount += 1 + if let isVerifiedClosure = isVerifiedClosure { + return isVerifiedClosure() } else { - return highlightCountReturnValue + return isVerifiedReturnValue } } - //MARK: - notificationCount + //MARK: - masterKey - var notificationCountUnderlyingCallsCount = 0 - open var notificationCountCallsCount: Int { + var masterKeyUnderlyingCallsCount = 0 + open var masterKeyCallsCount: Int { get { if Thread.isMainThread { - return notificationCountUnderlyingCallsCount + return masterKeyUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = notificationCountUnderlyingCallsCount + returnValue = masterKeyUnderlyingCallsCount } return returnValue! @@ -21985,27 +21382,27 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - notificationCountUnderlyingCallsCount = newValue + masterKeyUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - notificationCountUnderlyingCallsCount = newValue + masterKeyUnderlyingCallsCount = newValue } } } } - open var notificationCountCalled: Bool { - return notificationCountCallsCount > 0 + open var masterKeyCalled: Bool { + return masterKeyCallsCount > 0 } - var notificationCountUnderlyingReturnValue: UInt32! - open var notificationCountReturnValue: UInt32! { + var masterKeyUnderlyingReturnValue: String? + open var masterKeyReturnValue: String? { get { if Thread.isMainThread { - return notificationCountUnderlyingReturnValue + return masterKeyUnderlyingReturnValue } else { - var returnValue: UInt32? = nil + var returnValue: String?? = nil DispatchQueue.main.sync { - returnValue = notificationCountUnderlyingReturnValue + returnValue = masterKeyUnderlyingReturnValue } return returnValue! @@ -22013,23 +21410,63 @@ open class UnreadNotificationsCountSDKMock: MatrixRustSDK.UnreadNotificationsCou } set { if Thread.isMainThread { - notificationCountUnderlyingReturnValue = newValue + masterKeyUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - notificationCountUnderlyingReturnValue = newValue + masterKeyUnderlyingReturnValue = newValue } } } } - open var notificationCountClosure: (() -> UInt32)? + open var masterKeyClosure: (() -> String?)? - open override func notificationCount() -> UInt32 { - notificationCountCallsCount += 1 - if let notificationCountClosure = notificationCountClosure { - return notificationCountClosure() + open override func masterKey() -> String? { + masterKeyCallsCount += 1 + if let masterKeyClosure = masterKeyClosure { + return masterKeyClosure() } else { - return notificationCountReturnValue + return masterKeyReturnValue + } + } + + //MARK: - pin + + open var pinThrowableError: Error? + var pinUnderlyingCallsCount = 0 + open var pinCallsCount: Int { + get { + if Thread.isMainThread { + return pinUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinUnderlyingCallsCount = newValue + } + } + } + } + open var pinCalled: Bool { + return pinCallsCount > 0 + } + open var pinClosure: (() async throws -> Void)? + + open override func pin() async throws { + if let error = pinThrowableError { + throw error } + pinCallsCount += 1 + try await pinClosure?() } } open class WidgetDriverSDKMock: MatrixRustSDK.WidgetDriver { diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift new file mode 100644 index 0000000000..66bc9a93f2 --- /dev/null +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -0,0 +1,77 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import MatrixRustSDK + +@MainActor +struct InvitedRoomProxyMockConfiguration { + var id = UUID().uuidString + var name: String? + var avatarURL: URL? + var members: [RoomMemberProxyMock] = .allMembers + var inviter: RoomMemberProxyMock = .mockAlice +} + +extension InvitedRoomProxyMock { + @MainActor + convenience init(_ configuration: InvitedRoomProxyMockConfiguration) { + self.init() + id = configuration.id + info = RoomInfoProxy(roomInfo: .init(configuration)) + } +} + +extension RoomInfo { + @MainActor init(_ configuration: InvitedRoomProxyMockConfiguration) { + self.init(id: configuration.id, + creator: nil, + displayName: configuration.name, + rawName: nil, + topic: nil, + avatarUrl: configuration.avatarURL?.absoluteString, + isDirect: false, + isPublic: false, + isSpace: false, + isTombstoned: false, + isFavourite: false, + canonicalAlias: nil, + alternativeAliases: [], + membership: .knocked, + inviter: .init(configuration.inviter), + heroes: [], + activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), + invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), + joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), + userPowerLevels: [:], + highlightCount: 0, + notificationCount: 0, + cachedUserDefinedNotificationMode: nil, + hasRoomCall: false, + activeRoomCallParticipants: [], + isMarkedUnread: false, + numUnreadMessages: 0, + numUnreadNotifications: 0, + numUnreadMentions: 0, + pinnedEventIds: []) + } +} + +private extension RoomMember { + init(_ proxy: RoomMemberProxyProtocol) { + self.init(userId: proxy.userID, + displayName: proxy.displayName, + avatarUrl: proxy.avatarURL?.absoluteString, + membership: proxy.membership, + isNameAmbiguous: proxy.disambiguatedDisplayName != proxy.displayName, + powerLevel: Int64(proxy.powerLevel), + normalizedPowerLevel: Int64(proxy.powerLevel), + isIgnored: proxy.isIgnored, + suggestedRoleForPowerLevel: proxy.role) + } +} diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 86e33a7a48..2f147104fb 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import MatrixRustSDK enum RoomProxyMockError: Error { case generic @@ -46,18 +47,7 @@ extension JoinedRoomProxyMock { self.init() id = configuration.id - name = configuration.name - topic = configuration.topic - avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic. - avatarURL = configuration.avatarURL - isDirect = configuration.isDirect - isSpace = configuration.isSpace - isPublic = configuration.isPublic isEncrypted = configuration.isEncrypted - hasOngoingCall = configuration.hasOngoingCall - canonicalAlias = configuration.canonicalAlias - - underlyingPinnedEventIDs = configuration.pinnedEventIDs let timeline = TimelineProxyMock() timeline.sendMessageEventContentReturnValue = .success(()) @@ -78,14 +68,12 @@ extension JoinedRoomProxyMock { ownUserID = configuration.ownUserID + infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher() membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() - - joinedMembersCount = configuration.members.filter { $0.membership == .join }.count - activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count + identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher() updateMembersClosure = { } - underlyingActionsPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() setNameClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) } getMemberUserIDClosure = { [weak self] userID in @@ -101,7 +89,6 @@ extension JoinedRoomProxyMock { flagAsUnreadReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(()) - underlyingIsFavourite = false flagAsFavouriteReturnValue = .success(()) powerLevelsReturnValue = .success(.mock) @@ -153,3 +140,46 @@ extension JoinedRoomProxyMock { clearDraftReturnValue = .success(()) } } + +extension RoomInfo { + @MainActor init(_ configuration: JoinedRoomProxyMockConfiguration) { + self.init(id: configuration.id, + creator: nil, + displayName: configuration.name, + rawName: configuration.name, + topic: configuration.topic, + avatarUrl: configuration.avatarURL?.absoluteString, + isDirect: configuration.isDirect, + isPublic: configuration.isPublic, + isSpace: configuration.isSpace, + isTombstoned: false, + isFavourite: false, + canonicalAlias: configuration.canonicalAlias, + alternativeAliases: [], + membership: .joined, + inviter: configuration.inviter.map { RoomMember(userId: $0.userID, + displayName: $0.displayName, + avatarUrl: $0.avatarURL?.absoluteString, + membership: $0.membership, + isNameAmbiguous: false, + powerLevel: Int64($0.powerLevel), + normalizedPowerLevel: Int64($0.powerLevel), + isIgnored: $0.isIgnored, + suggestedRoleForPowerLevel: $0.role) }, + heroes: [], + activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), + invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), + joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), + userPowerLevels: [:], + highlightCount: 0, + notificationCount: 0, + cachedUserDefinedNotificationMode: .allMessages, + hasRoomCall: configuration.hasOngoingCall, + activeRoomCallParticipants: [], + isMarkedUnread: false, + numUnreadMessages: 0, + numUnreadNotifications: 0, + numUnreadMentions: 0, + pinnedEventIds: Array(configuration.pinnedEventIDs)) + } +} diff --git a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift new file mode 100644 index 0000000000..76559bae13 --- /dev/null +++ b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift @@ -0,0 +1,62 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation +import MatrixRustSDK + +@MainActor +struct KnockedRoomProxyMockConfiguration { + var id = UUID().uuidString + var name: String? + var avatarURL: URL? + var members: [RoomMemberProxyMock] = .allMembers +} + +extension KnockedRoomProxyMock { + @MainActor + convenience init(_ configuration: KnockedRoomProxyMockConfiguration) { + self.init() + id = configuration.id + info = RoomInfoProxy(roomInfo: .init(configuration)) + } +} + +extension RoomInfo { + @MainActor init(_ configuration: KnockedRoomProxyMockConfiguration) { + self.init(id: configuration.id, + creator: nil, + displayName: configuration.name, + rawName: nil, + topic: nil, + avatarUrl: configuration.avatarURL?.absoluteString, + isDirect: false, + isPublic: false, + isSpace: false, + isTombstoned: false, + isFavourite: false, + canonicalAlias: nil, + alternativeAliases: [], + membership: .knocked, + inviter: nil, + heroes: [], + activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), + invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), + joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), + userPowerLevels: [:], + highlightCount: 0, + notificationCount: 0, + cachedUserDefinedNotificationMode: nil, + hasRoomCall: false, + activeRoomCallParticipants: [], + isMarkedUnread: false, + numUnreadMessages: 0, + numUnreadNotifications: 0, + numUnreadMentions: 0, + pinnedEventIds: []) + } +} diff --git a/ElementX/Sources/Mocks/MediaProviderMock.swift b/ElementX/Sources/Mocks/MediaProviderMock.swift new file mode 100644 index 0000000000..eb1929eff4 --- /dev/null +++ b/ElementX/Sources/Mocks/MediaProviderMock.swift @@ -0,0 +1,57 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import SwiftUI + +extension MediaProviderMock { + struct Configuration { } + + convenience init(configuration: Configuration) { + self.init() + + imageFromSourceSizeClosure = { mediaSource, _ in + guard mediaSource != nil else { + return nil + } + + if mediaSource?.url == .picturesDirectory { + return Asset.Images.appLogo.image + } + + return UIImage(systemName: "photo") + } + + loadImageFromSourceSizeClosure = { _, _ in + guard let image = UIImage(systemName: "photo") else { + fatalError() + } + + return .success(image) + } + + loadImageDataFromSourceClosure = { _ in + guard let image = UIImage(systemName: "photo"), + let data = image.pngData() else { + fatalError() + } + + return .success(data) + } + + loadFileFromSourceFilenameReturnValue = .failure(.failedRetrievingFile) + + loadImageRetryingOnReconnectionSizeClosure = { _, _ in + Task { + guard let image = UIImage(systemName: "photo") else { + fatalError() + } + + return image + } + } + } +} diff --git a/ElementX/Sources/Mocks/PollMock.swift b/ElementX/Sources/Mocks/PollMock.swift index cc3322871b..821550f415 100644 --- a/ElementX/Sources/Mocks/PollMock.swift +++ b/ElementX/Sources/Mocks/PollMock.swift @@ -82,7 +82,7 @@ extension Poll.Option { extension PollRoomTimelineItem { static func mock(poll: Poll, isOutgoing: Bool = true, isEditable: Bool = false) -> Self { - .init(id: .init(timelineID: UUID().uuidString, eventID: UUID().uuidString), + .init(id: .randomEvent, poll: poll, body: "poll", timestamp: "Now", diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 9825257d0a..2074c014b1 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -25,6 +25,11 @@ extension RoomMemberProxyMock { self.init() userID = configuration.userID displayName = configuration.displayName + + if let displayName = configuration.displayName { + disambiguatedDisplayName = "\(displayName) (\(userID))" + } + avatarURL = configuration.avatarURL membership = configuration.membership diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index 7e47548e0f..ed88e9efe2 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -71,8 +71,7 @@ extension Array where Element == RoomSummary { static let mockRooms: [Element] = [ RoomSummary(roomListItem: RoomListItemSDKMock(), id: "1", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Foundation 🔭🪐🌌", isDirect: false, avatarURL: nil, @@ -89,8 +88,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "2", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Foundation and Empire", isDirect: false, avatarURL: URL.picturesDirectory, @@ -107,8 +105,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "3", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Second Foundation", isDirect: false, avatarURL: nil, @@ -125,8 +122,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "4", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Foundation's Edge", isDirect: false, avatarURL: nil, @@ -143,8 +139,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "5", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Foundation and Earth", isDirect: true, avatarURL: nil, @@ -161,8 +156,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "6", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Prelude to Foundation", isDirect: true, avatarURL: nil, @@ -179,8 +173,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "0", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Unknown", isDirect: false, avatarURL: nil, @@ -230,8 +223,7 @@ extension Array where Element == RoomSummary { static let mockInvites: [Element] = [ RoomSummary(roomListItem: RoomListItemSDKMock(), id: "someAwesomeRoomId1", - isInvite: false, - inviter: RoomMemberProxyMock.mockCharlie, + joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), name: "First room", isDirect: false, avatarURL: URL.picturesDirectory, @@ -248,8 +240,7 @@ extension Array where Element == RoomSummary { isFavourite: false), RoomSummary(roomListItem: RoomListItemSDKMock(), id: "someAwesomeRoomId2", - isInvite: false, - inviter: RoomMemberProxyMock.mockCharlie, + joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), name: "Second room", isDirect: true, avatarURL: nil, diff --git a/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift b/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift index 0e671fcfe0..53e9d73c99 100644 --- a/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineProviderMock.swift @@ -35,7 +35,12 @@ class AutoUpdatingRoomTimelineProviderMock: RoomTimelineProvider { let diff = TimelineDiffSDKMock() diff.changeReturnValue = .append - diff.appendReturnValue = [TimelineItemFixtures.messageTimelineItem] + + let timelineItem = TimelineItemSDKMock() + timelineItem.asEventReturnValue = EventTimelineItem.mockMessage + timelineItem.uniqueIdReturnValue = .init(id: UUID().uuidString) + + diff.appendReturnValue = [timelineItem] await Self.timelineListener?.onUpdate(diff: [diff]) } diff --git a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift new file mode 100644 index 0000000000..29e80bd3dc --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift @@ -0,0 +1,75 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension ClientSDKMock { + struct Configuration { + // MARK: Authentication + + var serverAddress = "matrix.org" + var homeserverURL = "https://matrix-client.matrix.org" + var slidingSyncVersion = SlidingSyncVersion.native + var oidcLoginURL: String? + var supportsPasswordLogin = true + var elementWellKnown = "{\"registration_helper_url\":\"https://develop.element.io/#/mobile_register\"}" + var validCredentials = (username: "alice", password: "12345678") + + // MARK: Session + + var userID: String? + var session = Session(accessToken: UUID().uuidString, + refreshToken: nil, + userId: "@alice:matrix.org", + deviceId: UUID().uuidString, + homeserverUrl: "https://matrix-client.matrix.org", + oidcData: nil, + slidingSyncVersion: .native) + } + + enum MockError: Error { case generic } + + convenience init(configuration: Configuration) { + self.init() + + homeserverLoginDetailsReturnValue = HomeserverLoginDetailsSDKMock(configuration: configuration) + slidingSyncVersionReturnValue = configuration.slidingSyncVersion + userIdServerNameThrowableError = MockError.generic + serverReturnValue = "https://\(configuration.serverAddress)" + getUrlUrlReturnValue = configuration.elementWellKnown + urlForOidcOidcConfigurationPromptReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration) + loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in + guard username == configuration.validCredentials.username, + password == configuration.validCredentials.password else { + throw MockError.generic // use the matrix error + } + } + + userIdReturnValue = configuration.userID + sessionReturnValue = configuration.session + } +} + +extension HomeserverLoginDetailsSDKMock { + convenience init(configuration: ClientSDKMock.Configuration) { + self.init() + + slidingSyncVersionReturnValue = configuration.slidingSyncVersion + supportsPasswordLoginReturnValue = configuration.supportsPasswordLogin + supportsOidcLoginReturnValue = configuration.oidcLoginURL != nil + urlReturnValue = configuration.homeserverURL + } +} + +extension OidcAuthorizationDataSDKMock { + convenience init(configuration: ClientSDKMock.Configuration) { + self.init() + + loginUrlReturnValue = configuration.oidcLoginURL + } +} diff --git a/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift b/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift new file mode 100644 index 0000000000..7f4808b2e1 --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/IdentityResetHandleSDKMock.swift @@ -0,0 +1,22 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension IdentityResetHandleSDKMock { + struct Configuration { } + + convenience init(_ configuration: Configuration) { + self.init() + + authTypeReturnValue = .uiaa + resetAuthClosure = { _ in + try await Task.sleep(for: .seconds(60)) + } + } +} diff --git a/ElementX/Sources/Mocks/SDK/UserIdentitySDKMock.swift b/ElementX/Sources/Mocks/SDK/UserIdentitySDKMock.swift new file mode 100644 index 0000000000..885cf1a418 --- /dev/null +++ b/ElementX/Sources/Mocks/SDK/UserIdentitySDKMock.swift @@ -0,0 +1,21 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +extension UserIdentitySDKMock { + struct Configuration { + var isVerified = false + } + + convenience init(configuration: Configuration) { + self.init() + + isVerifiedReturnValue = configuration.isVerified + } +} diff --git a/ElementX/Sources/Mocks/SecureBackupControllerMock.swift b/ElementX/Sources/Mocks/SecureBackupControllerMock.swift new file mode 100644 index 0000000000..a1c5e0554d --- /dev/null +++ b/ElementX/Sources/Mocks/SecureBackupControllerMock.swift @@ -0,0 +1,48 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Foundation + +extension SecureBackupControllerMock { + struct Configuration { + var recoveryState: SecureBackupRecoveryState = .enabled + var keyBackupState: SecureBackupKeyBackupState = .enabled + } + + convenience init(_ configuration: Configuration) { + self.init() + + let recoveryStateSubject = CurrentValueSubject(configuration.recoveryState) + underlyingRecoveryState = .init(recoveryStateSubject) + + let keyBackupStateSubject = CurrentValueSubject(configuration.keyBackupState) + underlyingKeyBackupState = .init(keyBackupStateSubject) + + disableClosure = { + recoveryStateSubject.send(.disabled) + keyBackupStateSubject.send(.unknown) + return .success(()) + } + + enableClosure = { + recoveryStateSubject.send(.disabled) + keyBackupStateSubject.send(.enabled) + return .success(()) + } + + generateRecoveryKeyClosure = { + recoveryStateSubject.send(.enabled) + return .success("a1B2 C3d4 E5F6 g7H8 i9J0 K1l2 M3n4 O5p6 Q7R8 s9T0 U1v2 W3X4") + } + + confirmRecoveryKeyClosure = { _ in + recoveryStateSubject.send(.enabled) + return .success(()) + } + } +} diff --git a/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift b/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift index 377404e7a8..55572db7b5 100644 --- a/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift +++ b/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift @@ -16,16 +16,18 @@ extension SessionVerificationControllerProxyMock { SessionVerificationEmoji(symbol: "🏁", description: "Flag"), SessionVerificationEmoji(symbol: "🌏", description: "Globe")] - static func configureMock(callbacks: PassthroughSubject = .init(), + static func configureMock(actions: PassthroughSubject = .init(), isVerified: Bool = false, requestDelay: Duration = .seconds(1)) -> SessionVerificationControllerProxyMock { let mock = SessionVerificationControllerProxyMock() - mock.underlyingCallbacks = callbacks + mock.underlyingActions = actions + + mock.acknowledgeVerificationRequestDetailsReturnValue = .success(()) mock.requestVerificationClosure = { [unowned mock] in Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.acceptedVerificationRequest) + mock.actions.send(.acceptedVerificationRequest) } return .success(()) @@ -34,11 +36,11 @@ extension SessionVerificationControllerProxyMock { mock.startSasVerificationClosure = { [unowned mock] in Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.startedSasVerification) + mock.actions.send(.startedSasVerification) Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.receivedVerificationData(emojis)) + mock.actions.send(.receivedVerificationData(emojis)) } } @@ -48,7 +50,7 @@ extension SessionVerificationControllerProxyMock { mock.approveVerificationClosure = { [unowned mock] in Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.finished) + mock.actions.send(.finished) } return .success(()) @@ -57,7 +59,7 @@ extension SessionVerificationControllerProxyMock { mock.declineVerificationClosure = { [unowned mock] in Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.cancelled) + mock.actions.send(.cancelled) } return .success(()) @@ -66,7 +68,7 @@ extension SessionVerificationControllerProxyMock { mock.cancelVerificationClosure = { [unowned mock] in Task.detached { try await Task.sleep(for: requestDelay) - mock.callbacks.send(.cancelled) + mock.actions.send(.cancelled) } return .success(()) diff --git a/ElementX/Sources/Mocks/TimelineItemMock.swift b/ElementX/Sources/Mocks/TimelineItemMock.swift deleted file mode 100644 index 82138364ea..0000000000 --- a/ElementX/Sources/Mocks/TimelineItemMock.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Combine -import Foundation -import LoremSwiftum -import MatrixRustSDK - -enum TimelineItemFixtures { - static var callInviteTimelineItem: TimelineItem { - let eventTimelineItem = EventTimelineItemSDKMock() - eventTimelineItem.isOwnReturnValue = true - eventTimelineItem.timestampReturnValue = 0 - eventTimelineItem.isEditableReturnValue = false - eventTimelineItem.canBeRepliedToReturnValue = false - eventTimelineItem.senderReturnValue = "@bob:matrix.org" - eventTimelineItem.senderProfileReturnValue = .pending - - let timelineItemContent = TimelineItemContentSDKMock() - timelineItemContent.kindReturnValue = .callInvite - eventTimelineItem.contentReturnValue = timelineItemContent - - let timelineItem = TimelineItemSDKMock() - timelineItem.asEventReturnValue = eventTimelineItem - - return timelineItem - } - - static var messageTimelineItem: TimelineItem { - let eventTimelineItem = EventTimelineItemSDKMock() - eventTimelineItem.eventIdReturnValue = UUID().uuidString - eventTimelineItem.isOwnReturnValue = true - eventTimelineItem.timestampReturnValue = 0 - eventTimelineItem.isEditableReturnValue = false - eventTimelineItem.canBeRepliedToReturnValue = false - eventTimelineItem.senderReturnValue = "@bob:matrix.org" - eventTimelineItem.senderProfileReturnValue = .pending - eventTimelineItem.reactionsReturnValue = [] - eventTimelineItem.readReceiptsReturnValue = [:] - - let timelineItemContent = TimelineItemContentSDKMock() - - timelineItemContent.kindReturnValue = .message - - let message = MessageSDKMock() - - let textMessageContent = TextMessageContent(body: Lorem.sentences(Int.random(in: 1...5)), formatted: nil) - message.msgtypeReturnValue = .text(content: textMessageContent) - message.isThreadedReturnValue = false - message.isEditedReturnValue = false - - timelineItemContent.asMessageReturnValue = message - - eventTimelineItem.contentReturnValue = timelineItemContent - - let timelineItem = TimelineItemSDKMock() - timelineItem.asEventReturnValue = eventTimelineItem - timelineItem.uniqueIdReturnValue = UUID().uuidString - - return timelineItem - } -} diff --git a/ElementX/Sources/Mocks/UserSessionMock.swift b/ElementX/Sources/Mocks/UserSessionMock.swift index 059b0b07f2..f9d0f510bb 100644 --- a/ElementX/Sources/Mocks/UserSessionMock.swift +++ b/ElementX/Sources/Mocks/UserSessionMock.swift @@ -17,7 +17,7 @@ extension UserSessionMock { self.init() clientProxy = configuration.clientProxy - mediaProvider = MockMediaProvider() + mediaProvider = MediaProviderMock(configuration: .init()) voiceMessageMediaManager = VoiceMessageMediaManagerMock() sessionSecurityStatePublisher = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher() diff --git a/ElementX/Sources/Mocks/UserSessionStoreMock.swift b/ElementX/Sources/Mocks/UserSessionStoreMock.swift new file mode 100644 index 0000000000..69b8418f1c --- /dev/null +++ b/ElementX/Sources/Mocks/UserSessionStoreMock.swift @@ -0,0 +1,17 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +extension UserSessionStoreMock { + struct Configuration { } + + convenience init(configuration: Configuration) { + self.init() + + userSessionForSessionDirectoriesPassphraseReturnValue = .success(UserSessionMock(.init(clientProxy: ClientProxyMock(.init())))) + clientSessionDelegate = KeychainControllerMock() + } +} diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index e78721e64a..0c6c95aab4 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -16,6 +16,8 @@ enum A11yIdentifiers { static let appLockSetupSettingsScreen = AppLockSetupSettingsScreen() static let bugReportScreen = BugReportScreen() static let changeServerScreen = ChangeServer() + static let encryptionResetScreen = EncryptionResetScreen() + static let encryptionResetPasswordScreen = EncryptionResetPasswordScreen() static let homeScreen = HomeScreen() static let loginScreen = LoginScreen() static let authenticationStartScreen = AuthenticationStartScreen() @@ -24,6 +26,9 @@ enum A11yIdentifiers { static let roomDetailsScreen = RoomDetailsScreen() static let roomNotificationSettingsScreen = RoomNotificationSettingsScreen() static let roomRolesAndPermissionsScreen = RoomRolesAndPermissionsScreen() + static let secureBackupScreen = SecureBackupScreen() + static let secureBackupKeyBackupScreen = SecureBackupKeyBackupScreen() + static let secureBackupRecoveryKeyScreen = SecureBackupRecoveryKeyScreen() static let serverConfirmationScreen = ServerConfirmationScreen() static let sessionVerificationScreen = SessionVerificationScreen() static let settingsScreen = SettingsScreen() @@ -81,6 +86,15 @@ enum A11yIdentifiers { let dismiss = "change_server-dismiss" } + struct EncryptionResetScreen { + let continueReset = "encryption_reset-continue_reset" + } + + struct EncryptionResetPasswordScreen { + let passwordField = "encryption_reset_password-password_field" + let submit = "encryption_reset_password-submit" + } + struct HomeScreen { let userAvatar = "home_screen-user_avatar" let recoveryKeyConfirmationBannerContinue = "home_screen-recovery_key_confirmation_continue" @@ -126,6 +140,9 @@ enum A11yIdentifiers { let timelineItemActionMenu = "room-timeline_item_action_menu" let joinCall = "room-join_call" let scrollToBottom = "room-scroll_to_bottom" + + let messageComposer = "room-message_composer" + let sendButton = "room-send_button" let composerToolbar = ComposerToolbar() @@ -175,12 +192,31 @@ enum A11yIdentifiers { let memberModeration = "room_roles_and_permissions-member_moderation" } + struct SecureBackupScreen { + let keyStorage = "secure_backup-key_storage" + let recoveryKey = "secure_backup-recovery_key" + } + + struct SecureBackupKeyBackupScreen { + let deleteKeyStorage = "secure_backup_key_backup-delete_key_storage" + } + + struct SecureBackupRecoveryKeyScreen { + let generateRecoveryKey = "secure_backup_recovery_key-generate_recovery_key" + let copyRecoveryKey = "secure_backup_recovery_key-copy_recovery_key" + let done = "secure_backup_recovery_key-done" + let recoveryKeyField = "secure_backup_recovery_key-recovery_key_field" + let confirm = "secure_backup_recovery_key-confirm" + } + struct ServerConfirmationScreen { let `continue` = "server_confirmation-continue" let changeServer = "server_confirmation-change_server" } struct SessionVerificationScreen { + let acceptVerificationRequest = "session_verification-accept_verification_request" + let ignoreVerificationRequest = "session_verification-ignore_verification_request" let requestVerification = "session_verification-request_verification" let startSasVerification = "session_verification-start_sas_verification" let acceptChallenge = "session_verification-accept_challenge" diff --git a/ElementX/Sources/Other/Extensions/Array.swift b/ElementX/Sources/Other/Extensions/Array.swift index 7622916e85..a5fd0fd0a7 100644 --- a/ElementX/Sources/Other/Extensions/Array.swift +++ b/ElementX/Sources/Other/Extensions/Array.swift @@ -61,6 +61,6 @@ extension Array { extension Array where Element == RoomTimelineItemProtocol { func firstUsingStableID(_ id: TimelineItemIdentifier) -> Element? { - first { $0.id.timelineID == id.timelineID } + first { $0.id.uniqueID == id.uniqueID } } } diff --git a/ElementX/Sources/Other/Extensions/ClientBuilder.swift b/ElementX/Sources/Other/Extensions/ClientBuilder.swift index 586fa4c201..6abce9a3b7 100644 --- a/ElementX/Sources/Other/Extensions/ClientBuilder.swift +++ b/ElementX/Sources/Other/Extensions/ClientBuilder.swift @@ -15,7 +15,7 @@ extension ClientBuilder { slidingSync: ClientBuilderSlidingSync, sessionDelegate: ClientSessionDelegate, appHooks: AppHooks, - invisibleCryptoEnabled: Bool) -> ClientBuilder { + enableOnlySignedDeviceIsolationMode: Bool) -> ClientBuilder { var builder = ClientBuilder() .enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, sessionDelegate: sessionDelegate) .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent()) @@ -34,10 +34,14 @@ extension ClientBuilder { .backupDownloadStrategy(backupDownloadStrategy: .afterDecryptionFailure) .autoEnableBackups(autoEnableBackups: true) - if invisibleCryptoEnabled { - builder = builder.roomKeyRecipientStrategy(strategy: CollectStrategy.identityBasedStrategy) + if enableOnlySignedDeviceIsolationMode { + builder = builder + .roomKeyRecipientStrategy(strategy: .identityBasedStrategy) + .roomDecryptionTrustRequirement(trustRequirement: .crossSignedOrLegacy) } else { - builder = builder.roomKeyRecipientStrategy(strategy: .deviceBasedStrategy(onlyAllowTrustedDevices: false, errorOnVerifiedUserProblem: true)) + builder = builder + .roomKeyRecipientStrategy(strategy: .deviceBasedStrategy(onlyAllowTrustedDevices: false, errorOnVerifiedUserProblem: true)) + .roomDecryptionTrustRequirement(trustRequirement: .untrusted) } } diff --git a/ElementX/Sources/Other/Extensions/Dictionary.swift b/ElementX/Sources/Other/Extensions/Dictionary.swift index 2fe7ddc8f3..14e064dcdd 100644 --- a/ElementX/Sources/Other/Extensions/Dictionary.swift +++ b/ElementX/Sources/Other/Extensions/Dictionary.swift @@ -13,7 +13,7 @@ extension Dictionary { options: [.fragmentsAllowed, .sortedKeys]) else { return nil } - return String(decoding: data, as: UTF8.self) + return String(data: data, encoding: .utf8) } /// Returns a dictionary containing the original values keyed by the results of mapping the given closure over its keys. diff --git a/ElementX/Sources/Other/ProcessInfo.swift b/ElementX/Sources/Other/Extensions/ProcessInfo.swift similarity index 100% rename from ElementX/Sources/Other/ProcessInfo.swift rename to ElementX/Sources/Other/Extensions/ProcessInfo.swift diff --git a/ElementX/Sources/Other/Extensions/ProposedViewSize.swift b/ElementX/Sources/Other/Extensions/ProposedViewSize.swift index 17067d58c6..a9995015ea 100644 --- a/ElementX/Sources/Other/Extensions/ProposedViewSize.swift +++ b/ElementX/Sources/Other/Extensions/ProposedViewSize.swift @@ -7,7 +7,7 @@ import SwiftUI -extension ProposedViewSize: Hashable { +extension ProposedViewSize: @retroactive Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(width) hasher.combine(height) diff --git a/ElementX/Sources/Other/Extensions/Snapshotting.swift b/ElementX/Sources/Other/Extensions/Snapshotting.swift index 77a968a15f..7de5ca95bb 100644 --- a/ElementX/Sources/Other/Extensions/Snapshotting.swift +++ b/ElementX/Sources/Other/Extensions/Snapshotting.swift @@ -33,7 +33,7 @@ public struct SnapshotPrecisionPreferenceKey: PreferenceKey { } public struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey { - public static var defaultValue: Float = 1.0 + public static var defaultValue: Float = 0.98 public static func reduce(value: inout Float, nextValue: () -> Float) { value = nextValue() @@ -50,7 +50,7 @@ public extension SwiftUI.View { /// - precision: The percentage of pixels that must match. /// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a match. 98-99% mimics the precision of the human eye. @inlinable - func snapshotPreferences(delay: TimeInterval = .zero, precision: Float = 1.0, perceptualPrecision: Float = 1.0) -> some SwiftUI.View { + func snapshotPreferences(delay: TimeInterval = .zero, precision: Float = 1.0, perceptualPrecision: Float = 0.98) -> some SwiftUI.View { preference(key: SnapshotDelayPreferenceKey.self, value: delay) .preference(key: SnapshotPrecisionPreferenceKey.self, value: precision) .preference(key: SnapshotPerceptualPrecisionPreferenceKey.self, value: perceptualPrecision) diff --git a/ElementX/Sources/Other/Extensions/String.swift b/ElementX/Sources/Other/Extensions/String.swift index 58caff4888..2e27561a2e 100644 --- a/ElementX/Sources/Other/Extensions/String.swift +++ b/ElementX/Sources/Other/Extensions/String.swift @@ -90,3 +90,10 @@ extension String { return result } } + +extension String { + /// detects if the string is empty or contains only whitespaces and newlines + var isBlank: Bool { + trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } +} diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index e23d72aea5..3506d46c5f 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -7,7 +7,7 @@ import Foundation -extension URL: ExpressibleByStringLiteral { +extension URL: @retroactive ExpressibleByStringLiteral { public init(stringLiteral value: StaticString) { guard let url = URL(string: "\(value)") else { fatalError("The static string used to create this URL is invalid") diff --git a/ElementX/Sources/Other/Extensions/XCUIElement.swift b/ElementX/Sources/Other/Extensions/XCUIElement.swift index bb345b3c03..4d7436a1f1 100644 --- a/ElementX/Sources/Other/Extensions/XCUIElement.swift +++ b/ElementX/Sources/Other/Extensions/XCUIElement.swift @@ -8,8 +8,10 @@ import XCTest extension XCUIElement { - func clearAndTypeText(_ text: String) { - forceTap() + func clearAndTypeText(_ text: String, app: XCUIApplication) { + tapCenter() + + app.showKeyboardIfNeeded() guard let currentValue = value as? String else { XCTFail("Tried to clear and type text into a non string value") @@ -24,8 +26,22 @@ extension XCUIElement { } } - func forceTap() { + func tapCenter() { let coordinate: XCUICoordinate = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.5)) coordinate.tap() } } + +extension XCUIApplication { + /// Ensures the software keyboard is shown on an iPad when a text field is focussed. + /// + /// Note: Whilst this could be added on XCUIElement to more closely tie it to a text field, it requires the + /// app instance anyway, and some of our tests assert that a default focus has been set on the text field, + /// so having a method that would set the focus and show the keyboard isn't always desirable. + func showKeyboardIfNeeded() { + if UIDevice.current.userInterfaceIdiom == .pad, keyboards.count == 0 { + buttons["Keyboard"].tap() + buttons["Show Keyboard"].tap() + } + } +} diff --git a/ElementX/Sources/Other/Logging/MXLog.swift b/ElementX/Sources/Other/Logging/MXLog.swift index 652c257f00..974b02f594 100644 --- a/ElementX/Sources/Other/Logging/MXLog.swift +++ b/ElementX/Sources/Other/Logging/MXLog.swift @@ -22,21 +22,18 @@ enum MXLog { private static var didConfigureOnce = false private static var rootSpan: Span! - private static var target: String! + private static var currentTarget: String! - static func configure(target: String? = nil, + static func configure(currentTarget: String, + filePrefix: String?, logLevel: TracingConfiguration.LogLevel) { guard !didConfigureOnce else { return } - RustTracing.setup(configuration: .init(logLevel: logLevel, target: target)) + RustTracing.setup(configuration: .init(logLevel: logLevel, currentTarget: currentTarget, filePrefix: filePrefix)) - if let target { - self.target = target - } else { - self.target = Constants.target - } + self.currentTarget = currentTarget - rootSpan = Span(file: #file, line: #line, level: .info, target: self.target, name: "root") + rootSpan = Span(file: #file, line: #line, level: .info, target: self.currentTarget, name: "root") rootSpan.enter() didConfigureOnce = true @@ -138,7 +135,7 @@ enum MXLog { rootSpan.enter() } - return Span(file: file, line: UInt32(line), level: level, target: target, name: name) + return Span(file: file, line: UInt32(line), level: level, target: currentTarget, name: name) } // periphery:ignore:parameters function,column,context @@ -157,6 +154,6 @@ enum MXLog { rootSpan.enter() } - logEvent(file: (file as NSString).lastPathComponent, line: UInt32(line), level: level, target: target, message: "\(message)") + logEvent(file: (file as NSString).lastPathComponent, line: UInt32(line), level: level, target: currentTarget, message: "\(message)") } } diff --git a/ElementX/Sources/Other/Logging/RustTracing.swift b/ElementX/Sources/Other/Logging/RustTracing.swift index 243d2d9e81..35f55000b4 100644 --- a/ElementX/Sources/Other/Logging/RustTracing.swift +++ b/ElementX/Sources/Other/Logging/RustTracing.swift @@ -13,7 +13,13 @@ enum RustTracing { /// name and other log management metadata during rotation. static let filePrefix = "console" /// The directory that stores all of the log files. - static var logsDirectory: URL { .appGroupContainerDirectory } + static var logsDirectory: URL { + if ProcessInfo.isRunningIntegrationTests { + "/Users/Shared" + } else { + .appGroupContainerDirectory + } + } private(set) static var currentTracingConfiguration: TracingConfiguration? static func setup(configuration: TracingConfiguration) { @@ -23,7 +29,15 @@ enum RustTracing { // as the app is unlikely to be running continuously. let maxFiles: UInt64 = 24 * 7 - setupTracing(config: .init(filter: configuration.filter, + // Log everything on integration tests to check whether + // the logs contain any sensitive data. See `UserFlowTests.swift` + let filter = if ProcessInfo.isRunningIntegrationTests { + TracingConfiguration(logLevel: .trace, currentTarget: "integrationtests", filePrefix: nil).filter + } else { + configuration.filter + } + + setupTracing(config: .init(filter: filter, writeToStdoutOrSystem: true, writeToFiles: .init(path: logsDirectory.path(percentEncoded: false), filePrefix: configuration.fileName, diff --git a/ElementX/Sources/Other/Logging/TracingConfiguration.swift b/ElementX/Sources/Other/Logging/TracingConfiguration.swift index 3ff5250de0..a8d0a061c2 100644 --- a/ElementX/Sources/Other/Logging/TracingConfiguration.swift +++ b/ElementX/Sources/Other/Logging/TracingConfiguration.swift @@ -11,9 +11,8 @@ import Collections // We can filter by level, crate and even file. See more details here: // https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples struct TracingConfiguration { - enum LogLevel: Codable, Hashable { + enum LogLevel: String, Codable, Hashable, Comparable { case error, warn, info, debug, trace - case custom(String) var title: String { switch self { @@ -27,34 +26,32 @@ struct TracingConfiguration { return "Debug" case .trace: return "Trace" - case .custom: - return "Custom" } } - fileprivate var rawValue: String { - switch self { - case .error: - return "error" - case .warn: - return "warn" - case .info: - return "info" - case .debug: - return "debug" - case .trace: - return "trace" - case .custom(let filter): - return filter + static func < (lhs: TracingConfiguration.LogLevel, rhs: TracingConfiguration.LogLevel) -> Bool { + switch (lhs, rhs) { + case (.error, _): + true + case (.warn, .error): + false + case (.warn, _): + true + case (.info, .error), (.info, .warn): + false + case (.info, _): + true + case (.debug, .error), (.debug, .warn), (.debug, .info): + false + case (.debug, _): + true + case (.trace, _): + false } } } enum Target: String { - case common = "" - - case elementx - case hyper, matrix_sdk_ffi, matrix_sdk_crypto case matrix_sdk_client = "matrix_sdk::client" @@ -66,9 +63,8 @@ struct TracingConfiguration { case matrix_sdk_ui_timeline = "matrix_sdk_ui::timeline" } + // The `common` target is excluded because 3rd-party crates might end up logging user data. static let targets: OrderedDictionary = [ - .common: .info, // Never set this lower than info - 3rd-party crates may start logging user data. - .elementx: .info, .hyper: .warn, .matrix_sdk_ffi: .info, .matrix_sdk_client: .trace, @@ -92,33 +88,30 @@ struct TracingConfiguration { /// - Parameter logLevel: the desired log level /// - Parameter target: the name of the target being configured /// - Returns: a custom tracing configuration - init(logLevel: LogLevel, target: String?) { - fileName = if let target { - "\(RustTracing.filePrefix)-\(target)" + init(logLevel: LogLevel, currentTarget: String, filePrefix: String?) { + fileName = if let filePrefix { + "\(RustTracing.filePrefix)-\(filePrefix)" } else { RustTracing.filePrefix } - - if case let .custom(filter) = logLevel { - self.filter = filter - return - } - + let overrides = Self.targets.keys.reduce(into: [Target: LogLevel]()) { partialResult, target in // Keep the defaults here - let ignoredTargets: [Target] = [.common, // Never remove common from the ignored targets (see above for more info). - .hyper, - .matrix_sdk_ffi, - .matrix_sdk_oidc, - .matrix_sdk_client, - .matrix_sdk_crypto, - .matrix_sdk_crypto_account, - .matrix_sdk_http_client] + let ignoredTargets: [Target] = [.hyper] + if ignoredTargets.contains(target) { return } - partialResult[target] = logLevel + guard let defaultTargetLogLevel = Self.targets[target] else { + return + } + + // Only change the targets that have default values + // smaller than the desired log level + if defaultTargetLogLevel < logLevel { + partialResult[target] = logLevel + } } var newTargets = Self.targets @@ -126,7 +119,7 @@ struct TracingConfiguration { newTargets.updateValue(logLevel, forKey: target) } - let components = newTargets.map { (target: Target, logLevel: LogLevel) in + var components = newTargets.map { (target: Target, logLevel: LogLevel) in guard !target.rawValue.isEmpty else { return logLevel.rawValue } @@ -134,6 +127,10 @@ struct TracingConfiguration { return "\(target.rawValue)=\(logLevel.rawValue)" } + // With `common` not being used we manually need to specify the log + // level for passed in targets + components.append("\(currentTarget)=\(logLevel.rawValue)") + filter = components.joined(separator: ",") } } diff --git a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift index 2e96579881..bfa883033c 100644 --- a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift +++ b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift @@ -46,7 +46,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests { // The mock viewModel simulates the loading logic for testing purposes context = PillContext.mock(type: .loadUser(isOwn: false)) - mediaProvider = MockMediaProvider() + mediaProvider = MediaProviderMock(configuration: .init()) } else if let timelineContext = delegate?.timelineContext { context = PillContext(timelineContext: timelineContext, data: pillData) mediaProvider = timelineContext.mediaProvider diff --git a/ElementX/Sources/Other/Pills/PillView.swift b/ElementX/Sources/Other/Pills/PillView.swift index 91265de629..2e51831a2d 100644 --- a/ElementX/Sources/Other/Pills/PillView.swift +++ b/ElementX/Sources/Other/Pills/PillView.swift @@ -30,14 +30,14 @@ struct PillView: View { .padding(.trailing, 6) .padding(.vertical, 1) .background { Capsule().foregroundColor(backgroundColor) } - .onChange(of: context.viewState.displayText) { _ in + .onChange(of: context.viewState.displayText) { didChangeText() } } } struct PillView_Previews: PreviewProvider, TestablePreview { - static let mockMediaProvider = MockMediaProvider() + static let mockMediaProvider = MediaProviderMock(configuration: .init()) static var previews: some View { PillView(mediaProvider: mockMediaProvider, diff --git a/ElementX/Sources/Other/SwiftUI/Search.swift b/ElementX/Sources/Other/SwiftUI/Search.swift index e9ba319837..cb951305d9 100644 --- a/ElementX/Sources/Other/SwiftUI/Search.swift +++ b/ElementX/Sources/Other/SwiftUI/Search.swift @@ -171,7 +171,7 @@ struct IsSearchingModifier: ViewModifier { func body(content: Content) -> some View { content - .onChange(of: isSearchingEnv) { isSearching = $0 } + .onChange(of: isSearchingEnv) { isSearching = $1 } } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index 4b63f20cae..2717af4a82 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -17,6 +17,7 @@ struct AvatarHeaderView: View { private enum Badge: Hashable { case encrypted(Bool) case `public` + case verified } private let avatarInfo: AvatarInfo @@ -26,13 +27,13 @@ struct AvatarHeaderView: View { private let avatarSize: AvatarSize private let mediaProvider: MediaProviderProtocol? - private var onAvatarTap: (() -> Void)? + private var onAvatarTap: ((URL) -> Void)? @ViewBuilder private var footer: () -> Footer init(room: RoomDetails, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { avatarInfo = .room(room.avatar) title = room.name ?? room.id @@ -54,7 +55,7 @@ struct AvatarHeaderView: View { init(accountOwner: RoomMemberDetails, dmRecipient: RoomMemberDetails, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { let dmRecipientProfile = UserProfileProxy(member: dmRecipient) avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)])) @@ -70,13 +71,15 @@ struct AvatarHeaderView: View { } init(member: RoomMemberDetails, + isVerified: Bool = false, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { let profile = UserProfileProxy(member: member) self.init(user: profile, + isVerified: isVerified, avatarSize: avatarSize, mediaProvider: mediaProvider, onAvatarTap: onAvatarTap, @@ -84,9 +87,10 @@ struct AvatarHeaderView: View { } init(user: UserProfileProxy, + isVerified: Bool, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { avatarInfo = .user(user) title = user.displayName ?? user.userID @@ -96,27 +100,29 @@ struct AvatarHeaderView: View { self.mediaProvider = mediaProvider self.onAvatarTap = onAvatarTap self.footer = footer - badges = [] + badges = isVerified ? [.verified] : [] } private var badgesStack: some View { HStack(spacing: 8) { ForEach(badges, id: \.self) { badge in switch badge { - case .encrypted(let isEncrypted): - if isEncrypted { - BadgeLabel(title: L10n.screenRoomDetailsBadgeEncrypted, - icon: \.lockSolid, - isHighlighted: true) - } else { - BadgeLabel(title: L10n.screenRoomDetailsBadgeNotEncrypted, - icon: \.lockOff, - isHighlighted: false) - } + case .encrypted(true): + BadgeLabel(title: L10n.screenRoomDetailsBadgeEncrypted, + icon: \.lockSolid, + isHighlighted: true) + case .encrypted(false): + BadgeLabel(title: L10n.screenRoomDetailsBadgeNotEncrypted, + icon: \.lockOff, + isHighlighted: false) case .public: BadgeLabel(title: L10n.screenRoomDetailsBadgePublic, icon: \.public, isHighlighted: false) + case .verified: + BadgeLabel(title: L10n.commonVerified, + icon: \.verified, + isHighlighted: true) } } } @@ -128,24 +134,22 @@ struct AvatarHeaderView: View { case .room(let roomAvatar): RoomAvatarImage(avatar: roomAvatar, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onAvatarTap: onAvatarTap) + case .user(let userProfile): LoadableAvatarImage(url: userProfile.avatarURL, name: userProfile.displayName, contentID: userProfile.userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) } } var body: some View { VStack(spacing: 8.0) { - Button { - onAvatarTap?() - } label: { - avatar - } - .buttonStyle(.borderless) // Add a button style to stop the whole row being tappable. + avatar Spacer() .frame(height: 9) @@ -191,7 +195,7 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview { isEncrypted: true, isPublic: true), avatarSize: .room(on: .details), - mediaProvider: MockMediaProvider()) { + mediaProvider: MediaProviderMock(configuration: .init())) { HStack(spacing: 32) { ShareLink(item: "test") { CompoundIcon(\.shareIos) @@ -205,7 +209,7 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview { Form { AvatarHeaderView(accountOwner: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockMe), dmRecipient: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), - mediaProvider: MockMediaProvider()) { + mediaProvider: MediaProviderMock(configuration: .init())) { HStack(spacing: 32) { ShareLink(item: "test") { CompoundIcon(\.shareIos) @@ -220,11 +224,16 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview { VStack(spacing: 16) { AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), avatarSize: .room(on: .details), - mediaProvider: MockMediaProvider()) { Text("") } + mediaProvider: MediaProviderMock(configuration: .init())) { Text("") } + + AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockBob), + isVerified: true, + avatarSize: .room(on: .details), + mediaProvider: MediaProviderMock(configuration: .init())) { Text("") } AvatarHeaderView(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockBanned[3]), avatarSize: .room(on: .details), - mediaProvider: MockMediaProvider()) { Text("") } + mediaProvider: MediaProviderMock(configuration: .init())) { Text("") } } .padding() .background(Color.compound.bgSubtleSecondaryLevel0) diff --git a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift b/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift similarity index 51% rename from ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift rename to ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift index f50ecb11e3..2551c02ca1 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift @@ -10,39 +10,36 @@ import SwiftUI /// An image that is styled for use as the main/top/hero screen icon. This component /// takes a compound icon. If you would like to apply it to an SFSymbol, you can call -/// the `heroImage()` modifier directly on the Image. -struct HeroImage: View { +/// the `bigIcon()` modifier directly on the Image. +struct BigIcon: View { enum Style { - case normal - case subtle + case defaultSolid + case `default` + case alertSolid + case alert + case successSolid case success - case critical - case criticalOnSecondary var foregroundColor: Color { switch self { - case .normal: - .compound.iconPrimary - case .subtle: + case .defaultSolid, .default: .compound.iconSecondary - case .success: - .compound.iconSuccessPrimary - case .critical, .criticalOnSecondary: + case .alertSolid, .alert: .compound.iconCriticalPrimary + case .successSolid, .success: + .compound.iconSuccessPrimary } } var backgroundFillColor: Color { switch self { - case .normal: + case .defaultSolid: .compound.bgSubtleSecondary - case .subtle: - .compound.bgSubtlePrimary - case .success: - .compound.bgSuccessSubtle - case .critical: + case .alertSolid: .compound.bgCriticalSubtle - case .criticalOnSecondary: + case .successSolid: + .compound.bgSuccessSubtle + case .default, .alert, .success: .compound.bgCanvasDefault } } @@ -50,32 +47,32 @@ struct HeroImage: View { /// The icon that is shown. let icon: KeyPath - var style: Style = .normal + var style: Style = .defaultSolid var body: some View { - CompoundIcon(icon, size: .custom(42), relativeTo: .title) - .modifier(HeroImageModifier(style: style)) + CompoundIcon(icon, size: .custom(32), relativeTo: .title) + .modifier(BigIconModifier(style: style)) } } extension Image { /// Styles the image for use as the main/top/hero screen icon. You should prefer - /// the HeroImage component when possible, by using an icon from Compound. - func heroImage(insets: CGFloat = 16, style: HeroImage.Style = .normal) -> some View { + /// the BigIcon component when possible, by using an icon from Compound. + func bigIcon(insets: CGFloat = 16, style: BigIcon.Style = .defaultSolid) -> some View { resizable() .renderingMode(.template) .aspectRatio(contentMode: .fit) .scaledPadding(insets, relativeTo: .title) - .modifier(HeroImageModifier(style: style)) + .modifier(BigIconModifier(style: style)) } } -private struct HeroImageModifier: ViewModifier { - let style: HeroImage.Style +private struct BigIconModifier: ViewModifier { + let style: BigIcon.Style func body(content: Content) -> some View { content - .scaledFrame(size: 70, relativeTo: .title) + .scaledFrame(size: 64, relativeTo: .title) .foregroundColor(style.foregroundColor) .background { RoundedRectangle(cornerRadius: 14) @@ -87,22 +84,32 @@ private struct HeroImageModifier: ViewModifier { // MARK: - Previews -struct HeroImage_Previews: PreviewProvider, TestablePreview { +struct BigIcon_Previews: PreviewProvider, TestablePreview { static var previews: some View { - VStack(spacing: 20) { + VStack(spacing: 40) { HStack(spacing: 20) { - HeroImage(icon: \.lockSolid) + BigIcon(icon: \.lockSolid) Image(systemName: "hourglass") - .heroImage() + .bigIcon() Image(asset: Asset.Images.serverSelectionIcon) - .heroImage(insets: 19) + .bigIcon(insets: 19) } - HStack(spacing: 20) { - HeroImage(icon: \.helpSolid, style: .subtle) - HeroImage(icon: \.checkCircleSolid, style: .success) - HeroImage(icon: \.error, style: .critical) - HeroImage(icon: \.error, style: .criticalOnSecondary) + VStack(spacing: 20) { + HStack(spacing: 20) { + BigIcon(icon: \.helpSolid) + BigIcon(icon: \.helpSolid, style: .default) + } + + HStack(spacing: 20) { + BigIcon(icon: \.error, style: .alertSolid) + BigIcon(icon: \.error, style: .alert) + } + + HStack(spacing: 20) { + BigIcon(icon: \.checkCircleSolid, style: .successSolid) + BigIcon(icon: \.checkCircleSolid, style: .success) + } } } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index f311b321f8..7daf4655de 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -13,28 +13,48 @@ struct LoadableAvatarImage: View { private let contentID: String? private let avatarSize: AvatarSize private let mediaProvider: MediaProviderProtocol? + private let onTap: ((URL) -> Void)? @ScaledMetric private var frameSize: CGFloat - init(url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol?) { + init(url: URL?, name: String?, + contentID: String?, + avatarSize: AvatarSize, + mediaProvider: MediaProviderProtocol?, + onTap: ((URL) -> Void)? = nil) { self.url = url self.name = name self.contentID = contentID self.avatarSize = avatarSize self.mediaProvider = mediaProvider + self.onTap = onTap _frameSize = ScaledMetric(wrappedValue: avatarSize.value) } var body: some View { + if let onTap, let url { + Button { + onTap(url) + } label: { + clippedAvatar + } + .buttonStyle(.borderless) // Add a button style to stop the whole row being tappable. + } else { + clippedAvatar + } + } + + private var clippedAvatar: some View { avatar .frame(width: frameSize, height: frameSize) .background(Color.compound.bgCanvasDefault) .clipShape(Circle()) + .environment(\.shouldAutomaticallyLoadImages, true) // We always load avatars. } @ViewBuilder - var avatar: some View { + private var avatar: some View { if let url { LoadableImage(url: url, mediaType: .avatar, diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift index d1183ca400..1f909688bb 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift @@ -6,12 +6,17 @@ // import Combine +import Compound import Kingfisher import SwiftUI /// Used to configure animations enum LoadableImageMediaType { + /// An avatar (can be displayed anywhere within the app). case avatar + /// An image displayed in the timeline. + case timelineItem + /// Any other media (can be displayed anywhere within the app). case generic } @@ -79,6 +84,8 @@ struct LoadableImage: View { } private struct LoadableImageContent: View, ImageDataProvider { + @Environment(\.shouldAutomaticallyLoadImages) private var loadAutomatically + private let mediaSource: MediaSourceProxy private let mediaType: LoadableImageMediaType private let blurhash: String? @@ -86,6 +93,7 @@ private struct LoadableImageContent PlaceholderView @StateObject private var contentLoader: ContentLoader + @State private var loadManually = false init(mediaSource: MediaSourceProxy, mediaType: LoadableImageMediaType, @@ -104,36 +112,40 @@ private struct LoadableImageContent some View { Color.compound._bgBubbleIncoming } + static func transformer(_ view: AnyView) -> some View { + view.overlay { + Image(systemSymbol: .playCircleFill) + .font(.largeTitle) + .foregroundStyle(.compound.iconAccentPrimary) + } + } + + static func makeMediaProvider(isLoading: Bool = false) -> MediaProviderProtocol { + let mediaProvider = MediaProviderMock(configuration: .init()) + + if isLoading { + mediaProvider.imageFromSourceSizeClosure = { _, _ in nil } + mediaProvider.loadFileFromSourceFilenameClosure = { _, _ in .failure(.failedRetrievingFile) } + mediaProvider.loadImageDataFromSourceClosure = { _ in .failure(.failedRetrievingImage) } + mediaProvider.loadImageFromSourceSizeClosure = { _, _ in .failure(.failedRetrievingImage) } + mediaProvider.loadThumbnailForSourceSourceSizeClosure = { _, _ in .failure(.failedRetrievingThumbnail) } + mediaProvider.loadImageRetryingOnReconnectionSizeClosure = { _, _ in + Task { throw MediaProviderError.failedRetrievingImage } + } + } + return mediaProvider + } +} + +private extension View { + func layout(title: String, hideTimelineMedia: Bool = false) -> some View { + aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay(alignment: .bottom) { + Text(title) + .font(.caption2) + .offset(y: 16) + .padding(.horizontal, -5) + } + .environment(\.shouldAutomaticallyLoadImages, !hideTimelineMedia) + } +} diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift index c7f77b116f..17cf55e783 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift @@ -25,6 +25,8 @@ struct RoomAvatarImage: View { let avatarSize: AvatarSize let mediaProvider: MediaProviderProtocol? + private(set) var onAvatarTap: ((URL) -> Void)? + var body: some View { switch avatar { case .room(let id, let name, let avatarURL): @@ -32,7 +34,8 @@ struct RoomAvatarImage: View { name: name, contentID: id, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) case .heroes(let users): // We will expand upon this with more stack sizes in the future. if users.count == 0 { @@ -45,14 +48,16 @@ struct RoomAvatarImage: View { name: users[0].displayName, contentID: users[0].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) .scaledFrame(size: clusterSize, alignment: .topTrailing) LoadableAvatarImage(url: users[1].avatarURL, name: users[1].displayName, contentID: users[1].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) .mask { Rectangle() .fill(Color.white) @@ -74,7 +79,8 @@ struct RoomAvatarImage: View { name: users[0].displayName, contentID: users[0].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) } } } @@ -87,30 +93,30 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview { name: "Room", avatarURL: nil), avatarSize: .room(on: .home), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomAvatarImage(avatar: .room(id: "!2:server.com", name: "Room", avatarURL: .picturesDirectory), avatarSize: .room(on: .home), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com", displayName: "User", avatarURL: nil)]), avatarSize: .room(on: .home), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com", displayName: "User", avatarURL: .picturesDirectory)]), avatarSize: .room(on: .home), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomAvatarImage(avatar: .heroes([.init(userID: "@alice:server.com", displayName: "Alice", avatarURL: nil), .init(userID: "@bob:server.net", displayName: "Bob", avatarURL: nil)]), avatarSize: .room(on: .home), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift similarity index 81% rename from ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift rename to ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift index e138e2fac6..c7291c4412 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift @@ -24,8 +24,8 @@ struct RoomHeaderView: View { .font(.compound.bodyLGSemibold) .accessibilityIdentifier(A11yIdentifiers.roomScreen.name) } - // Leading align whilst using the principal toolbar position. - .frame(maxWidth: .infinity, alignment: .leading) + // Take up as much space as possible, with a leading alignment for use in the principal toolbar position. + .frame(idealWidth: .greatestFiniteMagnitude, maxWidth: .infinity, alignment: .leading) } private var avatarImage: some View { @@ -42,7 +42,7 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { roomAvatar: .room(id: "1", name: "Some Room Name", avatarURL: URL.picturesDirectory), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .previewLayout(.sizeThatFits) .padding() @@ -50,7 +50,7 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { roomAvatar: .room(id: "1", name: "Some Room Name", avatarURL: nil), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .previewLayout(.sizeThatFits) .padding() } diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift index e3fa721ab1..2d7a71eb8f 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomInviterLabel.swift @@ -62,13 +62,13 @@ struct RoomInviterLabel_Previews: PreviewProvider, TestablePreview { static var previews: some View { VStack(spacing: 10) { RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockAlice), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockDan), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockNoName), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) RoomInviterLabel(inviter: .init(member: RoomMemberProxyMock.mockCharlie), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .foregroundStyle(.compound.textPrimary) } .font(.compound.bodyMD) diff --git a/ElementX/Sources/Other/SwiftUI/Views/UserProfileListRow.swift b/ElementX/Sources/Other/SwiftUI/Views/UserProfileListRow.swift index 762fbce485..4d43bfbd16 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/UserProfileListRow.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/UserProfileListRow.swift @@ -65,21 +65,21 @@ struct UserProfileCell_Previews: PreviewProvider, TestablePreview { static var previews: some View { Form { - UserProfileListRow(user: .mockAlice, membership: nil, mediaProvider: MockMediaProvider(), + UserProfileListRow(user: .mockAlice, membership: nil, mediaProvider: MediaProviderMock(configuration: .init()), kind: .multiSelection(isSelected: true, action: action)) - UserProfileListRow(user: .mockBob, membership: nil, mediaProvider: MockMediaProvider(), + UserProfileListRow(user: .mockBob, membership: nil, mediaProvider: MediaProviderMock(configuration: .init()), kind: .multiSelection(isSelected: false, action: action)) - UserProfileListRow(user: .mockCharlie, membership: .join, mediaProvider: MockMediaProvider(), + UserProfileListRow(user: .mockCharlie, membership: .join, mediaProvider: MediaProviderMock(configuration: .init()), kind: .multiSelection(isSelected: true, action: action)) .disabled(true) - UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: .join, mediaProvider: MockMediaProvider(), + UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: .join, mediaProvider: MediaProviderMock(configuration: .init()), kind: .multiSelection(isSelected: false, action: action)) .disabled(true) - UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: nil, mediaProvider: MockMediaProvider(), + UserProfileListRow(user: .init(userID: "@someone:matrix.org"), membership: nil, mediaProvider: MediaProviderMock(configuration: .init()), kind: .multiSelection(isSelected: false, action: action)) } .compoundList() diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoundedLabelItem.swift b/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift similarity index 70% rename from ElementX/Sources/Other/SwiftUI/Views/RoundedLabelItem.swift rename to ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift index d13e8c0aab..e84e00519b 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoundedLabelItem.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift @@ -24,11 +24,11 @@ enum ListPosition { } } -struct RoundedLabelItem: View { +struct VisualListItem: View { @Environment(\.backgroundStyle) private var backgroundStyle let title: String - let listPosition: ListPosition + let position: ListPosition let iconContent: () -> Icon private var backgroundColor: AnyShapeStyle { @@ -39,17 +39,17 @@ struct RoundedLabelItem: View { Label { Text(title) } icon: { iconContent() } - .labelStyle(CheckmarkLabelStyle()) - .padding(.horizontal, 20) + .labelStyle(VisualListItemLabelStyle()) + .padding(.horizontal, 16) .padding(.vertical, 12) .frame(maxWidth: .infinity, alignment: .leading) - .background(backgroundColor, in: RoundedCornerShape(radius: 16, corners: listPosition.roundedCorners)) + .background(backgroundColor, in: RoundedCornerShape(radius: 14, corners: position.roundedCorners)) } } -private struct CheckmarkLabelStyle: LabelStyle { +private struct VisualListItemLabelStyle: LabelStyle { func makeBody(configuration: Configuration) -> some View { - HStack(alignment: .top, spacing: 16) { + HStack(alignment: .top, spacing: 12) { configuration.icon configuration.title } @@ -60,7 +60,7 @@ private struct CheckmarkLabelStyle: LabelStyle { // MARK: - Previews -struct AnalyticsPromptScreenCheckmarkItem_Previews: PreviewProvider, TestablePreview { +struct VisualListItem_Previews: PreviewProvider, TestablePreview { static let strings = AnalyticsPromptScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL) @ViewBuilder @@ -75,17 +75,17 @@ struct AnalyticsPromptScreenCheckmarkItem_Previews: PreviewProvider, TestablePre static var previews: some View { VStack(alignment: .leading, spacing: 4) { - RoundedLabelItem(title: strings.point1, listPosition: .top) { + VisualListItem(title: strings.point1, position: .top) { testImage1 } - RoundedLabelItem(title: strings.point2, listPosition: .middle) { + VisualListItem(title: strings.point2, position: .middle) { testImage2 } - RoundedLabelItem(title: "This is a short string.", listPosition: .middle) { + VisualListItem(title: "This is a short string.", position: .middle) { testImage1 } - RoundedLabelItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.", - listPosition: .bottom) { + VisualListItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.", + position: .bottom) { testImage2 } } diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift index f607fbdc38..45c20b95b4 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift @@ -29,7 +29,7 @@ struct AppLockScreen: View { pinInputField .padding(.bottom, 16) .offset(x: pinInputFieldOffset) - .onChange(of: context.viewState.numberOfPINAttempts) { newValue in + .onChange(of: context.viewState.numberOfPINAttempts) { _, newValue in guard newValue > 0 else { return } // Reset without animation in Previews. accessibilitySubtitleFocus = true Task { await animatePINFailure() } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift index 95a93f289b..d0ca7d03d8 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift @@ -58,7 +58,7 @@ struct AppLockSetupPINScreen: View { var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.lockSolid) + BigIcon(icon: \.lockSolid) .padding(.bottom, 8) Text(context.viewState.title) diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift index 09f10d3655..408a7f8381 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift @@ -20,7 +20,7 @@ struct PINTextField: View { .textFieldStyle(PINTextFieldStyle(pinCode: pinCode, isSecure: isSecure, maxLength: maxLength, size: size)) .keyboardType(.numberPad) .accessibilityIdentifier(A11yIdentifiers.appLockSetupPINScreen.textField) - .onChange(of: pinCode) { newValue in + .onChange(of: pinCode) { _, newValue in let sanitized = sanitize(newValue) if sanitized != newValue { MXLog.warning("PIN code input sanitized.") diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/View/AppLockSetupSettingsScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/View/AppLockSetupSettingsScreen.swift index 097b27079c..3cb1dfc362 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/View/AppLockSetupSettingsScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/View/AppLockSetupSettingsScreen.swift @@ -29,7 +29,7 @@ struct AppLockSetupSettingsScreen: View { Section { ListRow(label: .plain(title: context.viewState.enableBiometricsTitle), kind: .toggle($context.enableBiometrics)) - .onChange(of: context.enableBiometrics) { _ in + .onChange(of: context.enableBiometrics) { context.send(viewAction: .enableBiometricsChanged) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 5bfb33da79..9f3e1fc763 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -25,6 +25,11 @@ struct LoginHomeserver: Equatable { self.registrationHelperURL = registrationHelperURL } + /// Whether or not the app is able to register on this homeserver. + var supportsRegistration: Bool { + loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil) + } + /// Sanitizes a user entered homeserver address with the following rules /// - Trim any whitespace. /// - Lowercase the address. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift index 775b7eedc4..9d7465355e 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift @@ -20,10 +20,8 @@ enum LoginMode: Equatable { var supportsOIDCFlow: Bool { switch self { - case .oidc: - return true - default: - return false + case .oidc: true + default: false } } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index 9c6d3e2be4..66e9524750 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -11,9 +11,9 @@ import SwiftUI struct LoginScreenCoordinatorParameters { /// The service used to authenticate the user. let authenticationService: AuthenticationServiceProtocol - - let analytics: AnalyticsService + let slidingSyncLearnMoreURL: URL let userIndicatorController: UserIndicatorControllerProtocol + let analytics: AnalyticsService } enum LoginScreenCoordinatorAction { @@ -42,8 +42,10 @@ final class LoginScreenCoordinator: CoordinatorProtocol { init(parameters: LoginScreenCoordinatorParameters) { self.parameters = parameters - viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver.value, - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) + viewModel = LoginScreenViewModel(authenticationService: parameters.authenticationService, + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL, + userIndicatorController: parameters.userIndicatorController, + analytics: parameters.analytics) } // MARK: - Public @@ -54,119 +56,20 @@ final class LoginScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .parseUsername(let username): - parseUsername(username) - case .forgotPassword: - showForgotPasswordScreen() - case .login(let username, let password): - login(username: username, password: password) + case .configuredForOIDC: + actionsSubject.send(.configuredForOIDC) + case .signedIn(let userSession): + actionsSubject.send(.signedIn(userSession)) } } .store(in: &cancellables) } func stop() { - stopLoading() + viewModel.stopLoading() } func toPresentable() -> AnyView { AnyView(LoginScreen(context: viewModel.context)) } - - // MARK: - Private - - private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading" - - private func startLoading(isInteractionBlocking: Bool) { - if isInteractionBlocking { - parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, - type: .modal, - title: L10n.commonLoading, - persistent: true)) - } else { - viewModel.update(isLoading: true) - } - } - - private func stopLoading() { - viewModel.update(isLoading: false) - parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) - } - - /// Processes an error to either update the flow or display it to the user. - private func handleError(_ error: AuthenticationServiceError) { - MXLog.info("Error occurred: \(error)") - - switch error { - case .invalidCredentials: - viewModel.displayError(.alert(L10n.screenLoginErrorInvalidCredentials)) - case .accountDeactivated: - viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount)) - case .invalidWellKnown(let error): - viewModel.displayError(.invalidWellKnownAlert(error)) - case .slidingSyncNotAvailable: - viewModel.displayError(.slidingSyncAlert) - case .sessionTokenRefreshNotSupported: - viewModel.displayError(.refreshTokenAlert) - default: - viewModel.displayError(.alert(L10n.errorUnknown)) - } - } - - /// Requests the authentication coordinator to log in using the specified credentials. - private func login(username: String, password: String) { - MXLog.info("Starting login with password.") - startLoading(isInteractionBlocking: true) - - Task { - parameters.analytics.signpost.beginLogin() - switch await authenticationService.login(username: username, - password: password, - initialDeviceName: UIDevice.current.initialDeviceName, - deviceID: nil) { - case .success(let userSession): - actionsSubject.send(.signedIn(userSession)) - parameters.analytics.signpost.endLogin() - stopLoading() - case .failure(let error): - stopLoading() - parameters.analytics.signpost.endLogin() - handleError(error) - } - } - } - - /// Parses the specified username and looks up the homeserver when a Matrix ID is entered. - private func parseUsername(_ username: String) { - guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return } - - let homeserverDomain = String(username.split(separator: ":")[1]) - - startLoading(isInteractionBlocking: false) - - Task { - switch await authenticationService.configure(for: homeserverDomain) { - case .success: - stopLoading() - if authenticationService.homeserver.value.loginMode == .oidc { - actionsSubject.send(.configuredForOIDC) - } else { - updateViewModel() - } - case .failure(let error): - stopLoading() - handleError(error) - } - } - } - - /// Updates the view model with a different homeserver. - private func updateViewModel() { - viewModel.update(homeserver: authenticationService.homeserver.value) - } - - /// Shows the forgot password screen. - private func showForgotPasswordScreen() { - viewModel.displayError(.alert("Not implemented.")) - } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenModels.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenModels.swift index e9c80169aa..b7979bd7c6 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenModels.swift @@ -7,23 +7,16 @@ import Foundation -enum LoginScreenViewModelAction: CustomStringConvertible { - /// Parse the username and update the homeserver if included. - case parseUsername(String) - /// The user would like to reset their password. - case forgotPassword - /// Login using the supplied credentials. - case login(username: String, password: String) +enum LoginScreenViewModelAction { + /// The homeserver was updated to one that supports OIDC. + case configuredForOIDC + /// Login was successful. + case signedIn(UserSessionProtocol) - /// A string representation of the action, ignoring any associated values that could leak PII. - var description: String { + var isConfiguredForOIDC: Bool { switch self { - case .parseUsername: - return "parseUsername" - case .forgotPassword: - return "forgotPassword" - case .login: - return "login" + case .configuredForOIDC: true + default: false } } } @@ -34,7 +27,7 @@ struct LoginScreenViewState: BindableState { /// Whether a new homeserver is currently being loaded. var isLoading = false /// View state that can be bound to from SwiftUI. - var bindings: LoginScreenBindings + var bindings = LoginScreenBindings() /// The types of login supported by the homeserver. var loginMode: LoginMode { homeserver.loginMode } @@ -62,8 +55,6 @@ struct LoginScreenBindings { enum LoginScreenViewAction { /// Parse the username to detect if a homeserver is included. case parseUsername - /// The user would like to reset their password. - case forgotPassword /// Continue using the input username and password. case next } @@ -71,8 +62,10 @@ enum LoginScreenViewAction { enum LoginScreenErrorType: Hashable { /// A specific error message shown in an alert. case alert(String) - /// Looking up the homeserver from the username failed. - case invalidHomeserver + /// An alert that informs the user to check their username/password. + case credentialsAlert + /// An alert that informs the user that their account has been deactivated. + case deactivatedAlert /// An alert that informs the user about a bad well-known file. case invalidWellKnownAlert(String) /// An alert that allows the user to learn about sliding sync. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift index 0c81c9e412..3935c8e309 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift @@ -11,57 +11,129 @@ import SwiftUI typealias LoginScreenViewModelType = StateStoreViewModel class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol { + private let authenticationService: AuthenticationServiceProtocol private let slidingSyncLearnMoreURL: URL + private let userIndicatorController: UserIndicatorControllerProtocol + private let analytics: AnalyticsService private var actionsSubject: PassthroughSubject = .init() - var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(homeserver: LoginHomeserver, slidingSyncLearnMoreURL: URL) { + init(authenticationService: AuthenticationServiceProtocol, + slidingSyncLearnMoreURL: URL, + userIndicatorController: UserIndicatorControllerProtocol, + analytics: AnalyticsService) { + self.authenticationService = authenticationService self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL - let bindings = LoginScreenBindings() - let viewState = LoginScreenViewState(homeserver: homeserver, bindings: bindings) + self.userIndicatorController = userIndicatorController + self.analytics = analytics + + let viewState = LoginScreenViewState(homeserver: authenticationService.homeserver.value) super.init(initialViewState: viewState) + + authenticationService.homeserver + .receive(on: DispatchQueue.main) + .weakAssign(to: \.state.homeserver, on: self) + .store(in: &cancellables) } override func process(viewAction: LoginScreenViewAction) { switch viewAction { case .parseUsername: - actionsSubject.send(.parseUsername(state.bindings.username)) - case .forgotPassword: - actionsSubject.send(.forgotPassword) + parseUsername() case .next: - actionsSubject.send(.login(username: state.bindings.username, password: state.bindings.password)) + login() + } + } + + func stopLoading() { + state.isLoading = false + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } + + // MARK: - Private + + /// Parses the specified username and looks up the homeserver when a Matrix ID is entered. + private func parseUsername() { + let username = state.bindings.username + + guard MatrixEntityRegex.isMatrixUserIdentifier(username) else { return } + + let homeserverDomain = String(username.split(separator: ":")[1]) + + startLoading(isInteractionBlocking: false) + + Task { + switch await authenticationService.configure(for: homeserverDomain, flow: .login) { + case .success: + if authenticationService.homeserver.value.loginMode == .oidc { + actionsSubject.send(.configuredForOIDC) + } + stopLoading() + case .failure(let error): + stopLoading() + handleError(error) + } } } - func update(isLoading: Bool) { - guard state.isLoading != isLoading else { return } - state.isLoading = isLoading + /// Requests the authentication coordinator to log in using the specified credentials. + private func login() { + MXLog.info("Starting login with password.") + startLoading(isInteractionBlocking: true) + + Task { + analytics.signpost.beginLogin() + switch await authenticationService.login(username: state.bindings.username, + password: state.bindings.password, + initialDeviceName: UIDevice.current.initialDeviceName, + deviceID: nil) { + case .success(let userSession): + actionsSubject.send(.signedIn(userSession)) + analytics.signpost.endLogin() + stopLoading() + case .failure(let error): + stopLoading() + analytics.signpost.endLogin() + handleError(error) + } + } } - func update(homeserver: LoginHomeserver) { - state.homeserver = homeserver + private static let loadingIndicatorIdentifier = "\(LoginScreenCoordinatorAction.self)-Loading" + + private func startLoading(isInteractionBlocking: Bool) { + if isInteractionBlocking { + userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: L10n.commonLoading, + persistent: true)) + } else { + state.isLoading = true + } } - func displayError(_ type: LoginScreenErrorType) { - switch type { - case .alert(let message): - state.bindings.alertInfo = AlertInfo(id: type, + /// Processes an error to either update the flow or display it to the user. + private func handleError(_ error: AuthenticationServiceError) { + MXLog.info("Error occurred: \(error)") + + switch error { + case .invalidCredentials: + state.bindings.alertInfo = AlertInfo(id: .credentialsAlert, title: L10n.commonError, - message: message) - case .invalidHomeserver: - state.bindings.alertInfo = AlertInfo(id: type, + message: L10n.screenLoginErrorInvalidCredentials) + case .accountDeactivated: + state.bindings.alertInfo = AlertInfo(id: .deactivatedAlert, title: L10n.commonError, - message: L10n.screenLoginErrorInvalidUserId) - case .invalidWellKnownAlert(let error): + message: L10n.screenLoginErrorDeactivatedAccount) + case .invalidWellKnown(let error): state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorInvalidWellKnown(error)) - case .slidingSyncAlert: + case .slidingSyncNotAvailable: let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, title: L10n.commonServerNotSupported, @@ -71,12 +143,12 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc // Clear out the invalid username to avoid an attempted login to matrix.org state.bindings.username = "" - case .refreshTokenAlert: - state.bindings.alertInfo = AlertInfo(id: type, + case .sessionTokenRefreshNotSupported: + state.bindings.alertInfo = AlertInfo(id: .refreshTokenAlert, title: L10n.commonServerNotSupported, message: L10n.screenLoginErrorRefreshTokens) - case .unknown: - state.bindings.alertInfo = AlertInfo(id: type) + default: + state.bindings.alertInfo = AlertInfo(id: .unknown) } } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift index 303c6151c9..cccafd219c 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift @@ -12,15 +12,6 @@ protocol LoginScreenViewModelProtocol { var actions: AnyPublisher { get } var context: LoginScreenViewModelType.Context { get } - /// Update the view to reflect that a new homeserver is being loaded. - /// - Parameter isLoading: Whether or not the homeserver is being loaded. - func update(isLoading: Bool) - - /// Update the view with new homeserver information. - /// - Parameter homeserver: The view data for the homeserver. This can be generated using `AuthenticationService.Homeserver.viewData`. - func update(homeserver: LoginHomeserver) - - /// Display an error to the user. - /// - Parameter type: The type of error to be displayed. - func displayError(_ type: LoginScreenErrorType) + /// Update the view to reflect that loaded has finished. + func stopLoading() } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 69408cd759..3226fcd928 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -29,6 +29,7 @@ struct LoginScreen: View { // This should never be shown. ProgressView() default: + // This should never be shown either. loginUnavailableText } } @@ -37,13 +38,14 @@ struct LoginScreen: View { .padding(.bottom, 16) } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .navigationBarTitleDisplayMode(.inline) .alert(item: $context.alertInfo) } /// The header containing the title and icon. var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.lockSolid) + BigIcon(icon: \.lockSolid) .padding(.bottom, 8) Text(L10n.screenLoginTitleWithHomeserver(context.viewState.homeserver.address)) @@ -72,7 +74,9 @@ struct LoginScreen: View { .textContentType(.username) .autocapitalization(.none) .submitLabel(.next) - .onChange(of: isUsernameFocused, perform: usernameFocusChanged) + .onChange(of: isUsernameFocused) { _, newValue in + usernameFocusChanged(isFocussed: newValue) + } .onSubmit { isPasswordFocused = true } .padding(.bottom, 20) @@ -124,35 +128,45 @@ struct LoginScreen: View { // MARK: - Previews struct LoginScreen_Previews: PreviewProvider, TestablePreview { - static let credentialsViewModel: LoginScreenViewModel = { - let viewModel = LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - viewModel.context.username = "alice" - viewModel.context.password = "password" - return viewModel - }() + static let viewModel = makeViewModel() + static let credentialsViewModel = makeViewModel(withCredentials: true) + static let unconfiguredViewModel = makeViewModel(homeserverAddress: "somethingtofailconfiguration") static var previews: some View { - screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)) - .previewDisplayName("matrix.org") - screen(for: credentialsViewModel) - .previewDisplayName("Credentials Entered") - screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)) - .previewDisplayName("Unsupported") - screen(for: LoginScreenViewModel(homeserver: .mockMatrixDotOrg, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL)) - .previewDisplayName("OIDC Fallback") - } - - static func screen(for viewModel: LoginScreenViewModel) -> some View { NavigationStack { LoginScreen(context: viewModel.context) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button { } label: { - Text("\(Image(systemName: "chevron.backward")) Back") - } - } - } } + .previewDisplayName("matrix.org") + .snapshotPreferences(delay: 1) + + NavigationStack { + LoginScreen(context: credentialsViewModel.context) + } + .previewDisplayName("Credentials Entered") + .snapshotPreferences(delay: 1) + + NavigationStack { + LoginScreen(context: unconfiguredViewModel.context) + } + .previewDisplayName("Unsupported") + .snapshotPreferences(delay: 1) + } + + static func makeViewModel(homeserverAddress: String = "matrix.org", withCredentials: Bool = false) -> LoginScreenViewModel { + let authenticationService = AuthenticationService.mock + + Task { await authenticationService.configure(for: homeserverAddress, flow: .login) } + + let viewModel = LoginScreenViewModel(authenticationService: authenticationService, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) + + if withCredentials { + viewModel.context.username = "alice" + viewModel.context.password = "password" + } + + return viewModel } } diff --git a/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift index 03b20e502e..d4f85f83e1 100644 --- a/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift +++ b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift @@ -14,16 +14,6 @@ class OIDCAuthenticationPresenter: NSObject { private let oidcRedirectURL: URL private let presentationAnchor: UIWindow - /// The data required to complete a request. - struct Request { - let session: ASWebAuthenticationSession - let oidcData: OIDCAuthorizationDataProxy - let continuation: CheckedContinuation, Never> - } - - /// The current request in progress. This is a single use value and will be moved on access. - @Consumable private var request: Request? - init(authenticationService: AuthenticationServiceProtocol, oidcRedirectURL: URL, presentationAnchor: UIWindow) { self.authenticationService = authenticationService self.oidcRedirectURL = oidcRedirectURL @@ -33,74 +23,35 @@ class OIDCAuthenticationPresenter: NSObject { /// Presents a web authentication session for the supplied data. func authenticate(using oidcData: OIDCAuthorizationDataProxy) async -> Result { - await withCheckedContinuation { continuation in - let session = ASWebAuthenticationSession(url: oidcData.url, - callbackURLScheme: oidcRedirectURL.scheme) { [weak self] url, error in - // This closure won't be called if the scheme is https, see handleUniversalLinkCallback for more info. - guard let self else { return } - - guard let url else { - // Check for user cancellation to avoid showing an alert in that instance. - if let nsError = error as? NSError, - nsError.domain == ASWebAuthenticationSessionErrorDomain, - nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { - self.completeAuthentication(throwing: .oidcError(.userCancellation)) - return - } - - self.completeAuthentication(throwing: .oidcError(.unknown)) - return - } - - completeAuthentication(callbackURL: url) + let (url, error) = await withCheckedContinuation { continuation in + let session = ASWebAuthenticationSession(url: oidcData.url, callback: .oidcRedirectURL(oidcRedirectURL)) { url, error in + continuation.resume(returning: (url, error)) } session.prefersEphemeralWebBrowserSession = false session.presentationContextProvider = self - request = Request(session: session, oidcData: oidcData, continuation: continuation) - session.start() } - } - - /// This method will be used if the `appSettings.oidcRedirectURL`'s scheme is `https`. - /// When using a custom scheme, the redirect will be handled by the web auth session's closure. - func handleUniversalLinkCallback(_ url: URL) { - completeAuthentication(callbackURL: url) - } - - /// Completes the authentication by exchanging the callback URL for a user session. - private func completeAuthentication(callbackURL: URL) { - guard let request else { - MXLog.error("Failed to complete authentication. Missing request.") - return - } - if callbackURL.scheme?.starts(with: "http") == true { - request.session.cancel() - } - - Task { - switch await authenticationService.loginWithOIDCCallback(callbackURL, data: request.oidcData) { - case .success(let userSession): - request.continuation.resume(returning: .success(userSession)) - case .failure(let error): - request.continuation.resume(returning: .failure(error)) + guard let url else { + // Check for user cancellation to avoid showing an alert in that instance. + if let nsError = error as? NSError, + nsError.domain == ASWebAuthenticationSessionErrorDomain, + nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { + await authenticationService.abortOIDCLogin(data: oidcData) + return .failure(.oidcError(.userCancellation)) } - } - } - - /// Aborts the authentication with the supplied error. - private func completeAuthentication(throwing error: AuthenticationServiceError) { - guard let request else { - MXLog.error("Failed to throw authentication error. Missing request.") - return + + await authenticationService.abortOIDCLogin(data: oidcData) + return .failure(.oidcError(.unknown)) } - Task { - await authenticationService.abortOIDCLogin(data: request.oidcData) - request.continuation.resume(returning: .failure(error)) + switch await authenticationService.loginWithOIDCCallback(url, data: oidcData) { + case .success(let userSession): + return .success(userSession) + case .failure(let error): + return .failure(error) } } } @@ -110,3 +61,15 @@ class OIDCAuthenticationPresenter: NSObject { extension OIDCAuthenticationPresenter: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { presentationAnchor } } + +extension ASWebAuthenticationSession.Callback { + static func oidcRedirectURL(_ url: URL) -> Self { + if url.scheme == "https", let host = url.host() { + .https(host: host, path: url.path()) + } else if let scheme = url.scheme { + .customScheme(scheme) + } else { + fatalError("Invalid OIDC redirect URL: \(url)") + } + } +} diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift index 37bd5f4845..6d13d835f7 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift @@ -11,6 +11,8 @@ import SwiftUI struct ServerConfirmationScreenCoordinatorParameters { let authenticationService: AuthenticationServiceProtocol let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL + let userIndicatorController: UserIndicatorControllerProtocol } enum ServerConfirmationScreenCoordinatorAction { @@ -29,7 +31,9 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol { init(parameters: ServerConfirmationScreenCoordinatorParameters) { viewModel = ServerConfirmationScreenViewModel(authenticationService: parameters.authenticationService, - authenticationFlow: parameters.authenticationFlow) + authenticationFlow: parameters.authenticationFlow, + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL, + userIndicatorController: parameters.userIndicatorController) } func start() { diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index d456b9ebaa..352bb7e28a 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -19,11 +19,11 @@ struct ServerConfirmationScreenViewState: BindableState { var homeserverAddress: String /// The flow being attempted on the selected homeserver. let authenticationFlow: AuthenticationFlow - /// Whether or not the homeserver supports registration. - var homeserverSupportsRegistration = false /// The presentation anchor used for OIDC authentication. var window: UIWindow? + var bindings = ServerConfirmationScreenBindings() + /// The screen's title. var title: String { switch authenticationFlow { @@ -46,23 +46,16 @@ struct ServerConfirmationScreenViewState: BindableState { "" } case .register: - if canContinue { - L10n.screenServerConfirmationMessageRegister - } else { - L10n.errorAccountCreationNotPossible - } - } - } - - /// Whether or not it is valid to continue the flow. - var canContinue: Bool { - switch authenticationFlow { - case .login: true - case .register: homeserverSupportsRegistration + L10n.screenServerConfirmationMessageRegister } } } +struct ServerConfirmationScreenBindings { + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + enum ServerConfirmationScreenViewAction { /// Updates the window used as the OIDC presentation anchor. case updateWindow(UIWindow) @@ -71,3 +64,18 @@ enum ServerConfirmationScreenViewAction { /// The user would like to change to a different homeserver. case changeServer } + +enum ServerConfirmationScreenAlert: Hashable { + /// An alert that informs the user that a server could not be found. + case homeserverNotFound + /// An alert that informs the user about a bad well-known file. + case invalidWellKnown(String) + /// An alert that allows the user to learn about sliding sync. + case slidingSync + /// An alert that informs the user that login isn't supported. + case login + /// An alert that informs the user that registration isn't supported. + case registration + /// An unknown error has occurred. + case unknownError +} diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index 717c8f76fc..b893a25328 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -11,46 +11,122 @@ import SwiftUI typealias ServerConfirmationScreenViewModelType = StateStoreViewModel class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, ServerConfirmationScreenViewModelProtocol { + let authenticationService: AuthenticationServiceProtocol + let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL + let userIndicatorController: UserIndicatorControllerProtocol + private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow) { - let homeserver = authenticationService.homeserver.value + init(authenticationService: AuthenticationServiceProtocol, + authenticationFlow: AuthenticationFlow, + slidingSyncLearnMoreURL: URL, + userIndicatorController: UserIndicatorControllerProtocol) { + self.authenticationService = authenticationService + self.authenticationFlow = authenticationFlow + self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL + self.userIndicatorController = userIndicatorController + let homeserver = authenticationService.homeserver.value super.init(initialViewState: ServerConfirmationScreenViewState(homeserverAddress: homeserver.address, - authenticationFlow: authenticationFlow, - homeserverSupportsRegistration: homeserver.supportsRegistration)) + authenticationFlow: authenticationFlow)) authenticationService.homeserver .receive(on: DispatchQueue.main) - .sink { [weak self] homeserver in - guard let self else { return } - state.homeserverAddress = homeserver.address - state.homeserverSupportsRegistration = homeserver.supportsRegistration - } + .map(\.address) + .weakAssign(to: \.state.homeserverAddress, on: self) .store(in: &cancellables) } - // MARK: - Public - override func process(viewAction: ServerConfirmationScreenViewAction) { switch viewAction { case .updateWindow(let window): guard state.window != window else { return } Task { state.window = window } case .confirm: - actionsSubject.send(.confirm) + Task { await configureAndContinue() } case .changeServer: actionsSubject.send(.changeServer) } } -} - -extension LoginHomeserver { - var supportsRegistration: Bool { - loginMode == .oidc || (address == "matrix.org" && registrationHelperURL != nil) + + // MARK: - Private + + private func configureAndContinue() async { + let homeserver = authenticationService.homeserver.value + + // If the login mode is unknown, the service hasn't be configured and we need to do it now. + // Otherwise we can continue the flow as server selection has been performed and succeeded. + guard homeserver.loginMode == .unknown || authenticationService.flow != authenticationFlow else { + actionsSubject.send(.confirm) + return + } + + startLoading() + defer { stopLoading() } + + switch await authenticationService.configure(for: homeserver.address, flow: authenticationFlow) { + case .success: + actionsSubject.send(.confirm) + case .failure(let error): + switch error { + case .invalidServer, .invalidHomeserverAddress: + displayError(.homeserverNotFound) + case .invalidWellKnown(let error): + displayError(.invalidWellKnown(error)) + case .slidingSyncNotAvailable: + displayError(.slidingSync) + case .loginNotSupported: + displayError(.login) + case .registrationNotSupported: + displayError(.registration) + default: + displayError(.unknownError) + } + } + } + + private func startLoading(label: String = L10n.commonLoading) { + userIndicatorController.submitIndicator(UserIndicator(type: .modal, + title: label, + persistent: true)) + } + + private func stopLoading() { + userIndicatorController.retractAllIndicators() + } + + private func displayError(_ type: ServerConfirmationScreenAlert) { + switch type { + case .homeserverNotFound: + state.bindings.alertInfo = AlertInfo(id: .homeserverNotFound, + title: L10n.errorUnknown, + message: L10n.screenChangeServerErrorInvalidHomeserver) + case .invalidWellKnown(let error): + state.bindings.alertInfo = AlertInfo(id: .invalidWellKnown(error), + title: L10n.commonServerNotSupported, + message: L10n.screenChangeServerErrorInvalidWellKnown(error)) + case .slidingSync: + let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } + state.bindings.alertInfo = AlertInfo(id: .slidingSync, + title: L10n.commonServerNotSupported, + message: L10n.screenChangeServerErrorNoSlidingSyncMessage, + primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), + secondaryButton: .init(title: L10n.actionCancel, action: nil)) + case .login: + state.bindings.alertInfo = AlertInfo(id: .login, + title: L10n.commonServerNotSupported, + message: L10n.screenLoginErrorUnsupportedAuthentication) + case .registration: + state.bindings.alertInfo = AlertInfo(id: .registration, + title: L10n.commonServerNotSupported, + message: L10n.errorAccountCreationNotPossible) + case .unknownError: + state.bindings.alertInfo = AlertInfo(id: .unknownError) + } } } diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift index 78fbed4289..701e845f55 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/View/ServerConfirmationScreen.swift @@ -19,6 +19,7 @@ struct ServerConfirmationScreen: View { } .background() .backgroundStyle(.compound.bgCanvasDefault) + .alert(item: $context.alertInfo) .introspect(.window, on: .supportedVersions) { window in context.send(viewAction: .updateWindow(window)) } @@ -28,7 +29,7 @@ struct ServerConfirmationScreen: View { var header: some View { VStack(spacing: 8) { Image(systemSymbol: .personCropCircleFill) - .heroImage() + .bigIcon() .padding(.bottom, 8) Text(context.viewState.title) @@ -53,7 +54,6 @@ struct ServerConfirmationScreen: View { } .buttonStyle(.compound(.primary)) .accessibilityIdentifier(A11yIdentifiers.serverConfirmationScreen.continue) - .disabled(!context.viewState.canContinue) Button { context.send(viewAction: .changeServer) } label: { Text(L10n.screenServerConfirmationChangeServer) @@ -68,10 +68,8 @@ struct ServerConfirmationScreen: View { // MARK: - Previews struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { - static let loginViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), - authenticationFlow: .login) - static let registerViewModel = ServerConfirmationScreenViewModel(authenticationService: MockAuthenticationService(), - authenticationFlow: .register) + static let loginViewModel = makeViewModel(flow: .login) + static let registerViewModel = makeViewModel(flow: .register) static var previews: some View { NavigationStack { @@ -86,4 +84,11 @@ struct ServerConfirmationScreen_Previews: PreviewProvider, TestablePreview { } .previewDisplayName("Register") } + + static func makeViewModel(flow: AuthenticationFlow) -> ServerConfirmationScreenViewModel { + ServerConfirmationScreenViewModel(authenticationService: AuthenticationService.mock, + authenticationFlow: flow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index 6459f24a9e..889b5ef62a 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -11,9 +11,9 @@ import SwiftUI struct ServerSelectionScreenCoordinatorParameters { /// The service used to authenticate the user. let authenticationService: AuthenticationServiceProtocol + let authenticationFlow: AuthenticationFlow + let slidingSyncLearnMoreURL: URL let userIndicatorController: UserIndicatorControllerProtocol - /// Whether the screen is presented modally or within a navigation stack. - let isModallyPresented: Bool } enum ServerSelectionScreenCoordinatorAction { @@ -37,9 +37,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { init(parameters: ServerSelectionScreenCoordinatorParameters) { self.parameters = parameters - viewModel = ServerSelectionScreenViewModel(homeserverAddress: parameters.authenticationService.homeserver.value.address, - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: parameters.isModallyPresented) + viewModel = ServerSelectionScreenViewModel(authenticationService: parameters.authenticationService, + authenticationFlow: parameters.authenticationFlow, + slidingSyncLearnMoreURL: parameters.slidingSyncLearnMoreURL, + userIndicatorController: parameters.userIndicatorController) userIndicatorController = parameters.userIndicatorController } @@ -51,8 +52,8 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .confirm(let homeserverAddress): - self.useHomeserver(homeserverAddress) + case .updated: + actionsSubject.send(.updated) case .dismiss: actionsSubject.send(.dismiss) } @@ -61,54 +62,10 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { } func stop() { - stopLoading() + parameters.userIndicatorController.retractAllIndicators() } func toPresentable() -> AnyView { AnyView(ServerSelectionScreen(context: viewModel.context)) } - - // MARK: - Private - - private func startLoading(label: String = L10n.commonLoading) { - userIndicatorController.submitIndicator(UserIndicator(type: .modal, - title: label, - persistent: true)) - } - - private func stopLoading() { - userIndicatorController.retractAllIndicators() - } - - /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. - private func useHomeserver(_ homeserverAddress: String) { - startLoading() - - Task { - switch await authenticationService.configure(for: homeserverAddress) { - case .success: - MXLog.info("Selected homeserver: \(homeserverAddress)") - actionsSubject.send(.updated) - stopLoading() - case .failure(let error): - MXLog.info("Invalid homeserver: \(homeserverAddress)") - stopLoading() - handleError(error) - } - } - } - - /// Processes an error to either update the flow or display it to the user. - private func handleError(_ error: AuthenticationServiceError) { - switch error { - case .invalidServer, .invalidHomeserverAddress: - viewModel.displayError(.footerMessage(L10n.screenChangeServerErrorInvalidHomeserver)) - case .invalidWellKnown(let error): - viewModel.displayError(.invalidWellKnownAlert(error)) - case .slidingSyncNotAvailable: - viewModel.displayError(.slidingSyncAlert) - default: - viewModel.displayError(.footerMessage(L10n.errorUnknown)) - } - } } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift index 94eb3b09d8..db72a3608b 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenModels.swift @@ -8,8 +8,8 @@ import Foundation enum ServerSelectionScreenViewModelAction { - /// The user would like to use the homeserver at the given address. - case confirm(homeserverAddress: String) + /// The homeserver selection has been updated. + case updated /// Dismiss the view without using the entered address. case dismiss } @@ -22,19 +22,12 @@ struct ServerSelectionScreenViewState: BindableState { var bindings: ServerSelectionScreenBindings /// An error message to be shown in the text field footer. var footerErrorMessage: String? - /// Whether the screen is presented modally or within a navigation stack. - var isModallyPresented: Bool /// The message to show in the text field footer. var footerMessage: AttributedString { footerErrorMessage.map(AttributedString.init) ?? regularFooterMessage } - /// The title shown on the confirm button. - var buttonTitle: String { - isModallyPresented ? L10n.actionContinue : L10n.actionNext - } - /// The text field is showing an error. var isShowingFooterError: Bool { footerErrorMessage != nil @@ -45,10 +38,9 @@ struct ServerSelectionScreenViewState: BindableState { bindings.homeserverAddress.isEmpty || isShowingFooterError } - init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil, isModallyPresented: Bool) { + init(slidingSyncLearnMoreURL: URL, bindings: ServerSelectionScreenBindings, footerErrorMessage: String? = nil) { self.bindings = bindings self.footerErrorMessage = footerErrorMessage - self.isModallyPresented = isModallyPresented let linkPlaceholder = "{link}" var message = AttributedString(L10n.screenChangeServerFormNotice(linkPlaceholder)) @@ -82,4 +74,8 @@ enum ServerSelectionScreenErrorType: Hashable { case invalidWellKnownAlert(String) /// An alert that allows the user to learn about sliding sync. case slidingSyncAlert + /// An alert that informs the user that login isn't supported. + case loginAlert + /// An alert that informs the user that registration isn't supported. + case registrationAlert } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index 57d12c3dbb..a13227b0b7 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -11,7 +11,10 @@ import SwiftUI typealias ServerSelectionScreenViewModelType = StateStoreViewModel class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, ServerSelectionScreenViewModelProtocol { + private let authenticationService: AuthenticationServiceProtocol + private let authenticationFlow: AuthenticationFlow private let slidingSyncLearnMoreURL: URL + private let userIndicatorController: UserIndicatorControllerProtocol private var actionsSubject: PassthroughSubject = .init() @@ -19,19 +22,23 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server actionsSubject.eraseToAnyPublisher() } - init(homeserverAddress: String, slidingSyncLearnMoreURL: URL, isModallyPresented: Bool) { + init(authenticationService: AuthenticationServiceProtocol, + authenticationFlow: AuthenticationFlow, + slidingSyncLearnMoreURL: URL, + userIndicatorController: UserIndicatorControllerProtocol) { + self.authenticationService = authenticationService + self.authenticationFlow = authenticationFlow self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL - let bindings = ServerSelectionScreenBindings(homeserverAddress: homeserverAddress) + self.userIndicatorController = userIndicatorController - super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, - bindings: bindings, - isModallyPresented: isModallyPresented)) + let bindings = ServerSelectionScreenBindings(homeserverAddress: authenticationService.homeserver.value.address) + super.init(initialViewState: ServerSelectionScreenViewState(slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, bindings: bindings)) } override func process(viewAction: ServerSelectionScreenViewAction) { switch viewAction { case .confirm: - actionsSubject.send(.confirm(homeserverAddress: state.bindings.homeserverAddress)) + configureHomeserver() case .dismiss: actionsSubject.send(.dismiss) case .clearFooterError: @@ -39,27 +46,72 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server } } - func displayError(_ type: ServerSelectionScreenErrorType) { - switch type { - case .footerMessage(let message): - withElementAnimation { - state.footerErrorMessage = message + // MARK: - Private + + /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. + private func configureHomeserver() { + let homeserverAddress = state.bindings.homeserverAddress + startLoading() + + Task { + switch await authenticationService.configure(for: homeserverAddress, flow: authenticationFlow) { + case .success: + MXLog.info("Selected homeserver: \(homeserverAddress)") + actionsSubject.send(.updated) + stopLoading() + case .failure(let error): + MXLog.info("Invalid homeserver: \(homeserverAddress)") + stopLoading() + handleError(error) } - case .invalidWellKnownAlert(let error): - state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, + } + } + + private func startLoading(label: String = L10n.commonLoading) { + userIndicatorController.submitIndicator(UserIndicator(type: .modal, + title: label, + persistent: true)) + } + + private func stopLoading() { + userIndicatorController.retractAllIndicators() + } + + /// Processes an error to either update the flow or display it to the user. + private func handleError(_ error: AuthenticationServiceError) { + switch error { + case .invalidServer, .invalidHomeserverAddress: + showFooterMessage(L10n.screenChangeServerErrorInvalidHomeserver) + case .invalidWellKnown(let error): + state.bindings.alertInfo = AlertInfo(id: .invalidWellKnownAlert(error), title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorInvalidWellKnown(error)) - case .slidingSyncAlert: + case .slidingSyncNotAvailable: let openURL = { UIApplication.shared.open(self.slidingSyncLearnMoreURL) } state.bindings.alertInfo = AlertInfo(id: .slidingSyncAlert, title: L10n.commonServerNotSupported, message: L10n.screenChangeServerErrorNoSlidingSyncMessage, primaryButton: .init(title: L10n.actionLearnMore, role: .cancel, action: openURL), secondaryButton: .init(title: L10n.actionCancel, action: nil)) + case .loginNotSupported: + state.bindings.alertInfo = AlertInfo(id: .loginAlert, + title: L10n.commonServerNotSupported, + message: L10n.screenLoginErrorUnsupportedAuthentication) + case .registrationNotSupported: + state.bindings.alertInfo = AlertInfo(id: .registrationAlert, + title: L10n.commonServerNotSupported, + message: L10n.errorAccountCreationNotPossible) + default: + showFooterMessage(L10n.errorUnknown) } } - // MARK: - Private + /// Set a new error message to be shown in the text field footer. + private func showFooterMessage(_ message: String) { + withElementAnimation { + state.footerErrorMessage = message + } + } /// Clear any errors shown in the text field footer. private func clearFooterError() { diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift index 320c0842a6..c8f748d60d 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift @@ -11,7 +11,4 @@ import Combine protocol ServerSelectionScreenViewModelProtocol { var actions: AnyPublisher { get } var context: ServerSelectionScreenViewModelType.Context { get } - - /// Displays an error to the user. - func displayError(_ type: ServerSelectionScreenErrorType) } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift index b166aabc67..b39a92e416 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/View/ServerSelectionScreen.swift @@ -32,7 +32,7 @@ struct ServerSelectionScreen: View { var header: some View { VStack(spacing: 8) { Image(asset: Asset.Images.serverSelectionIcon) - .heroImage(insets: 19) + .bigIcon(insets: 19) .padding(.bottom, 8) Text(L10n.screenChangeServerTitle) @@ -59,12 +59,12 @@ struct ServerSelectionScreen: View { .keyboardType(.URL) .autocapitalization(.none) .disableAutocorrection(true) - .onChange(of: context.homeserverAddress) { _ in context.send(viewAction: .clearFooterError) } + .onChange(of: context.homeserverAddress) { context.send(viewAction: .clearFooterError) } .submitLabel(.done) .onSubmit(submit) Button(action: submit) { - Text(context.viewState.buttonTitle) + Text(L10n.actionContinue) } .buttonStyle(.compound(.primary)) .disabled(context.viewState.hasValidationError) @@ -72,15 +72,12 @@ struct ServerSelectionScreen: View { } } - @ToolbarContentBuilder var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { - if context.viewState.isModallyPresented { - Button { context.send(viewAction: .dismiss) } label: { - Text(L10n.actionCancel) - } - .accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss) + Button { context.send(viewAction: .dismiss) } label: { + Text(L10n.actionCancel) } + .accessibilityIdentifier(A11yIdentifiers.changeServerScreen.dismiss) } } @@ -94,11 +91,38 @@ struct ServerSelectionScreen: View { // MARK: - Previews struct ServerSelection_Previews: PreviewProvider, TestablePreview { + static let matrixViewModel = makeViewModel(for: "https://matrix.org") + static let emptyViewModel = makeViewModel(for: "") + static let invalidViewModel = makeViewModel(for: "thisisbad") + static var previews: some View { - ForEach(MockServerSelectionScreenState.allCases, id: \.self) { state in - NavigationStack { - ServerSelectionScreen(context: state.viewModel.context) - } + NavigationStack { + ServerSelectionScreen(context: matrixViewModel.context) + } + + NavigationStack { + ServerSelectionScreen(context: emptyViewModel.context) + } + + NavigationStack { + ServerSelectionScreen(context: invalidViewModel.context) + } + .snapshotPreferences(delay: 1) + } + + static func makeViewModel(for homeserverAddress: String) -> ServerSelectionScreenViewModel { + let authenticationService = AuthenticationService.mock + + let slidingSyncLearnMoreURL = ServiceLocator.shared.settings.slidingSyncLearnMoreURL + + let viewModel = ServerSelectionScreenViewModel(authenticationService: authenticationService, + authenticationFlow: .login, + slidingSyncLearnMoreURL: slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + viewModel.context.homeserverAddress = homeserverAddress + if homeserverAddress == "thisisbad" { + viewModel.context.send(viewAction: .confirm) } + return viewModel } } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift index db748aa4f6..e306d2c352 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift @@ -85,15 +85,6 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { AnyView(SoftLogoutScreen(context: viewModel.context)) } - func handleOIDCRedirectURL(_ url: URL) { - guard let oidcPresenter else { - MXLog.error("Failed to find an OIDC request in progress.") - return - } - - oidcPresenter.handleUniversalLinkCallback(url) - } - // MARK: - Private private static let loadingIndicatorIdentifier = "\(SoftLogoutScreenCoordinator.self)-Loading" diff --git a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift index 10f9b16103..a390d7aab7 100644 --- a/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift +++ b/ElementX/Sources/Screens/AuthenticationStartScreen/AuthenticationStartScreenViewModel.swift @@ -19,7 +19,7 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType init(webRegistrationEnabled: Bool) { super.init(initialViewState: AuthenticationStartScreenViewState(isWebRegistrationEnabled: webRegistrationEnabled, - isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac && AppSettings.isDevelopmentBuild)) + isQRCodeLoginEnabled: !ProcessInfo.processInfo.isiOSAppOnMac)) } override func process(viewAction: AuthenticationStartScreenViewAction) { diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift index 977ae74a15..1922e1f92b 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift @@ -55,7 +55,7 @@ struct BlockedUsersScreen: View { struct BlockedUsersScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, clientProxy: ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: UserIndicatorControllerMock()) static var previews: some View { diff --git a/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift b/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift index d972d1279f..1be9527292 100644 --- a/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift +++ b/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift @@ -30,7 +30,7 @@ struct BugReportScreen: View { .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .interactiveDismissDisabled() - .onChange(of: selectedScreenshot) { newItem in + .onChange(of: selectedScreenshot) { _, newItem in Task { guard let data = try? await newItem?.loadTransferable(type: Data.self), let image = UIImage(data: data) diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index abbc817bb6..4c41b43586 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -170,7 +170,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol return } - await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle) + await elementCallService.setupCallSession(roomID: roomProxy.id, + roomDisplayName: roomProxy.infoPublisher.value.displayName ?? roomProxy.id) _ = await roomProxy.sendCallNotificationIfNeeded() @@ -213,14 +214,20 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol } private func postMessageToWidget(_ message: ElementCallWidgetMessage) async { + let data: Data do { - let data = try JSONEncoder().encode(message) - let json = String(decoding: data, as: UTF8.self) - - await postJSONToWidget(json) + data = try JSONEncoder().encode(message) } catch { MXLog.error("Failed encoding widget message with error: \(error)") + return } + + guard let json = String(data: data, encoding: .utf8) else { + MXLog.error("Invalid data for widget message") + return + } + + await postJSONToWidget(json) } private func postJSONToWidget(_ json: String) async { diff --git a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift index 16f85e5299..8ba9f9c7da 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift @@ -65,8 +65,8 @@ struct PollFormScreen: View { } .focused($focus, equals: .option(index: index)) .accessibilityIdentifier(A11yIdentifiers.pollFormScreen.optionID(index)) - .onChange(of: context.options[index].text) { optionText in - guard let lastCharacter = optionText.last, lastCharacter.isNewline else { + .onChange(of: context.options[index].text) { _, newOptionText in + guard let lastCharacter = newOptionText.last, lastCharacter.isNewline else { return } diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift index da965f2e2f..a37163f97c 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift @@ -37,7 +37,8 @@ final class CreateRoomCoordinator: CoordinatorProtocol { createRoomParameters: parameters.createRoomParameters, selectedUsers: parameters.selectedUsers, analytics: ServiceLocator.shared.analytics, - userIndicatorController: parameters.userIndicatorController) + userIndicatorController: parameters.userIndicatorController, + appSettings: ServiceLocator.shared.settings) } func start() { diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 87ef365a15..44cefd052c 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -25,6 +25,7 @@ enum CreateRoomViewModelAction { } struct CreateRoomViewState: BindableState { + let isKnockingFeatureEnabled: Bool var selectedUsers: [UserProfileProxy] var bindings: CreateRoomViewStateBindings var avatarURL: URL? @@ -37,6 +38,7 @@ struct CreateRoomViewStateBindings { var roomName: String var roomTopic: String var isRoomPrivate: Bool + var isKnockingOnly = false var showAttachmentConfirmationDialog = false /// Information describing the currently displayed alert. diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index e8d4e2da90..da70c7afb2 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -26,7 +26,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol createRoomParameters: CurrentValuePublisher, selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>, analytics: AnalyticsService, - userIndicatorController: UserIndicatorControllerProtocol) { + userIndicatorController: UserIndicatorControllerProtocol, + appSettings: AppSettings) { let parameters = createRoomParameters.value self.userSession = userSession @@ -36,7 +37,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate) - super.init(initialViewState: CreateRoomViewState(selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) + super.init(initialViewState: CreateRoomViewState(isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) createRoomParameters .map(\.avatarImageMedia) @@ -93,20 +94,27 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } .sink { [weak self] bindings in guard let self else { return } - createRoomParameters.name = bindings.roomName - createRoomParameters.topic = bindings.roomTopic - createRoomParameters.isRoomPrivate = bindings.isRoomPrivate + updateParameters(bindings: bindings) actionsSubject.send(.updateDetails(createRoomParameters)) } .store(in: &cancellables) } + private func updateParameters(bindings: CreateRoomViewStateBindings) { + createRoomParameters.name = bindings.roomName + createRoomParameters.topic = bindings.roomTopic + createRoomParameters.isRoomPrivate = bindings.isRoomPrivate + createRoomParameters.isKnockingOnly = bindings.isKnockingOnly + } + private func createRoom() async { defer { hideLoadingIndicator() } showLoadingIndicator() + // Since the parameters are throttled, we need to make sure that the latest values are used + updateParameters(bindings: state.bindings) let avatarURL: URL? if let media = createRoomParameters.avatarImageMedia { switch await userSession.clientProxy.uploadMedia(media) { @@ -136,6 +144,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol switch await userSession.clientProxy.createRoom(name: createRoomParameters.name, topic: createRoomParameters.topic, isRoomPrivate: createRoomParameters.isRoomPrivate, + // As of right now we don't want to make private rooms with the knock rule + isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly, userIDs: state.selectedUsers.map(\.userID), avatarURL: avatarURL) { case .success(let roomId): diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 32eb00b9e1..726cf99f0e 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -22,6 +22,10 @@ struct CreateRoomScreen: View { roomSection topicSection securitySection + if context.viewState.isKnockingFeatureEnabled, + !context.isRoomPrivate { + roomAccessSection + } } .compoundList() .track(screen: .CreateRoom) @@ -151,6 +155,20 @@ struct CreateRoomScreen: View { } } + private var roomAccessSection: some View { + Section { + ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionAnyoneOptionTitle, + description: L10n.screenCreateRoomAccessSectionAnyoneOptionDescription), + kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) + ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionKnockingOptionTitle, + description: L10n.screenCreateRoomAccessSectionKnockingOptionDescription), + kind: .selection(isSelected: context.isKnockingOnly) { context.isKnockingOnly = true }) + } header: { + Text(L10n.screenCreateRoomAccessSectionHeader.uppercased()) + .compoundListSectionHeader() + } + } + private var toolbar: some ToolbarContent { ToolbarItem(placement: .confirmationAction) { Button(L10n.actionCreate) { @@ -174,7 +192,8 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { createRoomParameters: .init(parameters), selectedUsers: .init(selectedUsers), analytics: ServiceLocator.shared.analytics, - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + appSettings: ServiceLocator.shared.settings) }() static let emtpyViewModel = { @@ -184,7 +203,21 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { createRoomParameters: .init(parameters), selectedUsers: .init([]), analytics: ServiceLocator.shared.analytics, - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + appSettings: ServiceLocator.shared.settings) + }() + + static let publicRoomViewModel = { + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com")))) + let parameters = CreateRoomFlowParameters(isRoomPrivate: false) + let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] + ServiceLocator.shared.settings.knockingEnabled = true + return CreateRoomViewModel(userSession: userSession, + createRoomParameters: .init(parameters), + selectedUsers: .init([]), + analytics: ServiceLocator.shared.analytics, + userIndicatorController: UserIndicatorControllerMock(), + appSettings: ServiceLocator.shared.settings) }() static var previews: some View { @@ -196,5 +229,9 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { CreateRoomScreen(context: emtpyViewModel.context) } .previewDisplayName("Create Room without users") + NavigationStack { + CreateRoomScreen(context: publicRoomViewModel.context) + } + .previewDisplayName("Create Public Room") } } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift index 5aa2c4cd95..a56940a6dd 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift @@ -44,6 +44,8 @@ struct EmojiPickerEmojiCategoryViewData: Identifiable { return L10n.emojiPickerCategorySymbols case "flags": return L10n.emojiPickerCategoryFlags + case EmojiCategory.frequentlyUsedCategoryIdentifier: + return L10n.commonFrequentlyUsed default: MXLog.failure("Missing translation for emoji category with id \(id)") return "" diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index ce71384ca6..a50238258f 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -36,6 +36,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr state.categories = convert(emojiCategories: categories) } case let .emojiTapped(emoji: emoji): + emojiProvider.markEmojiAsFrequentlyUsed(emoji.value) actionsSubject.send(.emojiSelected(emoji: emoji.value)) case .dismiss: actionsSubject.send(.dismiss) diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index ddbbeb75be..7f139f79a1 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -55,7 +55,7 @@ struct EmojiPickerScreen: View { } .presentationDetents([.medium, .large]) .presentationDragIndicator(isSearching ? .hidden : .visible) - .onChange(of: searchString) { _ in + .onChange(of: searchString) { context.send(viewAction: .search(searchString: searchString)) } } @@ -81,7 +81,7 @@ struct EmojiPickerScreen: View { // MARK: - Previews struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()) + static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"]) @@ -91,7 +91,7 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { } struct EmojiPickerScreenSheet_Previews: PreviewProvider { - static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()) + static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { Text("Timeline view") diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift index 3c2a969208..5a6003db20 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift @@ -8,17 +8,12 @@ import Combine import SwiftUI -struct EncryptionResetPasswordScreenCoordinatorParameters { } +struct EncryptionResetPasswordScreenCoordinatorParameters { + let passwordPublisher: PassthroughSubject +} -enum EncryptionResetPasswordScreenCoordinatorAction: CustomStringConvertible { - case resetIdentity(String) - - var description: String { - switch self { - case .resetIdentity: - "resetIdentity" - } - } +enum EncryptionResetPasswordScreenCoordinatorAction { + case passwordEntered } final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { @@ -35,7 +30,7 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { init(parameters: EncryptionResetPasswordScreenCoordinatorParameters) { self.parameters = parameters - viewModel = EncryptionResetPasswordScreenViewModel() + viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: parameters.passwordPublisher) } func start() { @@ -44,8 +39,8 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .resetIdentity(let password): - self.actionsSubject.send(.resetIdentity(password)) + case .passwordEntered: + self.actionsSubject.send(.passwordEntered) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift index d228ab4037..d226d29657 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenModels.swift @@ -7,15 +7,8 @@ import Foundation -enum EncryptionResetPasswordScreenViewModelAction: CustomStringConvertible { - case resetIdentity(String) - - var description: String { - switch self { - case .resetIdentity: - "resetIdentity" - } - } +enum EncryptionResetPasswordScreenViewModelAction { + case passwordEntered } struct EncryptionResetPasswordScreenViewState: BindableState { @@ -28,5 +21,5 @@ struct EncryptionResetPasswordScreenViewStateBindings { } enum EncryptionResetPasswordScreenViewAction { - case resetIdentity + case submit } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift index ed73d1924f..fd01cfc95b 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift @@ -11,12 +11,16 @@ import SwiftUI typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModel class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewModelType, EncryptionResetPasswordScreenViewModelProtocol { + private let passwordPublisher: PassthroughSubject + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init() { + init(passwordPublisher: PassthroughSubject) { + self.passwordPublisher = passwordPublisher + super.init(initialViewState: .init(bindings: .init(password: ""))) } @@ -26,8 +30,9 @@ class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewM MXLog.info("View model: received view action: \(viewAction)") switch viewAction { - case .resetIdentity: - actionsSubject.send(.resetIdentity(state.bindings.password)) + case .submit: + passwordPublisher.send(state.bindings.password) + actionsSubject.send(.passwordEntered) } } } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift index 20b934e1ce..df14c8d134 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/View/EncryptionResetPasswordScreen.swift @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. // +import Combine import Compound import SwiftUI @@ -15,7 +16,7 @@ struct EncryptionResetPasswordScreen: View { var body: some View { FullscreenDialog { VStack(spacing: 16) { - HeroImage(icon: \.lockSolid) + BigIcon(icon: \.lockSolid) Text(L10n.screenResetEncryptionPasswordTitle) .foregroundColor(.compound.textPrimary) @@ -32,9 +33,10 @@ struct EncryptionResetPasswordScreen: View { .padding(16) } bottomContent: { Button(L10n.actionResetIdentity, role: .destructive) { - context.send(viewAction: .resetIdentity) + context.send(viewAction: .submit) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.submit) } .background() .backgroundStyle(.compound.bgCanvasDefault) @@ -58,8 +60,9 @@ struct EncryptionResetPasswordScreen: View { .focused($textFieldFocus) .submitLabel(.done) .onSubmit { - context.send(viewAction: .resetIdentity) + context.send(viewAction: .submit) } + .accessibilityIdentifier(A11yIdentifiers.encryptionResetPasswordScreen.passwordField) } } } @@ -67,7 +70,8 @@ struct EncryptionResetPasswordScreen: View { // MARK: - Previews struct EncryptionResetPasswordScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = EncryptionResetPasswordScreenViewModel() + static let passwordPublisher = PassthroughSubject() + static let viewModel = EncryptionResetPasswordScreenViewModel(passwordPublisher: passwordPublisher) static var previews: some View { NavigationStack { EncryptionResetPasswordScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift index b2f7edeeab..446a5ab606 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift @@ -9,14 +9,14 @@ import Combine import SwiftUI enum EncryptionResetScreenCoordinatorAction { - case cancel case requestOIDCAuthorisation(URL) + case requestPassword(passwordPublisher: PassthroughSubject) case resetFinished + case cancel } struct EncryptionResetScreenCoordinatorParameters { let clientProxy: ClientProxyProtocol - let navigationStackCoordinator: NavigationStackCoordinator let userIndicatorController: UserIndicatorControllerProtocol } @@ -43,10 +43,10 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .requestPassword: - presentPasswordScreen() case .requestOIDCAuthorisation(let url): self.actionsSubject.send(.requestOIDCAuthorisation(url)) + case .requestPassword(let passwordPublisher): + self.actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher)) case .resetFinished: self.actionsSubject.send(.resetFinished) case .cancel: @@ -63,23 +63,4 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView(EncryptionResetScreen(context: viewModel.context)) } - - // MARK: - Private - - private func presentPasswordScreen() { - let coordinator = EncryptionResetPasswordScreenCoordinator(parameters: .init()) - - coordinator.actionsPublisher.sink { [weak self] action in - guard let self else { return } - - switch action { - case .resetIdentity(let password): - viewModel.continueResetFlowWith(password: password) - parameters.navigationStackCoordinator.pop() - } - } - .store(in: &cancellables) - - parameters.navigationStackCoordinator.push(coordinator) - } } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift index 77ca459153..d85984d452 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift @@ -5,10 +5,11 @@ // Please see LICENSE in the repository root for full details. // +import Combine import Foundation enum EncryptionResetScreenViewModelAction { - case requestPassword + case requestPassword(passwordPublisher: PassthroughSubject) case requestOIDCAuthorisation(url: URL) case resetFinished case cancel diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift index ebdfda67bc..a81c2f112c 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift @@ -21,6 +21,7 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp } private var identityResetHandle: IdentityResetHandle? + private var passwordCancellable: AnyCancellable? init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.clientProxy = clientProxy @@ -46,12 +47,6 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp } } - func continueResetFlowWith(password: String) { - Task { - await resetWith(password: password) - } - } - func stop() { Task { await identityResetHandle?.cancel() @@ -80,7 +75,14 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp switch handle.authType() { case .uiaa: - actionsSubject.send(.requestPassword) + let passwordPublisher = PassthroughSubject() + passwordCancellable = passwordPublisher.sink { [weak self] password in + guard let self else { return } + passwordCancellable = nil + Task { await self.resetWith(password: password) } + } + + actionsSubject.send(.requestPassword(passwordPublisher: passwordPublisher)) case .oidc(let oidcInfo): guard let url = URL(string: oidcInfo.approvalUrl) else { fatalError("Invalid URL received through identity reset handle: \(oidcInfo.approvalUrl)") diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift index 2ebf141d12..92b48a87d4 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModelProtocol.swift @@ -12,6 +12,5 @@ protocol EncryptionResetScreenViewModelProtocol { var actionsPublisher: AnyPublisher { get } var context: EncryptionResetScreenViewModelType.Context { get } - func continueResetFlowWith(password: String) func stop() } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift index ab03039bcc..e79f243b1e 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift @@ -19,6 +19,7 @@ struct EncryptionResetScreen: View { context.send(viewAction: .reset) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.encryptionResetScreen.continueReset) } .background() .backgroundStyle(.compound.bgSubtleSecondary) @@ -39,7 +40,7 @@ struct EncryptionResetScreen: View { private var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.error, style: .criticalOnSecondary) + BigIcon(icon: \.error, style: .alert) .padding(.bottom, 8) Text(L10n.screenEncryptionResetTitle) @@ -70,14 +71,10 @@ struct EncryptionResetScreen: View { @ViewBuilder private func checkMarkItem(title: String, position: ListPosition, positive: Bool) -> some View { - RoundedLabelItem(title: title, listPosition: position) { - if positive { - CompoundIcon(\.check) - .foregroundColor(.compound.iconAccentPrimary) - } else { - CompoundIcon(\.close) - .foregroundColor(.compound.iconCriticalPrimary) - } + VisualListItem(title: title, position: position) { + CompoundIcon(positive ? \.check : \.info) + .foregroundColor(positive ? .compound.iconAccentPrimary : .compound.iconSecondary) + .alignmentGuide(.top) { _ in 2 } } .backgroundStyle(.compound.bgCanvasDefault) } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift index 25fbd5a2c8..f7f4a6fd00 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift @@ -49,7 +49,7 @@ struct GlobalSearchScreen: View { selectedRoom = context.viewState.rooms.first searchFieldFocus = true } - .onChange(of: context.viewState.rooms) { _ in + .onChange(of: context.viewState.rooms) { selectedRoom = context.viewState.rooms.first } .onTapGesture { @@ -206,7 +206,7 @@ private class GlobalSearchTextField: UITextField { struct GlobalSearchScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift index c298a5353a..6a9225facd 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift @@ -37,7 +37,7 @@ struct GlobalSearchScreenListRow: View { struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview { static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) static var previews: some View { List { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index d9b7a2481f..00f2476bee 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -21,6 +21,8 @@ enum HomeScreenCoordinatorAction { case presentSettingsScreen case presentFeedbackScreen case presentSecureBackupSettings + case presentRecoveryKeyScreen + case presentEncryptionResetScreen case presentStartChatScreen case presentGlobalSearch case presentRoomDirectorySearch @@ -65,6 +67,10 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentSettingsScreen) case .presentSecureBackupSettings: actionsSubject.send(.presentSecureBackupSettings) + case .presentRecoveryKeyScreen: + actionsSubject.send(.presentRecoveryKeyScreen) + case .presentEncryptionResetScreen: + actionsSubject.send(.presentEncryptionResetScreen) case .presentStartChatScreen: actionsSubject.send(.presentStartChatScreen) case .presentGlobalSearch: diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 96106f1ea9..2e58c17ae7 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -14,6 +14,8 @@ enum HomeScreenViewModelAction { case presentRoomDetails(roomIdentifier: String) case roomLeft(roomIdentifier: String) case presentSecureBackupSettings + case presentRecoveryKeyScreen + case presentEncryptionResetScreen case presentSettingsScreen case presentFeedbackScreen case presentStartChatScreen @@ -30,7 +32,9 @@ enum HomeScreenViewAction { case confirmLeaveRoom(roomIdentifier: String) case showSettings case startChat + case setupRecovery case confirmRecoveryKey + case resetEncryption case skipRecoveryKeyConfirmation case confirmSlidingSyncUpgrade case skipSlidingSyncUpgrade @@ -127,10 +131,11 @@ struct HomeScreenViewStateBindings { } struct HomeScreenRoom: Identifiable, Equatable { - enum RoomType { + enum RoomType: Equatable { case placeholder case room - case invite + case invite(inviterDetails: RoomInviterDetails?) + case knock } static let placeholderLastMessage = AttributedString("Hidden last message") @@ -143,6 +148,13 @@ struct HomeScreenRoom: Identifiable, Equatable { let type: RoomType + var inviter: RoomInviterDetails? { + if case .invite(let inviter) = type { + return inviter + } + return nil + } + let badges: Badges struct Badges: Equatable { let isDotShown: Bool @@ -164,9 +176,7 @@ struct HomeScreenRoom: Identifiable, Equatable { let lastMessage: AttributedString? let avatar: RoomAvatar - - let inviter: RoomInviterDetails? - + let canonicalAlias: String? static func placeholder() -> HomeScreenRoom { @@ -181,7 +191,6 @@ struct HomeScreenRoom: Identifiable, Equatable { timestamp: "Now", lastMessage: placeholderLastMessage, avatar: .room(id: "", name: "", avatarURL: nil), - inviter: nil, canonicalAlias: nil) } } @@ -192,17 +201,21 @@ extension HomeScreenRoom { let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages - let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread + let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.joinRequestType?.isKnock == true let isMentionShown = summary.hasUnreadMentions && !summary.isMuted let isMuteShown = summary.isMuted let isCallShown = summary.hasOngoingCall - let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) + let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.joinRequestType?.isKnock == true - let inviter = summary.inviter.map(RoomInviterDetails.init) + let type: HomeScreenRoom.RoomType = switch summary.joinRequestType { + case .invite(let inviter): .invite(inviterDetails: inviter.map(RoomInviterDetails.init)) + case .knock: .knock + case .none: .room + } self.init(id: identifier, roomID: summary.id, - type: summary.isInvite ? .invite : .room, + type: type, badges: .init(isDotShown: isDotShown, isMentionShown: isMentionShown, isMuteShown: isMuteShown, @@ -214,7 +227,6 @@ extension HomeScreenRoom { timestamp: summary.lastMessageFormattedTimestamp, lastMessage: summary.lastMessage, avatar: summary.avatar, - inviter: inviter, canonicalAlias: summary.canonicalAlias) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 7b40e72d23..c1b21cf4f2 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -58,7 +58,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol switch (securityState.verificationState, securityState.recoveryState) { case (.verified, .disabled): state.requiresExtraAccountSetup = true - state.securityBannerMode = .none + state.securityBannerMode = .show case (.verified, .incomplete): state.requiresExtraAccountSetup = true @@ -138,8 +138,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol Task { await leaveRoom(roomID: roomIdentifier) } case .showSettings: actionsSubject.send(.presentSettingsScreen) - case .confirmRecoveryKey: + case .setupRecovery: actionsSubject.send(.presentSecureBackupSettings) + case .confirmRecoveryKey: + actionsSubject.send(.presentRecoveryKeyScreen) + case .resetEncryption: + actionsSubject.send(.presentEncryptionResetScreen) case .skipRecoveryKeyConfirmation: state.securityBannerMode = .dismissed case .confirmSlidingSyncUpgrade: @@ -360,10 +364,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - if roomProxy.isPublic { + if roomProxy.infoPublisher.value.isPublic { state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .public) } else { - state.bindings.leaveRoomAlertItem = if roomProxy.joinedMembersCount > 1 { + state.bindings.leaveRoomAlertItem = if roomProxy.infoPublisher.value.joinedMembersCount > 1 { LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .private) } else { LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty) @@ -408,7 +412,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol switch await roomProxy.acceptInvitation() { case .success: actionsSubject.send(.presentRoom(roomIdentifier: roomID)) - analyticsService.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount)) + analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect, + isSpace: roomProxy.info.isSpace, + activeMemberCount: UInt(roomProxy.info.activeMembersCount)) case .failure: displayError() } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index ec65e9420a..68c7408b9c 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -66,10 +66,10 @@ struct HomeScreenContent: View { .onReceive(scrollViewAdapter.isScrolling) { _ in updateVisibleRange() } - .onChange(of: context.searchQuery) { _ in + .onChange(of: context.searchQuery) { updateVisibleRange() } - .onChange(of: context.viewState.visibleRooms) { _ in + .onChange(of: context.viewState.visibleRooms) { updateVisibleRange() // We have been seeing a lot of issues around the room list not updating properly after @@ -130,7 +130,7 @@ struct HomeScreenContent: View { if context.viewState.slidingSyncMigrationBannerMode == .show { HomeScreenSlidingSyncMigrationBanner(context: context) } else if context.viewState.securityBannerMode == .show { - HomeScreenRecoveryKeyConfirmationBanner(context: context) + HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: context.viewState.requiresExtraAccountSetup, context: context) } } .background(Color.compound.bgCanvasDefault) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift index aaf7ca0fad..2abc3a7701 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -69,7 +69,8 @@ struct HomeScreenInviteCell: View { @ViewBuilder private var inviterView: some View { - if let inviter = room.inviter, !room.isDirect { + if let inviter = room.inviter, + !room.isDirect { RoomInviterLabel(inviter: inviter, mediaProvider: context.mediaProvider) .font(.compound.bodyMD) .foregroundStyle(.compound.textPlaceholder) @@ -177,8 +178,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(roomListItem: RoomListItemSDKMock(), id: "@someone:somewhere.com", - isInvite: false, - inviter: inviter, + joinRequestType: .invite(inviter: inviter), name: "Some Guy", isDirect: true, avatarURL: nil, @@ -205,8 +205,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(roomListItem: RoomListItemSDKMock(), id: "@someone:somewhere.com", - isInvite: false, - inviter: inviter, + joinRequestType: .invite(inviter: inviter), name: "Awesome Room", isDirect: false, avatarURL: avatarURL, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift new file mode 100644 index 0000000000..a1cdf9bfb4 --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift @@ -0,0 +1,200 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import Compound +import SwiftUI + +@MainActor +struct HomeScreenKnockedCell: View { + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + let room: HomeScreenRoom + let context: HomeScreenViewModel.Context + + var body: some View { + HStack(alignment: .top, spacing: 16) { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: room.avatar, + avatarSize: .custom(52), + mediaProvider: context.mediaProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + + mainContent + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 16) + .padding(.trailing, 16) + .multilineTextAlignment(.leading) + .overlay(alignment: .bottom) { + separator + } + } + .padding(.top, 12) + .padding(.leading, 16) + .onTapGesture { + if let roomID = room.roomID { + context.send(viewAction: .selectRoom(roomIdentifier: roomID)) + } + } + } + + // MARK: - Private + + private var mainContent: some View { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline, spacing: 16) { + textualContent + badge + } + + Text(L10n.screenRoomlistKnockEventSentDescription) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPlaceholder) + .padding(.top, room.canonicalAlias == nil ? 0 : 4) + .padding(.trailing, 16) + } + .fixedSize(horizontal: false, vertical: true) + .accessibilityElement(children: .combine) + } + } + + @ViewBuilder + private var textualContent: some View { + VStack(alignment: .leading, spacing: 0) { + Text(title) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textPrimary) + .lineLimit(2) + + if let subtitle { + Text(subtitle) + .font(.compound.bodyMD) + .foregroundColor(.compound.textPlaceholder) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var separator: some View { + Rectangle() + .fill(Color.compound.borderDisabled) + .frame(height: 1 / UIScreen.main.scale) + } + + private var title: String { + room.name + } + + private var subtitle: String? { + room.canonicalAlias + } + + private var badge: some View { + Circle() + .scaledFrame(size: 12) + .foregroundColor(.compound.iconAccentTertiary) + } +} + +struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + ScrollView { + VStack(spacing: 0) { + HomeScreenKnockedCell(room: .dmInvite, + context: viewModel().context) + + HomeScreenKnockedCell(room: .dmInvite, + context: viewModel().context) + + HomeScreenKnockedCell(room: .roomKnocked(), + context: viewModel().context) + + HomeScreenKnockedCell(room: .roomKnocked(), + context: viewModel().context) + + HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .picturesDirectory), + context: viewModel().context) + + HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org"), + context: viewModel().context) + .dynamicTypeSize(.accessibility1) + .previewDisplayName("Aliased room (AX1)") + } + } + } + + static func viewModel() -> HomeScreenViewModel { + let clientProxy = ClientProxyMock(.init()) + + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + return HomeScreenViewModel(userSession: userSession, + analyticsService: ServiceLocator.shared.analytics, + appSettings: ServiceLocator.shared.settings, + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } +} + +@MainActor +private extension HomeScreenRoom { + static var dmInvite: HomeScreenRoom { + let inviter = RoomMemberProxyMock() + inviter.displayName = "Jack" + inviter.userID = "@jack:somewhere.com" + + let summary = RoomSummary(roomListItem: RoomListItemSDKMock(), + id: "@someone:somewhere.com", + joinRequestType: .invite(inviter: inviter), + name: "Some Guy", + isDirect: true, + avatarURL: nil, + heroes: [.init(userID: "@someone:somewhere.com")], + lastMessage: nil, + lastMessageFormattedTimestamp: nil, + unreadMessagesCount: 0, + unreadMentionsCount: 0, + unreadNotificationsCount: 0, + notificationMode: nil, + canonicalAlias: "#footest:somewhere.org", + hasOngoingCall: false, + isMarkedUnread: false, + isFavourite: false) + + return .init(summary: summary, hideUnreadMessagesBadge: false) + } + + static func roomKnocked(alias: String? = nil, avatarURL: URL? = nil) -> HomeScreenRoom { + let inviter = RoomMemberProxyMock() + inviter.displayName = "Luca" + inviter.userID = "@jack:somewhi.nl" + inviter.avatarURL = avatarURL + + let summary = RoomSummary(roomListItem: RoomListItemSDKMock(), + id: "@someone:somewhere.com", + joinRequestType: .invite(inviter: inviter), + name: "Awesome Room", + isDirect: false, + avatarURL: avatarURL, + heroes: [.init(userID: "@someone:somewhere.com")], + lastMessage: nil, + lastMessageFormattedTimestamp: nil, + unreadMessagesCount: 0, + unreadMentionsCount: 0, + unreadNotificationsCount: 0, + notificationMode: nil, + canonicalAlias: alias, + hasOngoingCall: false, + isMarkedUnread: false, + isFavourite: false) + + return .init(summary: summary, hideUnreadMessagesBadge: false) + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift index 12fb8023e3..1adf1fac7b 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift @@ -6,21 +6,38 @@ // import Combine +import Compound import SwiftUI struct HomeScreenRecoveryKeyConfirmationBanner: View { + let requiresExtraAccountSetup: Bool var context: HomeScreenViewModel.Context + var title: String { requiresExtraAccountSetup ? L10n.bannerSetUpRecoveryTitle : L10n.confirmRecoveryKeyBannerTitle } + var message: String { requiresExtraAccountSetup ? L10n.bannerSetUpRecoveryContent : L10n.confirmRecoveryKeyBannerMessage } + var actionTitle: String { requiresExtraAccountSetup ? L10n.bannerSetUpRecoverySubmit : L10n.confirmRecoveryKeyBannerPrimaryButtonTitle } + var primaryAction: HomeScreenViewAction { requiresExtraAccountSetup ? .setupRecovery : .confirmRecoveryKey } + var body: some View { - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 16) { - Text(L10n.confirmRecoveryKeyBannerTitle) - .font(.compound.bodyLGSemibold) - .foregroundColor(.compound.textPrimary) - - Spacer() - + VStack(spacing: 16) { + content + buttons + } + .padding(16) + .background(Color.compound.bgSubtleSecondary) + .cornerRadius(14) + .padding(.horizontal, 16) + } + + var content: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .firstTextBaseline, spacing: 16) { + Text(title) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + + if requiresExtraAccountSetup { Button { context.send(viewAction: .skipRecoveryKeyConfirmation) } label: { @@ -29,22 +46,34 @@ struct HomeScreenRecoveryKeyConfirmationBanner: View { .frame(width: 12, height: 12) } } - Text(L10n.confirmRecoveryKeyBannerMessage) - .font(.compound.bodyMD) - .foregroundColor(.compound.textSecondary) } - Button(L10n.actionContinue) { - context.send(viewAction: .confirmRecoveryKey) + Text(message) + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + } + } + + var buttons: some View { + VStack(spacing: 16) { + Button(actionTitle) { + context.send(viewAction: primaryAction) } .frame(maxWidth: .infinity) .buttonStyle(.compound(.primary, size: .medium)) .accessibilityIdentifier(A11yIdentifiers.homeScreen.recoveryKeyConfirmationBannerContinue) + + if !requiresExtraAccountSetup { + Button { + context.send(viewAction: .resetEncryption) + } label: { + Text(L10n.confirmRecoveryKeyBannerSecondaryButtonTitle) + .padding(.vertical, 7) + .frame(maxWidth: .infinity) + } + .buttonStyle(.compound(.plain, size: .medium)) + } } - .padding(16) - .background(Color.compound.bgSubtleSecondary) - .cornerRadius(14) - .padding(.horizontal, 16) } } @@ -52,7 +81,12 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab static let viewModel = buildViewModel() static var previews: some View { - HomeScreenRecoveryKeyConfirmationBanner(context: viewModel.context) + HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: true, + context: viewModel.context) + .previewDisplayName("Set up recovery") + HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: false, + context: viewModel.context) + .previewDisplayName("Out of sync") } static func buildViewModel() -> HomeScreenViewModel { diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift index 393b5c7227..fb224413a2 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift @@ -32,6 +32,8 @@ struct HomeScreenRoomList: View { .redacted(reason: .placeholder) case .invite: HomeScreenInviteCell(room: room, context: context) + case .knock: + HomeScreenKnockedCell(room: room, context: context) case .room: let isSelected = context.viewState.selectedRoomID == room.id diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift index 067eb290c7..5e75198d69 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift @@ -110,7 +110,7 @@ struct InviteUsersScreen: View { .frame(width: cellWidth) } } - .onChange(of: context.viewState.scrollToLastID) { lastAddedID in + .onChange(of: context.viewState.scrollToLastID) { _, lastAddedID in guard let id = lastAddedID else { return } withElementAnimation(.easeInOut) { scrollView.scrollTo(id) @@ -154,7 +154,7 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview { return InviteUsersScreenViewModel(clientProxy: ClientProxyMock(.init()), selectedUsers: .init([]), roomType: .draft, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock()) }() diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift index 3f970c812a..5316cd9763 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreenSelectedItem.swift @@ -50,7 +50,7 @@ struct InviteUsersScreenSelectedItem_Previews: PreviewProvider, TestablePreview ScrollView(.horizontal) { HStack(spacing: 28) { ForEach(people, id: \.userID) { user in - InviteUsersScreenSelectedItem(user: user, mediaProvider: MockMediaProvider(), dismissAction: { }) + InviteUsersScreenSelectedItem(user: user, mediaProvider: MediaProviderMock(configuration: .init()), dismissAction: { }) .frame(width: 72) } } diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift index e669b42049..d259e53e1b 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift @@ -14,6 +14,7 @@ struct JoinRoomScreenCoordinatorParameters { let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol let userIndicatorController: UserIndicatorControllerProtocol + let appSettings: AppSettings } enum JoinRoomScreenCoordinatorAction { @@ -34,6 +35,7 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol { init(parameters: JoinRoomScreenCoordinatorParameters) { viewModel = JoinRoomScreenViewModel(roomID: parameters.roomID, via: parameters.via, + appSettings: parameters.appSettings, clientProxy: parameters.clientProxy, mediaProvider: parameters.mediaProvider, userIndicatorController: parameters.userIndicatorController) @@ -47,7 +49,7 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol { switch action { case .joined: actionsSubject.send(.joined) - case .cancelled: + case .dismiss: actionsSubject.send(.cancelled) } } diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift index f2c5d4ac60..5def712723 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift @@ -9,7 +9,7 @@ import Foundation enum JoinRoomScreenViewModelAction { case joined - case cancelled + case dismiss } enum JoinRoomScreenInteractionMode { @@ -18,6 +18,7 @@ enum JoinRoomScreenInteractionMode { case invited case join case knock + case knocked } struct JoinRoomScreenRoomDetails { @@ -48,6 +49,7 @@ struct JoinRoomScreenViewState: BindableState { case .loading: nil case .unknown: L10n.screenJoinRoomSubtitleNoPreview case .invited, .join, .knock: roomDetails?.canonicalAlias + case .knocked: nil } } @@ -58,13 +60,16 @@ struct JoinRoomScreenViewState: BindableState { struct JoinRoomScreenViewStateBindings { var alertInfo: AlertInfo? + var knockMessage = "" } enum JoinRoomScreenAlertType { case declineInvite + case cancelKnock } enum JoinRoomScreenViewAction { + case cancelKnock case knock case join case acceptInvite diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift index da59f09bad..2d88bfb14e 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift @@ -13,7 +13,7 @@ typealias JoinRoomScreenViewModelType = StateStoreViewModel JoinRoomScreenViewModel { @@ -145,11 +227,25 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview { (false, false, true, false) case .knock: (false, false, false, true) + case .knocked: + (false, false, false, false) } if mode == .unknown { clientProxy.roomPreviewForIdentifierViaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) } else { + switch mode { + case .knocked: + clientProxy.roomForIdentifierClosure = { _ in + .knocked(KnockedRoomProxyMock(.init(avatarURL: URL.homeDirectory))) + } + case .invited: + clientProxy.roomForIdentifierClosure = { _ in + .invited(InvitedRoomProxyMock(.init(avatarURL: URL.homeDirectory))) + } + default: + break + } clientProxy.roomPreviewForIdentifierViaReturnValue = .success(.init(roomID: "1", name: "The Three-Body Problem - 三体", canonicalAlias: "#3🌞problem:matrix.org", @@ -164,11 +260,13 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview { canKnock: membership.canKnock)) } + ServiceLocator.shared.settings.knockingEnabled = true + return JoinRoomScreenViewModel(roomID: "1", via: [], - allowKnocking: true, + appSettings: ServiceLocator.shared.settings, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index bc46ef4dbe..d1610f673f 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -77,7 +77,6 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, private func sendAttachment(mediaInfo: MediaInfo, progressSubject: CurrentValueSubject) async -> Result { let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in - self?.requestHandle?.cancel() self?.requestHandle = handle } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index c902a1ba48..30db52e56c 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -116,7 +116,7 @@ private class PreviewItem: NSObject, QLPreviewItem { struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: UserIndicatorControllerMock.default, roomProxy: JoinedRoomProxyMock(), - mediaUploadingPreprocessor: MediaUploadingPreprocessor(), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), title: "some random file name", url: URL.picturesDirectory) static var previews: some View { diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift index cb1fec8ea1..2801f66ef2 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift @@ -93,13 +93,13 @@ private struct MessageForwardingListRow: View { struct MessageForwardingScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) - let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .init(timelineID: ""), + let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .randomEvent, roomID: "", content: .init(noPointer: .init())), clientProxy: ClientProxyMock(.init()), roomSummaryProvider: summaryProvider, userIndicatorController: UserIndicatorControllerMock(), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) NavigationStack { MessageForwardingScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift index c616acc34f..a63f4ba6ef 100644 --- a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift @@ -35,7 +35,7 @@ struct AnalyticsPromptScreen: View { private var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.chart) + BigIcon(icon: \.chart) .padding(.bottom, 8) Text(L10n.screenAnalyticsPromptTitle(InfoPlistReader.main.bundleDisplayName)) @@ -65,7 +65,7 @@ struct AnalyticsPromptScreen: View { @ViewBuilder private func checkMarkItem(title: String, position: ListPosition) -> some View { - RoundedLabelItem(title: title, listPosition: position) { + VisualListItem(title: title, position: position) { CompoundIcon(\.checkCircle, size: .small, relativeTo: .body) .foregroundColor(.compound.iconAccentPrimary) } diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift index 0a4120dae4..d81d14944c 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift @@ -37,7 +37,7 @@ struct IdentityConfirmationScreen: View { @ViewBuilder private var screenHeader: some View { VStack(spacing: 0) { - HeroImage(icon: \.lockSolid) + BigIcon(icon: \.lockSolid) .padding(.bottom, 16) Text(L10n.screenIdentityConfirmationTitle) @@ -67,32 +67,27 @@ struct IdentityConfirmationScreen: View { context.send(viewAction: .otherDevice) } .buttonStyle(.compound(.primary)) - - if context.viewState.availableActions.contains(.recovery) { - Button(L10n.screenIdentityConfirmationUseRecoveryKey) { - context.send(viewAction: .recoveryKey) - } - .buttonStyle(.compound(.secondary)) - } - } else if context.viewState.availableActions.contains(.recovery) { + } + + if context.viewState.availableActions.contains(.recovery) { Button(L10n.screenIdentityConfirmationUseRecoveryKey) { context.send(viewAction: .recoveryKey) } .buttonStyle(.compound(.primary)) } + Button(L10n.screenIdentityConfirmationCannotConfirm) { + context.send(viewAction: .reset) + } + .buttonStyle(.compound(.secondary)) + if shouldShowSkipButton { - Button(L10n.actionSkip) { + Button("\(L10n.actionSkip) 🙉") { context.send(viewAction: .skip) } .buttonStyle(.compound(.plain)) + .padding(.vertical, 14) } - - Button(L10n.screenIdentityConfirmationCannotConfirm) { - context.send(viewAction: .reset) - } - .buttonStyle(.compound(.plain)) - .padding(.vertical, 14) } } @@ -113,7 +108,7 @@ struct IdentityConfirmationScreen_Previews: PreviewProvider, TestablePreview { NavigationStack { IdentityConfirmationScreen(context: viewModel.context) } - .snapshotPreferences(delay: 0.25) + .snapshotPreferences(delay: 1) } private static var viewModel: IdentityConfirmationScreenViewModel { diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift index fc8b705727..aa7bd0a1de 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift @@ -29,7 +29,7 @@ struct IdentityConfirmedScreen: View { @ViewBuilder private var screenHeader: some View { VStack(spacing: 0) { - HeroImage(icon: \.checkCircle, style: .success) + BigIcon(icon: \.checkCircle, style: .successSolid) .padding(.bottom, 16) Text(L10n.screenIdentityConfirmedTitle) diff --git a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift index 8d042091a2..d3b4872fec 100644 --- a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift @@ -27,7 +27,7 @@ struct NotificationPermissionsScreen: View { /// The main content of the screen that is shown inside the scroll view. private var mainContent: some View { VStack(spacing: 8) { - HeroImage(icon: \.notificationsSolid) + BigIcon(icon: \.notificationsSolid) .padding(.bottom, 8) Text(L10n.screenNotificationOptinTitle) diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift index adbd3adb54..9741f19709 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift @@ -6,14 +6,21 @@ // import Combine +import MatrixRustSDK import SwiftUI enum SessionVerificationScreenCoordinatorAction { case done } +enum SessionVerificationScreenFlow { + case initiator + case responder(details: SessionVerificationRequestDetails) +} + struct SessionVerificationScreenCoordinatorParameters { let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol + let flow: SessionVerificationScreenFlow } final class SessionVerificationScreenCoordinator: CoordinatorProtocol { @@ -27,7 +34,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol { } init(parameters: SessionVerificationScreenCoordinatorParameters) { - viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy) + viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy, + flow: parameters.flow) } // MARK: - Public diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift index 4046675952..ec7d72b44f 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift @@ -11,13 +11,61 @@ enum SessionVerificationScreenViewModelAction { case finished } +enum SessionVerificationScreenViewAction { + case acceptVerificationRequest + case ignoreVerificationRequest + case requestVerification + case startSasVerification + case restart + case accept + case decline + case done +} + struct SessionVerificationScreenViewState: BindableState { - var verificationState: SessionVerificationScreenStateMachine.State = .initial + let flow: SessionVerificationScreenFlow + var verificationState: SessionVerificationScreenStateMachine.State + + var headerImageName: String { + switch verificationState { + case .initial: + return "lock" + case .acceptingVerificationRequest: + return "hourglass" + case .requestingVerification: + return "hourglass" + case .verificationRequestAccepted: + return "face.smiling" + case .startingSasVerification: + return "hourglass" + case .sasVerificationStarted: + return "hourglass" + case .cancelling: + return "hourglass" + case .acceptingChallenge: + return "hourglass" + case .decliningChallenge: + return "hourglass" + case .showingChallenge: + return "face.smiling" + case .verified: + return "checkmark.shield" + case .cancelled: + return "exclamationmark.shield" + } + } var title: String? { switch verificationState { case .initial: - return L10n.screenSessionVerificationOpenExistingSessionTitle + switch flow { + case .initiator: + return L10n.screenSessionVerificationOpenExistingSessionTitle + case .responder: + return L10n.screenSessionVerificationRequestTitle + } + case .acceptingVerificationRequest: + return L10n.screenSessionVerificationRequestTitle case .requestingVerification: return L10n.screenSessionVerificationWaitingToAcceptTitle case .verificationRequestAccepted: @@ -31,13 +79,13 @@ struct SessionVerificationScreenViewState: BindableState { case .acceptingChallenge: return L10n.screenSessionVerificationCompareEmojisTitle case .decliningChallenge: - return nil + return L10n.screenSessionVerificationCompareEmojisTitle case .verified: return L10n.commonVerificationComplete case .cancelling: return nil case .cancelled: - return L10n.commonVerificationCancelled + return L10n.commonVerificationFailed } } @@ -48,7 +96,14 @@ struct SessionVerificationScreenViewState: BindableState { var message: String { switch verificationState { case .initial: - return L10n.screenSessionVerificationOpenExistingSessionSubtitle + switch flow { + case .initiator: + return L10n.screenSessionVerificationOpenExistingSessionSubtitle + case .responder: + return L10n.screenSessionVerificationRequestSubtitle + } + case .acceptingVerificationRequest: + return L10n.screenSessionVerificationRequestSubtitle case .requestingVerification: return L10n.screenSessionVerificationWaitingToAcceptSubtitle case .verificationRequestAccepted: @@ -60,7 +115,7 @@ struct SessionVerificationScreenViewState: BindableState { case .acceptingChallenge: return L10n.screenSessionVerificationCompareEmojisSubtitle case .decliningChallenge: - return L10n.commonWaiting + return L10n.screenSessionVerificationCompareEmojisSubtitle case .cancelling: return L10n.commonWaiting case .showingChallenge: @@ -68,15 +123,7 @@ struct SessionVerificationScreenViewState: BindableState { case .verified: return L10n.screenSessionVerificationCompleteSubtitle case .cancelled: - return L10n.screenSessionVerificationCancelledSubtitle + return L10n.screenSessionVerificationFailedSubtitle } } } - -enum SessionVerificationScreenViewAction { - case requestVerification - case startSasVerification - case restart - case accept - case decline -} diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift index 99445d097f..b68404e969 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift @@ -13,6 +13,8 @@ class SessionVerificationScreenStateMachine { enum State: StateType { /// The initial state, before verification started case initial + /// Accepting the remote verification request + case acceptingVerificationRequest /// Waiting for verification acceptance case requestingVerification /// Verification request accepted. Waiting for start @@ -37,6 +39,8 @@ class SessionVerificationScreenStateMachine { /// Events that can be triggered on the SessionVerification state machine enum Event: EventType { + /// Accept the remote verification request + case acceptVerificationRequest /// Request verification case requestVerification /// The current verification request has been accepted @@ -69,16 +73,23 @@ class SessionVerificationScreenStateMachine { stateMachine.state } - init() { - stateMachine = StateMachine(state: .initial) + init(state: State) { + stateMachine = StateMachine(state: state) configure() } private func configure() { + stateMachine.addRoutes(event: .acceptVerificationRequest, transitions: [.initial => .acceptingVerificationRequest]) stateMachine.addRoutes(event: .requestVerification, transitions: [.initial => .requestingVerification]) - stateMachine.addRoutes(event: .didAcceptVerificationRequest, transitions: [.requestingVerification => .verificationRequestAccepted]) + + stateMachine.addRoutes(event: .didAcceptVerificationRequest, transitions: [.acceptingVerificationRequest => .verificationRequestAccepted, + .requestingVerification => .verificationRequestAccepted]) + stateMachine.addRoutes(event: .startSasVerification, transitions: [.verificationRequestAccepted => .startingSasVerification]) - stateMachine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial]) + + stateMachine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial, + .acceptingVerificationRequest => .initial]) + stateMachine.addRoutes(event: .restart, transitions: [.cancelled => .initial]) // Transitions with associated values need to be handled through `addRouteMapping` diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift index b1a57333e8..7264b00414 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift @@ -12,6 +12,7 @@ typealias SessionVerificationViewModelType = StateStoreViewModel some View { + static func sessionVerificationScreen(state: SessionVerificationScreenStateMachine.State, + flow: SessionVerificationScreenFlow = .initiator) -> some View { let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(), + flow: flow, verificationState: state) return SessionVerificationScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 850f7026eb..3c7cf46e0f 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -15,6 +15,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters { let mediaPlayerProvider: MediaPlayerProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol let appMediator: AppMediatorProtocol + let emojiProvider: EmojiProviderProtocol } enum PinnedEventsTimelineScreenCoordinatorAction { @@ -49,7 +50,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: parameters.appMediator, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: parameters.emojiProvider) } func start() { diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 8e7f047115..8a18ec2744 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -37,7 +37,8 @@ struct PinnedEventsTimelineScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { TimelineItemMenu(item: info.item, actions: actions) @@ -50,7 +51,7 @@ struct PinnedEventsTimelineScreen: View { private var content: some View { if timelineContext.viewState.pinnedEventIDs.isEmpty { VStack(spacing: 16) { - HeroImage(icon: \.pin, style: .normal) + BigIcon(icon: \.pin) Text(L10n.screenPinnedTimelineEmptyStateHeadline) .font(.compound.headingSMSemibold) .foregroundStyle(.compound.textPrimary) @@ -90,13 +91,14 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { timelineController.timelineItems = [] return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: UserIndicatorControllerMock(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift index a0a9a0b664..13d9184fcb 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift @@ -41,12 +41,19 @@ struct QRCodeLoginScreen: View { FullscreenDialog { VStack(alignment: .leading, spacing: 40) { VStack(spacing: 16) { - HeroImage(icon: \.computer, style: .subtle) + BigIcon(icon: \.computer, style: .default) - Text(L10n.screenQrCodeLoginInitialStateTitle(InfoPlistReader.main.productionAppName)) - .foregroundColor(.compound.textPrimary) - .font(.compound.headingMDBold) - .multilineTextAlignment(.center) + VStack(spacing: 8) { + Text(L10n.screenQrCodeLoginInitialStateTitle(InfoPlistReader.main.productionAppName)) + .foregroundColor(.compound.textPrimary) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + + Text(L10n.screenQrCodeLoginInitialStateSubtitle) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textSecondary) + } } .padding(.horizontal, 24) @@ -94,7 +101,7 @@ struct QRCodeLoginScreen: View { VStack(spacing: 16) { switch state { case .deviceCode: - HeroImage(icon: \.computer, style: .subtle) + BigIcon(icon: \.computer, style: .default) VStack(spacing: 8) { Text(L10n.screenQrCodeLoginDeviceCodeTitle) @@ -108,7 +115,7 @@ struct QRCodeLoginScreen: View { .multilineTextAlignment(.center) } case .verificationCode: - HeroImage(icon: \.lock, style: .subtle) + BigIcon(icon: \.lock, style: .default) VStack(spacing: 8) { Text(L10n.screenQrCodeLoginVerifyCodeTitle) @@ -129,7 +136,7 @@ struct QRCodeLoginScreen: View { FullscreenDialog { VStack(spacing: 40) { VStack(spacing: 16) { - HeroImage(icon: \.takePhotoSolid, style: .subtle) + BigIcon(icon: \.takePhotoSolid, style: .default) Text(L10n.screenQrCodeLoginScanningStateTitle) .foregroundColor(.compound.textPrimary) @@ -249,7 +256,7 @@ struct QRCodeLoginScreen: View { switch errorState { case .noCameraPermission: VStack(spacing: 16) { - HeroImage(icon: \.takePhotoSolid, style: .subtle) + BigIcon(icon: \.takePhotoSolid, style: .default) VStack(spacing: 8) { Text(L10n.screenQrCodeLoginNoCameraPermissionStateTitle) @@ -266,7 +273,7 @@ struct QRCodeLoginScreen: View { case .connectionNotSecure: VStack(spacing: 40) { VStack(spacing: 16) { - HeroImage(icon: \.error, style: .criticalOnSecondary) + BigIcon(icon: \.error, style: .alert) VStack(spacing: 8) { Text(L10n.screenQrCodeLoginConnectionNoteSecureStateTitle) @@ -332,7 +339,7 @@ struct QRCodeLoginScreen: View { } VStack(spacing: 16) { - HeroImage(icon: \.error, style: .criticalOnSecondary) + BigIcon(icon: \.error, style: .alert) VStack(spacing: 8) { Text(title) diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift index 2d2df80a60..b0ef698b15 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -38,7 +38,7 @@ struct ResolveVerifiedUserSendFailureScreen: View { var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.error, style: .critical) + BigIcon(icon: \.error, style: .alertSolid) .padding(.bottom, 8) Text(context.viewState.title) @@ -94,7 +94,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, - itemID: .random, + itemID: .randomEvent, roomProxy: JoinedRoomProxyMock(.init()), userIndicatorController: UserIndicatorControllerMock()) } @@ -102,7 +102,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider { static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]), - itemID: .random, + itemID: .randomEvent, roomProxy: JoinedRoomProxyMock(.init()), userIndicatorController: UserIndicatorControllerMock()) diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index 984ce6cfea..d0b12c8f56 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -143,7 +143,8 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh let demotingUpdates = state.membersToDemote.map { ($0.id, Int64(0)) } // A task we can await until the room's info gets modified with the new power levels. - let infoTask = Task { await roomProxy.actionsPublisher.values.first { $0 == .roomInfoUpdate } } + // Note: Ignore the first value as the publisher is backed by a current value subject. + let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } } switch await roomProxy.updatePowerLevelsForUsers(promotingUpdates + demotingUpdates) { case .success: diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift index 54c328e73f..d116dc4c58 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift @@ -71,8 +71,8 @@ struct RoomChangeRolesScreen: View { .frame(width: cellWidth) } } - .onChange(of: context.viewState.lastPromotedMember) { member in - guard let member else { return } + .onChange(of: context.viewState.lastPromotedMember) { _, newValue in + guard let member = newValue else { return } withElementAnimation(.easeInOut) { scrollView.scrollTo(member.id) } @@ -122,7 +122,7 @@ struct RoomChangeRolesScreen_Previews: PreviewProvider, TestablePreview { static func makeViewModel(mode: RoomMemberDetails.Role) -> RoomChangeRolesScreenViewModel { RoomChangeRolesScreenViewModel(mode: mode, roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: UserIndicatorControllerMock(), analytics: ServiceLocator.shared.analytics) } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenRow.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenRow.swift index d9cec3331e..e0db0f7510 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenRow.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenRow.swift @@ -39,34 +39,34 @@ struct RoomChangeRolesScreenRow_Previews: PreviewProvider, TestablePreview { static var previews: some View { Form { RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockAlice), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: true, action: action) RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockBob), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: false, action: action) RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockInvited), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: false, action: action) RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockCharlie), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: true, action: action) .disabled(true) RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@someone:matrix.org", membership: .join))), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: false, action: action) .disabled(true) RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@someone:matrix.org", membership: .join))), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), isSelected: false, action: action) } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift index dbbe1d5d5b..435fd33a97 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift @@ -60,7 +60,7 @@ struct RoomChangeRolesScreenSelectedItem_Previews: PreviewProvider, TestablePrev HStack(spacing: 12) { ForEach(members, id: \.id) { member in RoomChangeRolesScreenSelectedItem(member: member, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), dismissAction: { }) .frame(width: 72) } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift index 0bec89382e..9d4dffcf52 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift @@ -11,6 +11,7 @@ import SwiftUI struct RoomDetailsEditScreenCoordinatorParameters { let roomProxy: JoinedRoomProxyProtocol let mediaProvider: MediaProviderProtocol + let mediaUploadingPreprocessor: MediaUploadingPreprocessor weak var navigationStackCoordinator: NavigationStackCoordinator? let userIndicatorController: UserIndicatorControllerProtocol let orientationManager: OrientationManagerProtocol @@ -35,6 +36,7 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol { viewModel = RoomDetailsEditScreenViewModel(roomProxy: parameters.roomProxy, mediaProvider: parameters.mediaProvider, + mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, userIndicatorController: parameters.userIndicatorController) } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index 26fa1f0a26..b750cf7280 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -14,7 +14,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe private let actionsSubject: PassthroughSubject = .init() private let roomProxy: JoinedRoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol - private let mediaPreprocessor: MediaUploadingPreprocessor = .init() + private let mediaUploadingPreprocessor: MediaUploadingPreprocessor var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -22,13 +22,15 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe init(roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol, + mediaUploadingPreprocessor: MediaUploadingPreprocessor, userIndicatorController: UserIndicatorControllerProtocol) { self.roomProxy = roomProxy + self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.userIndicatorController = userIndicatorController - let roomAvatar = roomProxy.avatarURL - let roomName = roomProxy.name - let roomTopic = roomProxy.topic + let roomAvatar = roomProxy.infoPublisher.value.avatarURL + let roomName = roomProxy.infoPublisher.value.displayName + let roomTopic = roomProxy.infoPublisher.value.topic super.init(initialViewState: RoomDetailsEditScreenViewState(roomID: roomProxy.id, initialAvatarURL: roomAvatar, @@ -76,7 +78,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe title: L10n.commonLoading, persistent: true)) - let mediaResult = await mediaPreprocessor.processMedia(at: url) + let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url) switch mediaResult { case .success(.image): diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift index db4a1c6abc..9705d0ce3b 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift @@ -153,7 +153,8 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview { members: [.mockMeAdmin])) return RoomDetailsEditScreenViewModel(roomProxy: roomProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), userIndicatorController: UserIndicatorControllerMock.default) }() @@ -163,7 +164,8 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview { members: [.mockAlice])) return RoomDetailsEditScreenViewModel(roomProxy: roomProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), userIndicatorController: UserIndicatorControllerMock.default) }() diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 77a328956f..e1854f8a41 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -44,7 +44,6 @@ struct RoomDetailsScreenViewState: BindableState { var canEditRolesOrPermissions = false var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading var canJoinCall = false - var isPinningEnabled = false var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading var canEdit: Bool { @@ -183,7 +182,7 @@ enum RoomDetailsScreenViewAction { case unignoreConfirmed case processTapNotifications case processToggleMuteNotifications - case displayAvatar + case displayAvatar(URL) case processTapPolls case toggleFavourite(isFavourite: Bool) case processTapRolesAndPermissions diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 79e83484e2..d56498ef80 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -63,25 +63,20 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr self.attributedStringBuilder = attributedStringBuilder self.appSettings = appSettings - let topic = attributedStringBuilder.fromPlain(roomProxy.topic) + let topic = attributedStringBuilder.fromPlain(roomProxy.infoPublisher.value.topic) super.init(initialViewState: .init(details: roomProxy.details, isEncrypted: roomProxy.isEncrypted, - isDirect: roomProxy.isDirect, + isDirect: roomProxy.infoPublisher.value.isDirect, topic: topic, topicSummary: topic?.unattributedStringByReplacingNewlinesWithSpaces(), - joinedMembersCount: roomProxy.joinedMembersCount, + joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, notificationSettingsState: .loading, bindings: .init()), mediaProvider: mediaProvider) - appSettings.$pinningEnabled - .weakAssign(to: \.state.isPinningEnabled, on: self) - .store(in: &cancellables) - - appSettings.$pinningEnabled - .combineLatest(appMediator.networkMonitor.reachabilityPublisher) - .filter { $0.0 && $0.1 == .reachable } + appMediator.networkMonitor.reachabilityPublisher + .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.setupPinnedEventsTimelineProviderIfNeeded() @@ -101,7 +96,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } - updateRoomInfo() + updateRoomInfo(roomProxy.infoPublisher.value) Task { await updatePowerLevelPermissions() } setupRoomSubscription() @@ -129,7 +124,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty) return } - state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: roomProxy.isPublic ? .public : .private) + state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, + isDM: roomProxy.isEncryptedOneToOneRoom, + state: roomProxy.infoPublisher.value.isPublic ? .public : .private) case .confirmLeave: Task { await leaveRoom() } case .processTapIgnore: @@ -150,8 +147,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } case .processToggleMuteNotifications: Task { await toggleMuteNotifications() } - case .displayAvatar: - displayFullScreenAvatar() + case .displayAvatar(let url): + displayFullScreenAvatar(url) case .processTapPolls: actionsSubject.send(.requestPollsHistoryPresentation) case .toggleFavourite(let isFavourite): @@ -167,29 +164,25 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } // MARK: - Private - + private func setupRoomSubscription() { - roomProxy.actionsPublisher - .filter { $0 == .roomInfoUpdate } + roomProxy.infoPublisher .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] _ in - self?.updateRoomInfo() + .sink { [weak self] roomInfo in + self?.updateRoomInfo(roomInfo) Task { await self?.updatePowerLevelPermissions() } } .store(in: &cancellables) } - private func updateRoomInfo() { + private func updateRoomInfo(_ roomInfo: RoomInfoProxy) { state.details = roomProxy.details - let topic = attributedStringBuilder.fromPlain(roomProxy.topic) + let topic = attributedStringBuilder.fromPlain(roomInfo.topic) state.topic = topic state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces() - state.joinedMembersCount = roomProxy.joinedMembersCount - - Task { - state.bindings.isFavourite = await roomProxy.isFavourite - } + state.joinedMembersCount = roomInfo.joinedMembersCount + state.bindings.isFavourite = roomInfo.isFavourite } private func fetchMembersIfNeeded() async { @@ -245,7 +238,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr do { let notificationMode = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id, isEncrypted: roomProxy.isEncrypted, - isOneToOne: roomProxy.activeMembersCount == 2) + isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2) state.notificationSettingsState = .loaded(settings: notificationMode) } catch { state.notificationSettingsState = .error @@ -263,7 +256,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr do { try await notificationSettingsProxy.unmuteRoom(roomId: roomProxy.id, isEncrypted: roomProxy.isEncrypted, - isOneToOne: roomProxy.activeMembersCount == 2) + isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2) } catch { state.bindings.alertInfo = AlertInfo(id: .alert, title: L10n.commonError, @@ -346,11 +339,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } - private func displayFullScreenAvatar() { - guard let avatarURL = roomProxy.avatarURL else { - return - } - + private func displayFullScreenAvatar(_ url: URL) { let loadingIndicatorIdentifier = "roomAvatarLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) @@ -360,8 +349,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { - state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.roomTitle) + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { + state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.infoPublisher.value.displayName) } } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 73a8446f98..207f10796c 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -65,8 +65,8 @@ struct RoomDetailsScreen: View { private var normalRoomHeaderSection: some View { AvatarHeaderView(room: context.viewState.details, avatarSize: .room(on: .details), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { if !context.viewState.shortcuts.isEmpty { headerSectionShortcuts @@ -78,8 +78,8 @@ struct RoomDetailsScreen: View { private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View { AvatarHeaderView(accountOwner: accountOwner, dmRecipient: recipient, - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { if !context.viewState.shortcuts.isEmpty { headerSectionShortcuts @@ -185,19 +185,17 @@ struct RoomDetailsScreen: View { ListRow(label: .default(title: L10n.commonFavourite, icon: \.favourite), kind: .toggle($context.isFavourite)) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.favourite) - .onChange(of: context.isFavourite) { newValue in + .onChange(of: context.isFavourite) { _, newValue in context.send(viewAction: .toggleFavourite(isFavourite: newValue)) } - if context.viewState.isPinningEnabled { - ListRow(label: .default(title: L10n.screenRoomDetailsPinnedEventsRowTitle, - icon: \.pin), - details: context.viewState.pinnedEventsActionState.isLoading ? .isWaiting(true) : .title(context.viewState.pinnedEventsActionState.count), - kind: context.viewState.pinnedEventsActionState.isLoading ? .label : .navigationLink(action: { - context.send(viewAction: .processTapPinnedEvents) - })) - .disabled(context.viewState.pinnedEventsActionState.isLoading) - } + ListRow(label: .default(title: L10n.screenRoomDetailsPinnedEventsRowTitle, + icon: \.pin), + details: context.viewState.pinnedEventsActionState.isLoading ? .isWaiting(true) : .title(context.viewState.pinnedEventsActionState.count), + kind: context.viewState.pinnedEventsActionState.isLoading ? .label : .navigationLink(action: { + context.send(viewAction: .processTapPinnedEvents) + })) + .disabled(context.viewState.pinnedEventsActionState.isLoading) if context.viewState.canEditRolesOrPermissions, context.viewState.dmRecipient == nil { ListRow(label: .default(title: L10n.screenRoomDetailsRolesAndPermissions, @@ -320,18 +318,16 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration() notificationSettingsProxyMockConfiguration.roomMode.isDefault = false let notificationSettingsProxy = NotificationSettingsProxyMock(with: notificationSettingsProxyMockConfiguration) - let appSettings = AppSettings() - appSettings.pinningEnabled = true return RoomDetailsScreenViewModel(roomProxy: roomProxy, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appMediator: AppMediatorMock.default, - appSettings: appSettings) + appSettings: ServiceLocator.shared.settings) }() static let dmRoomViewModel = { @@ -348,18 +344,16 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { canonicalAlias: "#alias:domain.com", members: members)) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) - let appSettings = AppSettings() - appSettings.pinningEnabled = true return RoomDetailsScreenViewModel(roomProxy: roomProxy, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appMediator: AppMediatorMock.default, - appSettings: appSettings) + appSettings: ServiceLocator.shared.settings) }() static let simpleRoomViewModel = { @@ -375,18 +369,16 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { isEncrypted: false, members: members)) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) - let appSettings = AppSettings() - appSettings.pinningEnabled = true return RoomDetailsScreenViewModel(roomProxy: roomProxy, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxy, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appMediator: AppMediatorMock.default, - appSettings: appSettings) + appSettings: ServiceLocator.shared.settings) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift index fab1dd568b..a1c423554a 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift @@ -55,7 +55,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: "Test title", avatarURL: nil), canBeJoined: true), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_2:matrix.org", alias: "#test:example.com", @@ -65,7 +65,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: nil, avatarURL: nil), canBeJoined: true), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_3:example.com", alias: "#test_no_topic:example.com", @@ -75,7 +75,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: "Test title no topic", avatarURL: nil), canBeJoined: true), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_4:example.com", alias: "#test_no_topic:example.com", @@ -85,7 +85,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: nil, avatarURL: nil), canBeJoined: true), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_5:example.com", alias: nil, @@ -95,7 +95,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: "Test title no alias", avatarURL: nil), canBeJoined: false), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_6:example.com", alias: nil, @@ -105,7 +105,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: "Test title no alias", avatarURL: nil), canBeJoined: false), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_7:example.com", alias: nil, @@ -115,7 +115,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: nil, avatarURL: nil), canBeJoined: false), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } RoomDirectorySearchCell(result: .init(id: "!test_id_8:example.com", alias: nil, name: nil, @@ -124,7 +124,7 @@ struct RoomDirectorySearchCell_Previews: PreviewProvider, TestablePreview { name: nil, avatarURL: nil), canBeJoined: false), - mediaProvider: MockMediaProvider()) { } + mediaProvider: MediaProviderMock(configuration: .init())) { } } .compoundList() } diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift index 779a312cf7..95a67e0305 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift @@ -93,7 +93,7 @@ struct RoomDirectorySearchScreen_Previews: PreviewProvider, TestablePreview { return RoomDirectorySearchScreenViewModel(clientProxy: clientProxy, userIndicatorController: UserIndicatorControllerMock(), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index fb444c2a10..ad319e9212 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -16,11 +16,20 @@ enum RoomMemberDetailsScreenViewModelAction { struct RoomMemberDetailsScreenViewState: BindableState { let userID: String var memberDetails: RoomMemberDetails? + var isVerified: Bool? var isOwnMemberDetails = false var isProcessingIgnoreRequest = false var dmRoomID: String? var bindings: RoomMemberDetailsScreenViewStateBindings + + var showVerifiedBadge: Bool { + isVerified == true // We purposely show the badge on your own account for consistency with Web. + } + + var showVerificationSection: Bool { + isVerified == false && !isOwnMemberDetails + } } struct RoomMemberDetailsScreenViewStateBindings { @@ -74,7 +83,7 @@ enum RoomMemberDetailsScreenViewAction { case showIgnoreAlert case ignoreConfirmed case unignoreConfirmed - case displayAvatar + case displayAvatar(URL) case openDirectChat case startCall(roomID: String) } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 6249d7da6c..178183a853 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -43,25 +43,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro showMemberLoadingIndicator() Task { - defer { - hideMemberLoadingIndicator() - } - - switch await roomProxy.getMember(userID: userID) { - case .success(let member): - roomMemberProxy = member - state.memberDetails = RoomMemberDetails(withProxy: member) - state.isOwnMemberDetails = member.userID == roomProxy.ownUserID - switch await clientProxy.directRoomForUserID(member.userID) { - case .success(let roomID): - state.dmRoomID = roomID - case .failure: - break - } - case .failure(let error): - MXLog.warning("Failed to find member: \(error)") - actionsSubject.send(.openUserProfile) - } + await loadMember() + hideMemberLoadingIndicator() } } @@ -84,8 +67,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro Task { await ignoreUser() } case .unignoreConfirmed: Task { await unignoreUser() } - case .displayAvatar: - Task { await displayFullScreenAvatar() } + case .displayAvatar(let url): + Task { await displayFullScreenAvatar(url) } case .openDirectChat: Task { await openDirectChat() } case .startCall(let roomID): @@ -94,8 +77,37 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro } // MARK: - Private - - @MainActor + + private func loadMember() async { + async let memberResult = roomProxy.getMember(userID: state.userID) + async let identityResult = clientProxy.userIdentity(for: state.userID) + + switch await memberResult { + case .success(let member): + roomMemberProxy = member + state.memberDetails = RoomMemberDetails(withProxy: member) + state.isOwnMemberDetails = member.userID == roomProxy.ownUserID + switch await clientProxy.directRoomForUserID(member.userID) { + case .success(let roomID): + state.dmRoomID = roomID + case .failure: + break + } + case .failure(let error): + MXLog.warning("Failed to find member: \(error)") + // As we didn't find a member with the specified user ID in this room we instead + // fall back to showing a generic user profile screen as the source is likely + // a message containing a permalink to someone who's not in this room. + actionsSubject.send(.openUserProfile) + } + + if case let .success(.some(identity)) = await identityResult { + state.isVerified = identity.isVerified() + } else { + MXLog.error("Failed to find the member's identity.") + } + } + private func ignoreUser() async { guard let roomMemberProxy else { fatalError() @@ -143,21 +155,17 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro } } - private func displayFullScreenAvatar() async { + private func displayFullScreenAvatar(_ url: URL) async { guard let roomMemberProxy else { fatalError() } - guard let avatarURL = roomMemberProxy.avatarURL else { - return - } - let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName) } } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index 1861cd1deb..c075c5618f 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -15,6 +15,8 @@ struct RoomMemberDetailsScreen: View { Form { headerSection + verificationSection + if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails { blockUserSection } @@ -30,6 +32,25 @@ struct RoomMemberDetailsScreen: View { // MARK: - Private @ViewBuilder + private var headerSection: some View { + if let memberDetails = context.viewState.memberDetails { + AvatarHeaderView(member: memberDetails, + isVerified: context.viewState.showVerifiedBadge, + avatarSize: .user(on: .memberDetails), + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) + } footer: { + otherUserFooter + } + } else { + AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), + isVerified: context.viewState.showVerifiedBadge, + avatarSize: .user(on: .memberDetails), + mediaProvider: context.mediaProvider, + footer: { }) + } + } + private var otherUserFooter: some View { HStack(spacing: 8) { if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails { @@ -62,20 +83,15 @@ struct RoomMemberDetailsScreen: View { } @ViewBuilder - private var headerSection: some View { - if let memberDetails = context.viewState.memberDetails { - AvatarHeaderView(member: memberDetails, - avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) - } footer: { - otherUserFooter + var verificationSection: some View { + if context.viewState.showVerificationSection { + Section { + ListRow(label: .default(title: L10n.commonVerifyIdentity, + description: L10n.screenRoomMemberDetailsVerifyButtonSubtitle, + icon: \.lock), + kind: .button { }) + .disabled(true) } - } else { - AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), - avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider, - footer: { }) } } @@ -117,11 +133,15 @@ struct RoomMemberDetailsScreen: View { // MARK: - Previews struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { - static let otherUserViewModel = makeViewModel(member: .mockDan) + static let verifiedUserViewModel = makeViewModel(member: .mockDan) + static let otherUserViewModel = makeViewModel(member: .mockAlice) static let accountOwnerViewModel = makeViewModel(member: .mockMe) static let ignoredUserViewModel = makeViewModel(member: .mockIgnored) static var previews: some View { + RoomMemberDetailsScreen(context: verifiedUserViewModel.context) + .previewDisplayName("Verified User") + .snapshotPreferences(delay: 0.25) RoomMemberDetailsScreen(context: otherUserViewModel.context) .previewDisplayName("Other User") .snapshotPreferences(delay: 0.25) @@ -136,9 +156,12 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { static func makeViewModel(member: RoomMemberProxyMock) -> RoomMemberDetailsScreenViewModel { let roomProxyMock = JoinedRoomProxyMock(.init(name: "")) roomProxyMock.getMemberUserIDReturnValue = .success(member) - let clientProxyMock = ClientProxyMock(.init()) + clientProxyMock.userIdentityForClosure = { userID in + let isVerified = userID == RoomMemberProxyMock.mockDan.userID + return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified))) + } // to avoid mock the call state for the account owner test case if member.userID != RoomMemberProxyMock.mockMe.userID { clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") @@ -147,7 +170,7 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { return RoomMemberDetailsScreenViewModel(userID: member.userID, roomProxy: roomProxyMock, clientProxy: clientProxyMock, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index 129b8e8faa..a43afebdc6 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -32,7 +32,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe self.userIndicatorController = userIndicatorController self.analytics = analytics - super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, + super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, bindings: .init(mode: initialMode)), mediaProvider: mediaProvider) @@ -92,7 +92,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe let members = members.sorted() let roomMembersDetails = await buildMembersDetails(members: members) self.members = members - self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, + self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers, bannedMembers: roomMembersDetails.bannedMembers, diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift index ca99f99108..06d007a59e 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift @@ -100,7 +100,7 @@ private extension RoomMembersListScreenViewModel { static var mock: RoomMembersListScreenViewModel { RoomMembersListScreenViewModel(initialMode: .members, roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index ade0098d71..a5ac031b32 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -174,7 +174,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview { members: members, ownUserID: ownUserID, canUserInvite: false)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index f84e177565..efd25c1706 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -99,7 +99,7 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomMembersListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Some room", members: members)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift index 1075a58f42..068e2e44af 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift @@ -26,11 +26,11 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie let bindings = RoomNotificationSettingsScreenViewStateBindings() self.notificationSettingsProxy = notificationSettingsProxy self.roomProxy = roomProxy - let navigationTitle = displayAsUserDefinedRoomSettings ? roomProxy.roomTitle : L10n.screenRoomDetailsNotificationTitle + let navigationTitle = displayAsUserDefinedRoomSettings ? roomProxy.infoPublisher.value.displayName : L10n.screenRoomDetailsNotificationTitle let customSettingsSectionHeader = displayAsUserDefinedRoomSettings ? L10n.screenRoomNotificationSettingsRoomCustomSettingsTitle : L10n.screenRoomNotificationSettingsCustomSettingsTitle super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings, displayAsUserDefinedRoomSettings: displayAsUserDefinedRoomSettings, - navigationTitle: navigationTitle, + navigationTitle: navigationTitle ?? L10n.screenRoomDetailsNotificationTitle, customSettingsSectionHeader: customSettingsSectionHeader)) setupNotificationSettingsSubscription() @@ -80,7 +80,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie // `isOneToOne` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id, isEncrypted: roomProxy.isEncrypted, - isOneToOne: roomProxy.activeMembersCount == 2) + isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2) guard !Task.isCancelled else { return } state.notificationSettingsState = .loaded(settings: settings) if !state.isRestoringDefaultSetting { diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift index 7c1ae23f76..ef3b58f7d9 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift @@ -35,7 +35,7 @@ struct RoomNotificationSettingsScreen: View { kind: .toggle($context.allowCustomSetting)) .accessibilityIdentifier(A11yIdentifiers.roomNotificationSettingsScreen.allowCustomSetting) .disabled(context.viewState.notificationSettingsState.isLoading) - .onChange(of: context.allowCustomSetting) { _ in + .onChange(of: context.allowCustomSetting) { context.send(viewAction: .changedAllowCustomSettings) } } footer: { diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift index 9358e02270..0029e48b64 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift @@ -47,8 +47,8 @@ struct RoomPollsHistoryScreen: View { } .pickerStyle(.segmented) .readableFrame(maxWidth: 475) - .onChange(of: context.filter) { value in - context.send(viewAction: .filter(value)) + .onChange(of: context.filter) { _, newValue in + context.send(viewAction: .filter(newValue)) } } @@ -91,7 +91,7 @@ struct RoomPollsHistoryScreen: View { Button { context.send(viewAction: .loadMore) } label: { - Text(L10n.Action.loadMore) + Text(L10n.actionLoadMore) .font(.compound.bodyLGSemibold) .padding(.horizontal, 12) } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 692a32698b..2c7a4a50da 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -37,8 +37,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM updateMembers(roomProxy.membersPublisher.value) // Automatically update the room permissions - roomProxy.actionsPublisher - .filter { $0 == .roomInfoUpdate } + roomProxy.infoPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in Task { await self?.updatePermissions() } @@ -93,7 +92,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM showSavingIndicator() // A task we can await until the room's info gets modified with the new power levels. - let infoTask = Task { await roomProxy.actionsPublisher.values.first { $0 == .roomInfoUpdate } } + // Note: Ignore the first value as the publisher is backed by a current value subject. + let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } } switch await roomProxy.updatePowerLevelsForUsers([(userID: roomProxy.ownUserID, powerLevel: role.rustPowerLevel)]) { case .success: diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index de54046aa0..a084736de8 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -51,7 +51,7 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { membersSuggestion .insert(SuggestionItem.allUsers(item: .init(id: PillConstants.atRoom, displayName: PillConstants.everyone, - avatarURL: self.roomProxy.avatarURL, + avatarURL: self.roomProxy.infoPublisher.value.avatarURL, range: suggestionTrigger.range)), at: 0) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 9f3f169bfb..3459fb1c81 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -6,6 +6,7 @@ // import Compound +import MatrixRustSDK import SwiftUI import WysiwygComposer @@ -284,8 +285,8 @@ extension FormatType { enum ComposerMode: Equatable { case `default` - case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool) - case edit(originalItemId: TimelineItemIdentifier) + case reply(eventID: String, replyDetails: TimelineItemReplyDetails, isThread: Bool) + case edit(originalEventOrTransactionID: EventOrTransactionId) case recordVoiceMessage(state: AudioRecorderState) case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) @@ -323,8 +324,8 @@ enum ComposerMode: Equatable { var replyEventID: String? { switch self { - case .reply(let itemID, _, _): - return itemID.eventID + case .reply(let eventID, _, _): + return eventID default: return nil } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index f9bad3c7de..36471da729 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -258,9 +258,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool case .newMessage: set(mode: .default) case .edit(let eventID): - set(mode: .edit(originalItemId: .init(timelineID: "", eventID: eventID))) + set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID))) case .reply(let eventID): - set(mode: .reply(itemID: .init(timelineID: "", eventID: eventID), replyDetails: .loading(eventID: eventID), isThread: false)) + set(mode: .reply(eventID: eventID, replyDetails: .loading(eventID: eventID), isThread: false)) replyLoadingTask = Task { let reply = switch await draftService.getReply(eventID: eventID) { case .success(let reply): @@ -273,7 +273,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool return } - set(mode: .reply(itemID: .init(timelineID: "", eventID: eventID), replyDetails: reply.details, isThread: reply.isThreaded)) + set(mode: .reply(eventID: eventID, replyDetails: reply.details, isThread: reply.isThreaded)) } } } @@ -314,17 +314,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool switch state.composerMode { case .default: type = .newMessage - case .edit(let itemID): - guard let eventID = itemID.eventID else { - MXLog.error("The event id for this message is missing") - return - } - type = .edit(eventID: eventID) - case .reply(let itemID, _, _): - guard let eventID = itemID.eventID else { - MXLog.error("The event id for this message is missing") - return - } + case .edit(.eventId(let originalEventID)): + type = .edit(eventID: originalEventID) + case .reply(let eventID, _, _): type = .reply(eventID: eventID) default: if isVolatile { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift index fcb0536b98..29b5ff4cc9 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift @@ -116,12 +116,12 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview { static var previews: some View { // Putting them is VStack allows the preview to work properly in tests VStack(spacing: 8) { - CompletionSuggestionView(mediaProvider: MockMediaProvider(), + CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()), items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init())), .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory, range: .init()))]) { _ in } } VStack(spacing: 8) { - CompletionSuggestionView(mediaProvider: MockMediaProvider(), + CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()), items: multipleItems) { _ in } } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index 4f20be1a38..0cb580d0ab 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -143,6 +143,7 @@ struct ComposerToolbar: View { .disabled(context.viewState.sendButtonDisabled) .animation(.linear(duration: 0.1).disabledDuringTests(), value: context.viewState.sendButtonDisabled) .keyboardShortcut(.return, modifiers: [.command]) + .accessibilityIdentifier(A11yIdentifiers.roomScreen.sendButton) } private var messageComposer: some View { @@ -174,22 +175,23 @@ struct ComposerToolbar: View { .focused($composerFocused) .padding(.leading, context.composerFormattingEnabled ? 7 : 0) .padding(.trailing, context.composerFormattingEnabled ? 4 : 0) + .accessibilityIdentifier(A11yIdentifiers.roomScreen.messageComposer) .onTapGesture { guard !composerFocused else { return } composerFocused = true } - .onChange(of: context.composerFocused) { newValue in + .onChange(of: context.composerFocused) { _, newValue in guard composerFocused != newValue else { return } composerFocused = newValue } - .onChange(of: composerFocused) { newValue in + .onChange(of: composerFocused) { _, newValue in context.composerFocused = newValue } - .onChange(of: context.plainComposerText) { _ in + .onChange(of: context.plainComposerText) { context.send(viewAction: .plainComposerTextChanged) } - .onChange(of: context.composerFormattingEnabled) { _ in + .onChange(of: context.composerFormattingEnabled) { context.send(viewAction: .didToggleFormattingOptions) } .onAppear { @@ -293,7 +295,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { static let wysiwygViewModel = WysiwygComposerViewModel() static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) @@ -336,7 +338,7 @@ extension ComposerToolbar { var composerViewModel: ComposerToolbarViewModel { let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) @@ -353,7 +355,7 @@ extension ComposerToolbar { var composerViewModel: ComposerToolbarViewModel { let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) @@ -370,7 +372,7 @@ extension ComposerToolbar { var composerViewModel: ComposerToolbarViewModel { let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) @@ -388,7 +390,7 @@ extension ComposerToolbar { var composerViewModel: ComposerToolbarViewModel { let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) @@ -409,14 +411,14 @@ extension ComposerToolbar { var composerViewModel: ComposerToolbarViewModel { let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) - model.state.composerMode = isLoading ? .reply(itemID: .init(timelineID: ""), + model.state.composerMode = isLoading ? .reply(eventID: UUID().uuidString, replyDetails: .loading(eventID: ""), isThread: false) : - .reply(itemID: .init(timelineID: ""), + .reply(eventID: UUID().uuidString, replyDetails: .loaded(sender: .init(id: "", displayName: "Test"), eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift index 41e29b414d..eea817a9e6 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift @@ -35,7 +35,7 @@ struct MentionSuggestionItemView: View { } struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview { - static let mockMediaProvider = MockMediaProvider() + static let mockMediaProvider = MediaProviderMock(configuration: .init()) static var previews: some View { MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test", displayName: "Test", avatarURL: URL.documentsDirectory, range: .init())) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 85e0fd018d..0a142b37a1 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -206,16 +206,26 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { static let replyTypes: [TimelineItemReplyDetails] = [ .loaded(sender: .init(id: "Dave"), eventID: "123", - eventContent: .message(.audio(.init(body: "Audio: Ride the lightning", duration: 100, waveform: nil, source: nil, contentType: nil)))), + eventContent: .message(.audio(.init(filename: "lightning.mp3", + caption: "Audio: Ride the lightning", + duration: 100, + waveform: nil, + source: nil, + contentType: nil)))), .loaded(sender: .init(id: "James"), eventID: "123", eventContent: .message(.emote(.init(body: "Emote: James thinks he's the phantom lord")))), .loaded(sender: .init(id: "Robert"), eventID: "123", - eventContent: .message(.file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil)))), + eventContent: .message(.file(.init(filename: "brain-surgery.pdf", + caption: "File: Crash course in brain surgery", + source: nil, + thumbnailSource: nil, + contentType: nil)))), .loaded(sender: .init(id: "Cliff"), eventID: "123", - eventContent: .message(.image(.init(body: "Image: Pushead", + eventContent: .message(.image(.init(filename: "head.png", + caption: "Image: Pushead", source: .init(url: .picturesDirectory, mimeType: nil), thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))), .loaded(sender: .init(id: "Jason"), @@ -226,7 +236,8 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { eventContent: .message(.text(.init(body: "Text: Where the wild things are")))), .loaded(sender: .init(id: "Lars"), eventID: "123", - eventContent: .message(.video(.init(body: "Video: Through the never", + eventContent: .message(.video(.init(filename: "never.mov", + caption: "Video: Through the never", duration: 100, source: nil, thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))), @@ -264,9 +275,9 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { messageComposer() messageComposer(.init(string: "Some message"), - mode: .edit(originalItemId: .random)) + mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString))) - messageComposer(mode: .reply(itemID: .random, + messageComposer(mode: .reply(eventID: UUID().uuidString, replyDetails: .loaded(sender: .init(id: "Kirk"), eventID: "123", eventContent: .message(.text(.init(body: "Text: Where the wild things are")))), @@ -277,7 +288,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { ScrollView { VStack(spacing: 8) { ForEach(replyTypes, id: \.self) { replyDetails in - messageComposer(mode: .reply(itemID: .random, + messageComposer(mode: .reply(eventID: UUID().uuidString, replyDetails: replyDetails, isThread: false)) } } @@ -289,7 +300,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { ScrollView { VStack(spacing: 8) { ForEach(replyTypes, id: \.self) { replyDetails in - messageComposer(mode: .reply(itemID: .random, + messageComposer(mode: .reply(eventID: UUID().uuidString, replyDetails: replyDetails, isThread: true)) } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index 05469baf14..458e382d9c 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -79,6 +79,12 @@ private struct UITextViewWrapper: UIViewRepresentable { textView.textContainer.lineFragmentPadding = 0.0 textView.textContainerInset = .zero textView.keyboardType = .default + + // AutoCorrection doesn't work properly when running on the Mac + // https://github.com/element-hq/element-x-ios/issues/1786 + if ProcessInfo.processInfo.isiOSAppOnMac { + textView.autocorrectionType = .no + } textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -104,6 +110,11 @@ private struct UITextViewWrapper: UIViewRepresentable { // Remember the selection if only the attributes have changed. let selection = textView.attributedText.string == text.string ? textView.selectedTextRange : nil + // Fixes pill views not loading on the first attempt on iOS 18 + // because the textContainers's superview comes in as nil + // https://github.com/element-hq/element-x-ios/issues/3369 + _ = textView.layoutManager + textView.attributedText = text // Re-apply the default font when setting text for e.g. edits. diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift index 0871f4f9db..791ee8d548 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -89,7 +89,7 @@ private struct RoomAttachmentPickerButtonStyle: ButtonStyle { struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview { static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift index f290780761..0e20440141 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift @@ -48,8 +48,8 @@ struct VoiceMessagePreviewComposer: View { showCursor: playerState.showProgressIndicator, onSeek: onSeek) } - .onChange(of: isDragging) { isDragging in - onScrubbing(isDragging) + .onChange(of: isDragging) { _, newValue in + onScrubbing(newValue) } .padding(.vertical, 4.0) .padding(.horizontal, 6.0) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index f1bbc1506d..1f63793857 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -12,6 +12,7 @@ import SwiftUI import WysiwygComposer struct RoomScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol let roomProxy: JoinedRoomProxyProtocol var focussedEvent: FocusEvent? let timelineController: RoomTimelineControllerProtocol @@ -61,13 +62,15 @@ final class RoomScreenCoordinator: CoordinatorProtocol { selectedPinnedEventID = focussedEvent.shouldSetPin ? focussedEvent.eventID : nil } - roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, + roomViewModel = RoomScreenViewModel(clientProxy: parameters.clientProxy, + roomProxy: parameters.roomProxy, initialSelectedPinnedEventID: selectedPinnedEventID, mediaProvider: parameters.mediaProvider, ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher, appMediator: parameters.appMediator, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEvent?.eventID, @@ -78,7 +81,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: parameters.appMediator, appSettings: parameters.appSettings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: parameters.emojiProvider) wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, maxCompressedHeight: ComposerConstant.maxHeight, @@ -149,10 +153,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { .store(in: &cancellables) roomViewModel.actions - .sink { [weak self] actions in + .sink { [weak self] action in guard let self else { return } - switch actions { + switch action { case .focusEvent(eventID: let eventID): focusOnEvent(FocusEvent(eventID: eventID, shouldSetPin: false)) case .displayPinnedEventsTimeline: diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index cd2d31f02c..92c9bfdbe2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -21,6 +21,7 @@ enum RoomScreenViewAction { case viewAllPins case displayRoomDetails case displayCall + case footerViewAction(RoomScreenFooterViewAction) } struct RoomScreenViewState: BindableState { @@ -28,22 +29,31 @@ struct RoomScreenViewState: BindableState { var roomAvatar: RoomAvatar var lastScrollDirection: ScrollDirection? - var isPinningEnabled = false // This is used to control the banner var pinnedEventsBannerState: PinnedEventsBannerState = .loading(numbersOfEvents: 0) var shouldShowPinnedEventsBanner: Bool { - isPinningEnabled && !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top + !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top } var canJoinCall = false var hasOngoingCall: Bool var shouldShowCallButton = true + var footerDetails: RoomScreenFooterViewDetails? + var bindings: RoomScreenViewStateBindings } struct RoomScreenViewStateBindings { } +enum RoomScreenFooterViewAction { + case resolvePinViolation(userID: String) +} + +enum RoomScreenFooterViewDetails { + case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) +} + enum PinnedEventsBannerState: Equatable { case loading(numbersOfEvents: Int) case loaded(state: PinnedEventsState) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 8c33e3e3ad..93c95e1a3f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -7,18 +7,24 @@ import Combine import Foundation +import MatrixRustSDK import OrderedCollections import SwiftUI typealias RoomScreenViewModelType = StateStoreViewModel class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol private let roomProxy: JoinedRoomProxyProtocol private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analyticsService: AnalyticsService - private let pinnedEventStringBuilder: RoomEventStringBuilder + private let userIndicatorController: UserIndicatorControllerProtocol + private var initialSelectedPinnedEventID: String? + private let pinnedEventStringBuilder: RoomEventStringBuilder + + private var identityPinningViolations = [String: RoomMemberProxyProtocol]() private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -43,28 +49,33 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - init(roomProxy: JoinedRoomProxyProtocol, + init(clientProxy: ClientProxyProtocol, + roomProxy: JoinedRoomProxyProtocol, initialSelectedPinnedEventID: String?, mediaProvider: MediaProviderProtocol, ongoingCallRoomIDPublisher: CurrentValuePublisher, appMediator: AppMediatorProtocol, appSettings: AppSettings, - analyticsService: AnalyticsService) { + analyticsService: AnalyticsService, + userIndicatorController: UserIndicatorControllerProtocol) { + self.clientProxy = clientProxy self.roomProxy = roomProxy self.appMediator = appMediator self.appSettings = appSettings self.analyticsService = analyticsService + self.userIndicatorController = userIndicatorController + self.initialSelectedPinnedEventID = initialSelectedPinnedEventID pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) - super.init(initialViewState: .init(roomTitle: roomProxy.roomTitle, - roomAvatar: roomProxy.avatar, - hasOngoingCall: roomProxy.hasOngoingCall, + super.init(initialViewState: .init(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id, + roomAvatar: roomProxy.infoPublisher.value.avatar, + hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall, bindings: .init()), mediaProvider: mediaProvider) Task { - await handleRoomInfoUpdate() + await handleRoomInfoUpdate(roomProxy.infoPublisher.value) } setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher) @@ -87,6 +98,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayCall) actionsSubject.send(.removeComposerFocus) analyticsService.trackInteraction(name: .MobileRoomCallButton) + case .footerViewAction(let action): + switch action { + case .resolvePinViolation(let userID): + Task { await resolveIdentityPinningViolation(userID) } + } } } @@ -98,41 +114,48 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID) } + // MARK: - Private + private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher) { let roomInfoSubscription = roomProxy - .actionsPublisher - .filter { $0 == .roomInfoUpdate } + .infoPublisher roomInfoSubscription .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] _ in + .sink { [weak self] roomInfo in guard let self else { return } - state.roomTitle = roomProxy.roomTitle - state.roomAvatar = roomProxy.avatar - state.hasOngoingCall = roomProxy.hasOngoingCall + state.roomTitle = roomInfo.displayName ?? roomProxy.id + state.roomAvatar = roomInfo.avatar + state.hasOngoingCall = roomInfo.hasRoomCall } .store(in: &cancellables) Task { [weak self] in - for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { + for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values { guard !Task.isCancelled else { return } - await self?.handleRoomInfoUpdate() + await self?.handleRoomInfoUpdate(roomInfo) } } .store(in: &cancellables) - let pinningEnabledPublisher = appSettings.$pinningEnabled + let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main) - pinningEnabledPublisher - .weakAssign(to: \.state.isPinningEnabled, on: self) - .store(in: &cancellables) + Task { [weak self] in + for await changes in identityStatusChangesPublisher.values { + guard !Task.isCancelled else { + return + } + + await self?.processIdentityStatusChanges(changes) + } + } + .store(in: &cancellables) - pinningEnabledPublisher - .combineLatest(appMediator.networkMonitor.reachabilityPublisher) - .filter { $0.0 && $0.1 == .reachable } + appMediator.networkMonitor.reachabilityPublisher + .filter { $0 == .reachable } .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.setupPinnedEventsTimelineProviderIfNeeded() @@ -148,6 +171,43 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .store(in: &cancellables) } + private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async { + for change in changes { + switch change.changedTo { + case .pinned: + identityPinningViolations[change.userId] = nil + case .pinViolation: + guard case let .success(member) = await roomProxy.getMember(userID: change.userId) else { + MXLog.error("Failed retrieving room member for identity status change: \(change)") + continue + } + + identityPinningViolations[change.userId] = member + default: + break + } + } + + if let member = identityPinningViolations.values.first { + state.footerDetails = .pinViolation(member: member, + learnMoreURL: appSettings.identityPinningViolationDetailsURL) + } else { + state.footerDetails = nil + } + } + + private func resolveIdentityPinningViolation(_ userID: String) async { + defer { + hideLoadingIndicator() + } + + showLoadingIndicator() + + if case .failure = await clientProxy.pinUserIdentity(userID) { + userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) + } + } + private func buildPinnedEventContents(timelineItems: [TimelineItemProxy]) { var pinnedEventContents = OrderedDictionary() @@ -169,8 +229,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - private func handleRoomInfoUpdate() async { - let pinnedEventIDs = await roomProxy.pinnedEventIDs + private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async { + let pinnedEventIDs = roomInfo.pinnedEventIDs // Only update the loading state of the banner if state.pinnedEventsBannerState.isLoading { state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) @@ -197,28 +257,30 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } } + + // MARK: Loading indicators + + private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading" + + private func showLoadingIndicator() { + userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading)) + } + + private func hideLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } } extension RoomScreenViewModel { static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel { - RoomScreenViewModel(roomProxy: roomProxyMock, + RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) - } -} - -private struct RoomContextKey: EnvironmentKey { - @MainActor static let defaultValue: RoomScreenViewModel.Context? = nil -} - -extension EnvironmentValues { - /// Used to access and inject the room context without observing it - var roomContext: RoomScreenViewModel.Context? { - get { self[RoomContextKey.self] } - set { self[RoomContextKey.self] = newValue } + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index e1f1b81ea0..9e187d8ab3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -28,21 +28,6 @@ struct RoomScreen: View { var body: some View { timeline .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) - .safeAreaInset(edge: .bottom, spacing: 0) { - composerToolbar - .padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12) - .background { - if composerToolbarContext.composerFormattingEnabled { - RoundedRectangle(cornerRadius: 20) - .stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5) - .ignoresSafeArea() - } - } - .padding(.top, 8) - .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) - .environmentObject(timelineContext) - .environment(\.timelineContext, timelineContext) - } .overlay(alignment: .top) { Group { if roomContext.viewState.shouldShowPinnedEventsBanner { @@ -51,6 +36,30 @@ struct RoomScreen: View { } .animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner) } + .safeAreaInset(edge: .bottom, spacing: 0) { + VStack(spacing: 0) { + RoomScreenFooterView(details: roomContext.viewState.footerDetails, + mediaProvider: roomContext.mediaProvider) { action in + roomContext.send(viewAction: .footerViewAction(action)) + } + + composerToolbar + .padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12) + .background { + if composerToolbarContext.composerFormattingEnabled { + RoundedRectangle(cornerRadius: 20) + .stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5) + .ignoresSafeArea() + } + } + .padding(.top, 8) + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .environmentObject(timelineContext) + .environment(\.timelineContext, timelineContext) + // Make sure the reply header honours the hideTimelineMedia setting too. + .environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia) + } + } .navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text. .navigationBarTitleDisplayMode(.inline) .navigationBarHidden(isNavigationBarHidden) @@ -67,7 +76,8 @@ struct RoomScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { TimelineItemMenu(item: info.item, actions: actions) @@ -214,13 +224,14 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift new file mode 100644 index 0000000000..3be6e50242 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift @@ -0,0 +1,98 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import SwiftUI + +struct RoomScreenFooterView: View { + let details: RoomScreenFooterViewDetails? + let mediaProvider: MediaProviderProtocol? + let callback: (RoomScreenFooterViewAction) -> Void + + var body: some View { + if let details { + ZStack(alignment: .top) { + VStack(spacing: 0) { + Color.compound.borderInfoSubtle + .frame(height: 1) + LinearGradient(colors: [.compound.bgInfoSubtle, .compound.bgCanvasDefault], + startPoint: .top, + endPoint: .bottom) + } + + switch details { + case .pinViolation(let member, let learnMoreURL): + pinViolation(member: member, learnMoreURL: learnMoreURL) + } + } + .padding(.top, 8) + .fixedSize(horizontal: false, vertical: true) + } + } + + private func pinViolation(member: RoomMemberProxyProtocol, + learnMoreURL: URL) -> some View { + VStack(spacing: 16) { + HStack(spacing: 16) { + LoadableAvatarImage(url: member.avatarURL, + name: member.disambiguatedDisplayName, + contentID: member.userID, + avatarSize: .user(on: .timeline), + mediaProvider: mediaProvider) + + Text(pinViolationDescriptionWithLearnMoreLink(displayName: member.displayName, + userID: member.userID, + url: learnMoreURL)) + .font(.compound.bodyMD) + .foregroundColor(.compound.textPrimary) + } + + Button(L10n.actionOk) { + callback(.resolvePinViolation(userID: member.userID)) + } + .buttonStyle(.compound(.primary, size: .medium)) + } + .padding(.top, 16) + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + + private func pinViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString { + let userIDPlaceholder = "{mxid}" + let linkPlaceholder = "{link}" + let displayName = displayName ?? fallbackDisplayName(userID) + var description = AttributedString(L10n.cryptoIdentityChangePinViolationNew(displayName, userIDPlaceholder, linkPlaceholder)) + + var userIDString = AttributedString(L10n.cryptoIdentityChangePinViolationNewUserId(userID)) + userIDString.bold() + description.replace(userIDPlaceholder, with: userIDString) + + var linkString = AttributedString(L10n.actionLearnMore) + linkString.link = url + linkString.bold() + description.replace(linkPlaceholder, with: linkString) + return description + } + + private func fallbackDisplayName(_ userID: String) -> String { + guard let localpart = userID.components(separatedBy: ":").first else { return userID } + return String(localpart.trimmingPrefix("@")) + } +} + +struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { + static let bobDetails: RoomScreenFooterViewDetails = .pinViolation(member: RoomMemberProxyMock.mockBob, + learnMoreURL: "https://element.io/") + static let noNameDetails: RoomScreenFooterViewDetails = .pinViolation(member: RoomMemberProxyMock.mockNoName, + learnMoreURL: "https://element.io/") + + static var previews: some View { + RoomScreenFooterView(details: bobDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in } + .previewDisplayName("With displayname") + RoomScreenFooterView(details: noNameDetails, mediaProvider: MediaProviderMock(configuration: .init())) { _ in } + .previewDisplayName("Without displayname") + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift index 09c9286fd3..78b444b83c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift @@ -32,8 +32,8 @@ struct SwipeRightAction: ViewModifier { .offset(x: xOffset, y: 0.0) .animation(.interactiveSpring().speed(0.5), value: xOffset) .timelineGesture(gesture) - .onChange(of: dragGestureActive) { value in - if value == true { + .onChange(of: dragGestureActive) { _, newValue in + if newValue == true { if shouldStartAction() { feedbackGenerator.prepare() canStartAction = true diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift index d578bcd9b9..4a0de09b95 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift @@ -22,6 +22,7 @@ struct SecureBackupKeyBackupScreen: View { Text(L10n.screenChatBackupKeyBackupActionDisable) } .buttonStyle(.compound(.primary)) + .accessibilityIdentifier(A11yIdentifiers.secureBackupKeyBackupScreen.deleteKeyStorage) } .background() .backgroundStyle(.compound.bgCanvasDefault) @@ -39,37 +40,37 @@ struct SecureBackupKeyBackupScreen: View { } private var disableBackupSection: some View { - VStack(spacing: 16) { - HeroImage(icon: \.keyOffSolid) - - Text(L10n.screenKeyBackupDisableTitle) - .foregroundColor(.compound.textPrimary) - .font(.compound.headingMDBold) - .multilineTextAlignment(.center) - - Text(L10n.screenKeyBackupDisableDescription) - .foregroundColor(.compound.textSecondary) - .font(.compound.bodyMD) - .multilineTextAlignment(.center) - - VStack(alignment: .leading, spacing: 10) { - Label { - Text(L10n.screenKeyBackupDisableDescriptionPoint1) + VStack(spacing: 24) { + VStack(spacing: 16) { + BigIcon(icon: \.error, style: .alertSolid) + + VStack(spacing: 8) { + Text(L10n.screenKeyBackupDisableTitle) + .foregroundColor(.compound.textPrimary) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + + Text(L10n.screenKeyBackupDisableDescription) .foregroundColor(.compound.textSecondary) .font(.compound.bodyMD) - } icon: { - CompoundIcon(\.close) + .multilineTextAlignment(.center) + } + } + + VStack(alignment: .leading, spacing: 4) { + VisualListItem(title: L10n.screenKeyBackupDisableDescriptionPoint1, + position: .top) { + CompoundIcon(\.close, size: .small, relativeTo: .body) .foregroundColor(.compound.iconCriticalPrimary) } + .backgroundStyle(.compound.bgActionSecondaryHovered) - Label { - Text(L10n.screenKeyBackupDisableDescriptionPoint2(InfoPlistReader.main.bundleDisplayName)) - .foregroundColor(.compound.textSecondary) - .font(.compound.bodyMD) - } icon: { - CompoundIcon(\.close) + VisualListItem(title: L10n.screenKeyBackupDisableDescriptionPoint2(InfoPlistReader.main.productionAppName), + position: .bottom) { + CompoundIcon(\.close, size: .small, relativeTo: .body) .foregroundColor(.compound.iconCriticalPrimary) } + .backgroundStyle(.compound.bgActionSecondaryHovered) } } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift index 0c682c53c1..6db15cb4ba 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift @@ -15,7 +15,7 @@ struct SecureBackupLogoutConfirmationScreen: View { var body: some View { FullscreenDialog { VStack(spacing: 16) { - HeroImage(icon: \.keyOffSolid) + BigIcon(icon: \.keyOffSolid) content } .padding() diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift index 0713b3405c..38788a0028 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenCoordinator.swift @@ -15,23 +15,22 @@ struct SecureBackupRecoveryKeyScreenCoordinatorParameters { } enum SecureBackupRecoveryKeyScreenCoordinatorAction { - case cancel - case recoverySetUp - case recoveryChanged - case recoveryFixed - case resetEncryption + case complete } final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { + private let parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters private var viewModel: SecureBackupRecoveryKeyScreenViewModelProtocol - private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } init(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters) { + self.parameters = parameters viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: parameters.secureBackupController, userIndicatorController: parameters.userIndicatorController, isModallyPresented: parameters.isModallyPresented) @@ -44,20 +43,19 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { case .cancel: - self.actionsSubject.send(.cancel) + self.actionsSubject.send(.complete) case .done(let mode): switch mode { case .setupRecovery: - self.actionsSubject.send(.recoverySetUp) + showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess) case .changeRecovery: - self.actionsSubject.send(.recoveryChanged) + showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess) case .fixRecovery: - self.actionsSubject.send(.recoveryFixed) + showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess) case .unknown: fatalError() } - case .resetEncryption: - self.actionsSubject.send(.resetEncryption) + self.actionsSubject.send(.complete) } } .store(in: &cancellables) @@ -66,4 +64,14 @@ final class SecureBackupRecoveryKeyScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView(SecureBackupRecoveryKeyScreen(context: viewModel.context)) } + + // MARK: - Private + + private func showSuccessIndicator(title: String) { + parameters.userIndicatorController.submitIndicator(.init(id: .init(), + type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false), + title: title, + iconName: "checkmark", + persistent: false)) + } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift index b39de38910..066577b9df 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenModels.swift @@ -10,7 +10,6 @@ import Foundation enum SecureBackupRecoveryKeyScreenViewModelAction { case done(mode: SecureBackupRecoveryKeyScreenViewMode) case cancel - case resetEncryption } enum SecureBackupRecoveryKeyScreenViewMode { @@ -27,6 +26,7 @@ struct SecureBackupRecoveryKeyScreenViewState: BindableState { let mode: SecureBackupRecoveryKeyScreenViewMode var recoveryKey: String? + var isGeneratingKey = false var doneButtonEnabled = false var bindings: SecureBackupRecoveryKeyScreenViewBindings @@ -81,7 +81,6 @@ enum SecureBackupRecoveryKeyScreenViewAction { case copyKey case keySaved case confirmKey - case resetEncryption case done case cancel } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift index 35a51a48d1..0c3ca4ac1e 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift @@ -28,18 +28,6 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM super.init(initialViewState: .init(isModallyPresented: isModallyPresented, mode: secureBackupController.recoveryState.value.viewMode, bindings: .init())) - - secureBackupController.recoveryState - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] state in - switch state { - case .settingUp: - self?.showLoadingIndicator() - default: - self?.hideLoadingIndicator() - } - }) - .store(in: &cancellables) } // MARK: - Public @@ -49,6 +37,8 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM switch viewAction { case .generateKey: + state.isGeneratingKey = true + Task { switch await secureBackupController.generateRecoveryKey() { case .success(let key): @@ -58,7 +48,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM state.bindings.alertInfo = .init(id: .init()) } - hideLoadingIndicator() + state.isGeneratingKey = false } case .copyKey: UIPasteboard.general.string = state.recoveryKey @@ -75,7 +65,9 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM actionsSubject.send(.done(mode: context.viewState.mode)) case .failure(let error): MXLog.error("Failed confirming recovery key with error: \(error)") - state.bindings.alertInfo = .init(id: .init()) + state.bindings.alertInfo = .init(id: .init(), + title: L10n.screenRecoveryKeyConfirmErrorTitle, + message: L10n.screenRecoveryKeyConfirmErrorContent) } hideLoadingIndicator() @@ -86,13 +78,11 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM state.bindings.alertInfo = .init(id: .init(), title: L10n.screenRecoveryKeySetupConfirmationTitle, message: L10n.screenRecoveryKeySetupConfirmationDescription, - primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), - secondaryButton: .init(title: L10n.actionContinue, action: { [weak self] in + primaryButton: .init(title: L10n.actionContinue) { [weak self] in guard let self else { return } actionsSubject.send(.done(mode: context.viewState.mode)) - })) - case .resetEncryption: - actionsSubject.send(.resetEncryption) + }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift index f9d17174a6..acd39bd180 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -18,8 +18,7 @@ struct SecureBackupRecoveryKeyScreen: View { FullscreenDialog { ScrollViewReader { reader in mainContent - .padding(16) - .onChange(of: focused) { newValue in + .onChange(of: focused) { _, newValue in guard newValue == true else { return } reader.scrollTo(textFieldIdentifier) } @@ -53,7 +52,7 @@ struct SecureBackupRecoveryKeyScreen: View { private var header: some View { VStack(spacing: 16) { - HeroImage(icon: \.keySolid) + BigIcon(icon: \.keySolid) Text(context.viewState.title) .foregroundColor(.compound.textPrimary) @@ -90,24 +89,17 @@ struct SecureBackupRecoveryKeyScreen: View { } .buttonStyle(.compound(.primary)) .disabled(context.confirmationRecoveryKey.isEmpty) - - Button { - context.send(viewAction: .resetEncryption) - } label: { - Text(L10n.screenIdentityConfirmationCreateNewRecoveryKey) - .padding(.vertical, 14) - } - .buttonStyle(.compound(.plain)) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.confirm) } } private var recoveryCreatedActionButtons: some View { - VStack(spacing: 8.0) { + VStack(spacing: 16) { if let recoveryKey = context.viewState.recoveryKey { ShareLink(item: recoveryKey) { Label(L10n.screenRecoveryKeySaveAction, icon: \.download) } - .buttonStyle(.compound(.primary)) + .buttonStyle(.compound(.secondary)) .simultaneousGesture(TapGesture().onEnded { _ in context.send(viewAction: .keySaved) }) @@ -120,6 +112,7 @@ struct SecureBackupRecoveryKeyScreen: View { } .buttonStyle(.compound(.primary)) .disabled(context.viewState.recoveryKey == nil || context.viewState.doneButtonEnabled == false) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.done) } } @@ -139,20 +132,32 @@ struct SecureBackupRecoveryKeyScreen: View { Text(L10n.commonRecoveryKey) .foregroundColor(.compound.textPrimary) .font(.compound.bodySMSemibold) + .padding(.leading, 16) Group { if context.viewState.recoveryKey == nil { - Button(generateButtonTitle) { - context.send(viewAction: .generateKey) + if !context.viewState.isGeneratingKey { + Button(generateButtonTitle) { + context.send(viewAction: .generateKey) + } + .font(.compound.bodyLGSemibold) + .padding(.vertical, 11) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey) + } else { + HStack(spacing: 8) { + ProgressView() + Text(L10n.screenRecoveryKeyGeneratingKey) + } + .font(.compound.bodyLGSemibold) + .foregroundStyle(.compound.textPrimary) + .padding(.vertical, 11) } - .font(.compound.bodyLGSemibold) } else { - HStack(alignment: .top, spacing: 8) { + HStack(spacing: 8) { Text(context.viewState.recoveryKey ?? "") .foregroundColor(.compound.textPrimary) .font(.compound.bodyLG) - - Spacer() + .frame(maxWidth: .infinity, alignment: .leading) Button { context.send(viewAction: .copyKey) @@ -161,25 +166,21 @@ struct SecureBackupRecoveryKeyScreen: View { } .tint(.compound.iconSecondary) .accessibilityLabel(L10n.actionCopy) + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey) } } } .frame(maxWidth: .infinity) - .padding() + .padding(.vertical, 14) + .padding(.horizontal, 16) .background(Color.compound.bgSubtleSecondaryLevel0) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: 14)) if let subtitle = context.viewState.recoveryKeySubtitle { - Label { - Text(subtitle) - .foregroundColor(.compound.textSecondary) - .font(.compound.bodySM) - } icon: { - if context.viewState.recoveryKey == nil { - CompoundIcon(\.infoSolid, size: .small, relativeTo: .compound.bodySM) - } - } - .labelStyle(.custom(spacing: 8, alignment: .top)) + Text(subtitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodySM) + .padding(.leading, 16) } } } @@ -207,6 +208,7 @@ struct SecureBackupRecoveryKeyScreen: View { .onSubmit { context.send(viewAction: .confirmKey) } + .accessibilityIdentifier(A11yIdentifiers.secureBackupRecoveryKeyScreen.recoveryKeyField) if let subtitle = context.viewState.recoveryKeySubtitle { Text(subtitle) @@ -220,8 +222,10 @@ struct SecureBackupRecoveryKeyScreen: View { // MARK: - Previews struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview { - static let setupViewModel = viewModel(recoveryState: .enabled) + static let key = "EsTM njec uHYA yHmh dQdW Nj4o bNRU 9jMN XGMc KUNM UFr5 R8GY" static let notSetUpViewModel = viewModel(recoveryState: .disabled) + static let generatingViewModel = viewModel(recoveryState: .disabled, generateKey: true) + static let setupViewModel = viewModel(recoveryState: .enabled, generateKey: true, key: key) static let incompleteViewModel = viewModel(recoveryState: .incomplete) static let unknownViewModel = viewModel(recoveryState: .unknown) @@ -231,10 +235,17 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview } .previewDisplayName("Not set up") + NavigationStack { + SecureBackupRecoveryKeyScreen(context: generatingViewModel.context) + } + .previewDisplayName("Generating") + .snapshot(delay: 0.25) + NavigationStack { SecureBackupRecoveryKeyScreen(context: setupViewModel.context) } .previewDisplayName("Set up") + .snapshot(delay: 0.25) NavigationStack { SecureBackupRecoveryKeyScreen(context: incompleteViewModel.context) @@ -247,12 +258,27 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview .previewDisplayName("Unknown") } - static func viewModel(recoveryState: SecureBackupRecoveryState) -> SecureBackupRecoveryKeyScreenViewModelType { + static func viewModel(recoveryState: SecureBackupRecoveryState, generateKey: Bool = false, key: String? = nil) -> SecureBackupRecoveryKeyScreenViewModelType { let backupController = SecureBackupControllerMock() backupController.underlyingRecoveryState = CurrentValueSubject(recoveryState).asCurrentValuePublisher() - return SecureBackupRecoveryKeyScreenViewModel(secureBackupController: backupController, - userIndicatorController: UserIndicatorControllerMock(), - isModallyPresented: true) + if let key { + backupController.generateRecoveryKeyReturnValue = .success(key) + } else { + backupController.generateRecoveryKeyClosure = { + try? await Task.sleep(for: .seconds(1000)) + return .success("youshouldntseeme") + } + } + + let viewModel = SecureBackupRecoveryKeyScreenViewModel(secureBackupController: backupController, + userIndicatorController: UserIndicatorControllerMock(), + isModallyPresented: true) + + if generateKey { + viewModel.context.send(viewAction: .generateKey) + } + + return viewModel } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift index 6605642e8f..52710b9344 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenCoordinator.swift @@ -11,12 +11,12 @@ import SwiftUI struct SecureBackupScreenCoordinatorParameters { let appSettings: AppSettings let clientProxy: ClientProxyProtocol - weak var navigationStackCoordinator: NavigationStackCoordinator? let userIndicatorController: UserIndicatorControllerProtocol } enum SecureBackupScreenCoordinatorAction { - case requestOIDCAuthorisation(URL) + case manageRecoveryKey + case disableKeyBackup } final class SecureBackupScreenCoordinator: CoordinatorProtocol { @@ -43,53 +43,10 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .recoveryKey: - let recoveryNavigationStackCoordinator = NavigationStackCoordinator() - - let recoveryKeyCoordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController, - userIndicatorController: parameters.userIndicatorController, - isModallyPresented: true)) - - recoveryKeyCoordinator.actions.sink { [weak self] action in - guard let self else { return } - switch action { - case .cancel: - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoverySetUp: - showSuccessIndicator(title: L10n.screenRecoveryKeySetupSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoveryChanged: - showSuccessIndicator(title: L10n.screenRecoveryKeyChangeSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .recoveryFixed: - showSuccessIndicator(title: L10n.screenRecoveryKeyConfirmSuccess) - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - case .resetEncryption: - showEncryptionReset(recoveryNavigationStackCoordinator: recoveryNavigationStackCoordinator) - } - } - .store(in: &cancellables) - - recoveryNavigationStackCoordinator.setRootCoordinator(recoveryKeyCoordinator, animated: true) - - parameters.navigationStackCoordinator?.setSheetCoordinator(recoveryNavigationStackCoordinator) - case .keyBackup: - let navigationStackCoordinator = NavigationStackCoordinator() - - let keyBackupCoordinator = SecureBackupKeyBackupScreenCoordinator(parameters: .init(secureBackupController: parameters.clientProxy.secureBackupController, - userIndicatorController: parameters.userIndicatorController)) - - keyBackupCoordinator.actions.sink { [weak self] action in - switch action { - case .done: - self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - } - } - .store(in: &cancellables) - - navigationStackCoordinator.setRootCoordinator(keyBackupCoordinator, animated: true) - - parameters.navigationStackCoordinator?.setSheetCoordinator(navigationStackCoordinator) + case .manageRecoveryKey: + actionsSubject.send(.manageRecoveryKey) + case .disableKeyBackup: + actionsSubject.send(.disableKeyBackup) } } .store(in: &cancellables) @@ -98,41 +55,4 @@ final class SecureBackupScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView(SecureBackupScreen(context: viewModel.context)) } - - // MARK: - Private - - private func showSuccessIndicator(title: String) { - parameters.userIndicatorController.submitIndicator(.init(id: .init(), - type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false), - title: title, - iconName: "checkmark", - persistent: false)) - } - - private func showEncryptionReset(recoveryNavigationStackCoordinator: NavigationStackCoordinator) { - let resetNavigationStackCoordinator = NavigationStackCoordinator() - - let coordinator = EncryptionResetScreenCoordinator(parameters: .init(clientProxy: parameters.clientProxy, - navigationStackCoordinator: resetNavigationStackCoordinator, - userIndicatorController: parameters.userIndicatorController)) - - coordinator.actionsPublisher.sink { [weak self] action in - guard let self else { return } - - switch action { - case .cancel: - recoveryNavigationStackCoordinator.setSheetCoordinator(nil) - case .requestOIDCAuthorisation(let url): - actionsSubject.send(.requestOIDCAuthorisation(url)) - case .resetFinished: - parameters.navigationStackCoordinator?.setSheetCoordinator(nil) // Dismiss the recovery screen - recoveryNavigationStackCoordinator.setSheetCoordinator(nil) - } - } - .store(in: &cancellables) - - resetNavigationStackCoordinator.setRootCoordinator(coordinator) - - recoveryNavigationStackCoordinator.setSheetCoordinator(resetNavigationStackCoordinator) - } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift index 1fa713d1c4..85925e073f 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenModels.swift @@ -8,22 +8,27 @@ import Foundation enum SecureBackupScreenViewModelAction { - case recoveryKey - case keyBackup + case manageRecoveryKey + case disableKeyBackup } struct SecureBackupScreenViewState: BindableState { let chatBackupDetailsURL: URL var recoveryState = SecureBackupRecoveryState.unknown var keyBackupState = SecureBackupKeyBackupState.unknown - var bindings = SecureBackupScreenViewStateBindings() + var bindings: SecureBackupScreenViewStateBindings + + var keyStorageToggleDescription: String? { + keyBackupState.keyStorageToggleState ? nil : L10n.screenChatBackupKeyStorageDisabledError + } } struct SecureBackupScreenViewStateBindings { + var keyStorageEnabled: Bool var alertInfo: AlertInfo? } enum SecureBackupScreenViewAction { case recoveryKey - case keyBackup + case keyStorageToggled(Bool) } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift index e3fda71d4d..c929f5319a 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -25,7 +25,8 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup self.secureBackupController = secureBackupController self.userIndicatorController = userIndicatorController - super.init(initialViewState: .init(chatBackupDetailsURL: chatBackupDetailsURL)) + super.init(initialViewState: .init(chatBackupDetailsURL: chatBackupDetailsURL, + bindings: SecureBackupScreenViewStateBindings(keyStorageEnabled: secureBackupController.keyBackupState.value.keyStorageToggleState))) secureBackupController.recoveryState .receive(on: DispatchQueue.main) @@ -34,7 +35,11 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup secureBackupController.keyBackupState .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.keyBackupState, on: self) + .sink { [weak self] state in + guard let self else { return } + self.state.keyBackupState = state + self.state.bindings.keyStorageEnabled = state.keyStorageToggleState + } .store(in: &cancellables) } @@ -43,13 +48,16 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup override func process(viewAction: SecureBackupScreenViewAction) { switch viewAction { case .recoveryKey: - actionsSubject.send(.recoveryKey) - case .keyBackup: - switch secureBackupController.keyBackupState.value { - case .unknown: + actionsSubject.send(.manageRecoveryKey) + case .keyStorageToggled(let enable): + let keyBackupState = secureBackupController.keyBackupState.value + switch (keyBackupState, enable) { + case (.unknown, true): + state.bindings.keyStorageEnabled = keyBackupState.keyStorageToggleState // Reset the toggle in case enabling fails enableBackup() - case .enabled: - actionsSubject.send(.keyBackup) + case (.enabled, false): + state.bindings.keyStorageEnabled = keyBackupState.keyStorageToggleState // Reset the toggle in case the user cancels + actionsSubject.send(.disableKeyBackup) default: break } @@ -74,3 +82,12 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup } } } + +extension SecureBackupKeyBackupState { + var keyStorageToggleState: Bool { + switch self { + case .unknown, .enabling: false + case .enabled, .disabling: true + } + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift index cdc0aa2827..a0aaa17181 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -28,7 +28,7 @@ struct SecureBackupScreen: View { } } .compoundList() - .navigationTitle(L10n.commonChatBackup) + .navigationTitle(L10n.commonEncryption) .navigationBarTitleDisplayMode(.inline) .alert(item: $context.alertInfo) } @@ -39,7 +39,7 @@ struct SecureBackupScreen: View { private var keyBackupSection: some View { Section { ListRow(kind: .custom { - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 8) { Text(L10n.screenChatBackupKeyBackupTitle) .font(.compound.bodyLGSemibold) .foregroundColor(.compound.textPrimary) @@ -53,7 +53,13 @@ struct SecureBackupScreen: View { .accessibilityElement(children: .combine) }) - keyBackupButton + ListRow(label: .plain(title: L10n.screenChatBackupKeyStorageToggleTitle, + description: context.viewState.keyStorageToggleDescription), + kind: .toggle($context.keyStorageEnabled)) + .onChange(of: context.keyStorageEnabled) { _, newValue in + context.send(viewAction: .keyStorageToggled(newValue)) + } + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.keyStorage) } } @@ -67,35 +73,29 @@ struct SecureBackupScreen: View { return description } - @ViewBuilder - private var keyBackupButton: some View { - switch context.viewState.keyBackupState { - case .enabled, .disabling: - ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionDisable, role: .destructive), kind: .navigationLink { - context.send(viewAction: .keyBackup) - }) - case .unknown, .enabling: - ListRow(label: .plain(title: L10n.screenChatBackupKeyBackupActionEnable), kind: .navigationLink { - context.send(viewAction: .keyBackup) - }) - } - } - - @ViewBuilder private var recoveryKeySection: some View { Section { switch context.viewState.recoveryState { case .enabled: - ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionChange), + ListRow(label: .default(title: L10n.screenChatBackupRecoveryActionChange, + description: L10n.screenChatBackupRecoveryActionChangeDescription, + icon: \.key, + iconAlignment: .top), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) case .disabled: - ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionSetup), + ListRow(label: .default(title: L10n.screenChatBackupRecoveryActionSetup, + description: L10n.screenChatBackupRecoveryActionChangeDescription, + icon: \.key, + iconAlignment: .top), details: .icon(BadgeView(size: 10)), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) case .incomplete: ListRow(label: .plain(title: L10n.screenChatBackupRecoveryActionConfirm), details: .icon(BadgeView(size: 10)), kind: .navigationLink { context.send(viewAction: .recoveryKey) }) + .accessibilityIdentifier(A11yIdentifiers.secureBackupScreen.recoveryKey) default: ListRow(label: .plain(title: L10n.commonLoading), details: .isWaiting(true), kind: .label) } @@ -108,8 +108,6 @@ struct SecureBackupScreen: View { @ViewBuilder private var recoveryKeySectionFooter: some View { switch context.viewState.recoveryState { - case .disabled: - Text(L10n.screenChatBackupRecoveryActionSetupDescription(InfoPlistReader.main.bundleDisplayName)) case .incomplete: Text(L10n.screenChatBackupRecoveryActionConfirmDescription) default: diff --git a/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift b/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift index c6980de8eb..3be0d1a2a2 100644 --- a/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift +++ b/ElementX/Sources/Screens/Settings/AccountSettings/OIDCAccountSettingsPresenter.swift @@ -28,7 +28,7 @@ class OIDCAccountSettingsPresenter: NSObject { /// Presents a web authentication session for the supplied data. func start() { - let session = ASWebAuthenticationSession(url: accountURL, callbackURLScheme: oidcRedirectURL.scheme) { _, _ in } + let session = ASWebAuthenticationSession(url: accountURL, callback: .oidcRedirectURL(oidcRedirectURL)) { _, _ in } session.prefersEphemeralWebBrowserSession = false session.presentationContextProvider = self session.start() diff --git a/ElementX/Sources/Screens/Settings/AnalyticsSettingsScreen/View/AnalyticsSettingsScreen.swift b/ElementX/Sources/Screens/Settings/AnalyticsSettingsScreen/View/AnalyticsSettingsScreen.swift index 1972bdabba..916a9e6c23 100644 --- a/ElementX/Sources/Screens/Settings/AnalyticsSettingsScreen/View/AnalyticsSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/AnalyticsSettingsScreen/View/AnalyticsSettingsScreen.swift @@ -24,7 +24,7 @@ struct AnalyticsSettingsScreen: View { Section { ListRow(label: .plain(title: L10n.screenAnalyticsSettingsShareData), kind: .toggle($context.enableAnalytics)) - .onChange(of: context.enableAnalytics) { _ in + .onChange(of: context.enableAnalytics) { context.send(viewAction: .toggleAnalytics) } } footer: { diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift index 20715a393f..1094a14a12 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift @@ -8,11 +8,16 @@ import Combine import SwiftUI +struct AdvancedSettingsScreenCoordinatorParameters { + let appSettings: AppSettings + let analytics: AnalyticsService +} + final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol { private var viewModel: AdvancedSettingsScreenViewModelProtocol - init() { - viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings) + init(parameters: AdvancedSettingsScreenCoordinatorParameters) { + viewModel = AdvancedSettingsScreenViewModel(advancedSettings: parameters.appSettings, analytics: parameters.analytics) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift index 35f0e8148e..2003ae6688 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift @@ -26,12 +26,16 @@ struct AdvancedSettingsScreenViewStateBindings { } } -enum AdvancedSettingsScreenViewAction { } +enum AdvancedSettingsScreenViewAction { + case optimizeMediaUploadsChanged +} protocol AdvancedSettingsProtocol: AnyObject { var viewSourceEnabled: Bool { get set } var appAppearance: AppAppearance { get set } var sharePresence: Bool { get set } + + var optimizeMediaUploads: Bool { get set } } extension AppSettings: AdvancedSettingsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift index cc7e51a561..a7a05932a1 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift @@ -11,12 +11,20 @@ import SwiftUI typealias AdvancedSettingsScreenViewModelType = StateStoreViewModel class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, AdvancedSettingsScreenViewModelProtocol { - init(advancedSettings: AdvancedSettingsProtocol) { - let bindings = AdvancedSettingsScreenViewStateBindings(advancedSettings: advancedSettings) - let state = AdvancedSettingsScreenViewState(bindings: bindings) + private let analytics: AnalyticsService + + init(advancedSettings: AdvancedSettingsProtocol, analytics: AnalyticsService) { + self.analytics = analytics + let state = AdvancedSettingsScreenViewState(bindings: .init(advancedSettings: advancedSettings)) super.init(initialViewState: state) } - override func process(viewAction: AdvancedSettingsScreenViewAction) { } + override func process(viewAction: AdvancedSettingsScreenViewAction) { + switch viewAction { + case .optimizeMediaUploadsChanged: + // Note: Using a view action here as sinking the AppSettings publisher tracks the initial value. + analytics.trackInteraction(name: state.bindings.optimizeMediaUploads ? .MobileSettingsOptimizeMediaUploadsEnabled : .MobileSettingsOptimizeMediaUploadsDisabled) + } + } } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift index 17436c310a..1c2e9fb29e 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift @@ -18,12 +18,20 @@ struct AdvancedSettingsScreen: View { kind: .picker(selection: $context.appAppearance, items: AppAppearance.allCases.map { (title: $0.name, tag: $0) })) - ListRow(label: .plain(title: L10n.actionViewSource), + ListRow(label: .plain(title: L10n.actionViewSource, + description: L10n.screenAdvancedSettingsViewSourceDescription), kind: .toggle($context.viewSourceEnabled)) ListRow(label: .plain(title: L10n.screenAdvancedSettingsSharePresence, description: L10n.screenAdvancedSettingsSharePresenceDescription), kind: .toggle($context.sharePresence)) + + ListRow(label: .plain(title: L10n.screenAdvancedSettingsMediaCompressionTitle, + description: L10n.screenAdvancedSettingsMediaCompressionDescription), + kind: .toggle($context.optimizeMediaUploads)) + .onChange(of: context.optimizeMediaUploads) { + context.send(viewAction: .optimizeMediaUploadsChanged) + } } } .compoundList() @@ -48,7 +56,8 @@ private extension AppAppearance { // MARK: - Previews struct AdvancedSettingsScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings) + static let viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings, + analytics: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { AdvancedSettingsScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 8ad26e512f..f8523ff3c1 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -44,10 +44,12 @@ protocol DeveloperOptionsProtocol: AnyObject { var logLevel: TracingConfiguration.LogLevel { get set } var slidingSyncDiscovery: AppSettings.SlidingSyncDiscovery { get set } var hideUnreadMessagesBadge: Bool { get set } - var elementCallBaseURLOverride: URL? { get set } var fuzzyRoomListSearchEnabled: Bool { get set } - var pinningEnabled: Bool { get set } - var invisibleCryptoEnabled: Bool { get set } + var hideTimelineMedia: Bool { get set } + var enableOnlySignedDeviceIsolationMode: Bool { get set } + var elementCallBaseURLOverride: URL? { get set } + var knockingEnabled: Bool { get set } + var frequentEmojisEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 7f9217f665..d9e6f2a6cd 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -35,13 +35,6 @@ struct DeveloperOptionsScreen: View { Text(context.viewState.slidingSyncFooter) } - Section("Message Pinning") { - Toggle(isOn: $context.pinningEnabled) { - Text("Enable message pinning") - Text("Requires app reboot") - } - } - Section("Room List") { Toggle(isOn: $context.hideUnreadMessagesBadge) { Text("Hide grey dots") @@ -52,15 +45,32 @@ struct DeveloperOptionsScreen: View { } } + Section("Timeline") { + Toggle(isOn: $context.hideTimelineMedia) { + Text("Hide image & video previews") + } + + Toggle(isOn: $context.frequentEmojisEnabled) { + Text("Show frequently used emojis") + } + } + + Section("Join rules") { + Toggle(isOn: $context.knockingEnabled) { + Text("Knocking") + Text("Experimental, still using mocked data") + } + } + Section { - Toggle(isOn: $context.invisibleCryptoEnabled) { - Text("Enabled Invisible Crypto") + Toggle(isOn: $context.enableOnlySignedDeviceIsolationMode) { + Text("Exclude insecure devices when sending/receiving messages") Text("Requires app reboot") } } header: { Text("Trust and Decoration") } footer: { - Text("This setting controls how end-to-end encryption (E2EE) keys are shared. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.") + Text("This setting controls how end-to-end encryption (E2EE) keys are exchanged. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.") } Section { @@ -135,18 +145,6 @@ struct DeveloperOptionsScreen: View { private struct LogLevelConfigurationView: View { @Binding var logLevel: TracingConfiguration.LogLevel - @State private var customTracingConfiguration: String - - init(logLevel: Binding) { - _logLevel = logLevel - - if case .custom(let configuration) = logLevel.wrappedValue { - customTracingConfiguration = configuration - } else { - customTracingConfiguration = TracingConfiguration(logLevel: .info, target: nil).filter - } - } - var body: some View { Picker(selection: $logLevel) { ForEach(logLevels, id: \.self) { logLevel in @@ -156,24 +154,11 @@ private struct LogLevelConfigurationView: View { Text("Log level") Text("Requires app reboot") } - - if case .custom = logLevel { - TextEditor(text: $customTracingConfiguration) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .onChange(of: customTracingConfiguration) { newValue in - logLevel = .custom(newValue) - } - } } /// Allows the picker to work with associated values private var logLevels: [TracingConfiguration.LogLevel] { - if case let .custom(filter) = logLevel { - return [.error, .warn, .info, .debug, .trace, .custom(filter)] - } else { - return [.error, .warn, .info, .debug, .trace, .custom("")] - } + [.error, .warn, .info, .debug, .trace] } } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift index c47f71a2d6..934b383798 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenViewModel.swift @@ -131,7 +131,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie for roomSummary in filteredRoomsSummary { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomSummary.id) else { continue } // `isOneToOneRoom` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. - let isOneToOneRoom = roomProxy.activeMembersCount == 2 + let isOneToOneRoom = roomProxy.infoPublisher.value.activeMembersCount == 2 // display only the rooms we're interested in switch chatType { case .oneToOneChat where isOneToOneRoom, .groupChat where !isOneToOneRoom: diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index dca3369aac..2c0319c140 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -86,7 +86,7 @@ struct NotificationSettingsScreen: View { Section { ListRow(label: .plain(title: L10n.screenNotificationSettingsEnableNotifications), kind: .toggle($context.enableNotifications)) - .onChange(of: context.enableNotifications) { _ in + .onChange(of: context.enableNotifications) { context.send(viewAction: .changedEnableNotifications) } } @@ -128,7 +128,7 @@ struct NotificationSettingsScreen: View { kind: .toggle($context.roomMentionsEnabled)) .disabled(context.viewState.settings?.roomMentionsEnabled == nil) .allowsHitTesting(!context.viewState.applyingChange) - .onChange(of: context.roomMentionsEnabled) { _ in + .onChange(of: context.roomMentionsEnabled) { context.send(viewAction: .roomMentionChanged) } } header: { @@ -143,7 +143,7 @@ struct NotificationSettingsScreen: View { kind: .toggle($context.callsEnabled)) .disabled(context.viewState.settings?.callsEnabled == nil) .allowsHitTesting(!context.viewState.applyingChange) - .onChange(of: context.callsEnabled) { _ in + .onChange(of: context.callsEnabled) { context.send(viewAction: .callsChanged) } } header: { @@ -158,7 +158,7 @@ struct NotificationSettingsScreen: View { kind: .toggle($context.invitationsEnabled)) .disabled(context.viewState.settings?.invitationsEnabled == nil) .allowsHitTesting(!context.viewState.applyingChange) - .onChange(of: context.invitationsEnabled) { _ in + .onChange(of: context.invitationsEnabled) { context.send(viewAction: .invitationsChanged) } } header: { diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index efe901dbcd..5e8d027eec 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -90,7 +90,7 @@ struct SettingsScreen: View { switch context.viewState.securitySectionMode { case .secureBackup: - ListRow(label: .default(title: L10n.commonChatBackup, + ListRow(label: .default(title: L10n.commonEncryption, icon: \.key), details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil, kind: .navigationLink { context.send(viewAction: .secureBackup) }) diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift index b4eb6e3361..bcb06ec35d 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenCoordinator.swift @@ -12,6 +12,7 @@ struct UserDetailsEditScreenCoordinatorParameters { let orientationManager: OrientationManagerProtocol let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol + let mediaUploadingPreprocessor: MediaUploadingPreprocessor weak var navigationStackCoordinator: NavigationStackCoordinator? let userIndicatorController: UserIndicatorControllerProtocol } @@ -26,6 +27,7 @@ final class UserDetailsEditScreenCoordinator: CoordinatorProtocol { viewModel = UserDetailsEditScreenViewModel(clientProxy: parameters.clientProxy, mediaProvider: parameters.mediaProvider, + mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, userIndicatorController: parameters.userIndicatorController) } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift index f83b911fda..0b03ed0d15 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift @@ -14,7 +14,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe private let actionsSubject: PassthroughSubject = .init() private let clientProxy: ClientProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol - private let mediaPreprocessor: MediaUploadingPreprocessor = .init() + private let mediaUploadingPreprocessor: MediaUploadingPreprocessor var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -22,8 +22,10 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, + mediaUploadingPreprocessor: MediaUploadingPreprocessor, userIndicatorController: UserIndicatorControllerProtocol) { self.clientProxy = clientProxy + self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.userIndicatorController = userIndicatorController super.init(initialViewState: UserDetailsEditScreenViewState(userID: clientProxy.userID, @@ -88,7 +90,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe title: L10n.commonLoading, persistent: true)) - let mediaResult = await mediaPreprocessor.processMedia(at: url) + let mediaResult = await mediaUploadingPreprocessor.processMedia(at: url) switch mediaResult { case .success(.image): diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift index ed6a29f866..953f3f96d3 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift @@ -116,7 +116,8 @@ struct UserDetailsEditScreen: View { struct UserDetailsEditScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = UserDetailsEditScreenViewModel(clientProxy: ClientProxyMock(.init(userID: "@stefan:matrix.org")), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaUploadingPreprocessor: .init(appSettings: ServiceLocator.shared.settings), userIndicatorController: UserIndicatorControllerMock.default) static var previews: some View { diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift index 56791a1a4a..2bc682ef4e 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift @@ -14,6 +14,7 @@ struct StartChatScreenCoordinatorParameters { let userIndicatorController: UserIndicatorControllerProtocol weak var navigationStackCoordinator: NavigationStackCoordinator? let userDiscoveryService: UserDiscoveryServiceProtocol + let mediaUploadingPreprocessor: MediaUploadingPreprocessor } enum StartChatScreenCoordinatorAction { @@ -134,7 +135,6 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { // MARK: - Private - let mediaUploadingPreprocessor = MediaUploadingPreprocessor() private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) { let stackCoordinator = NavigationStackCoordinator() @@ -159,7 +159,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { Task { [weak self] in guard let self else { return } do { - let media = try await mediaUploadingPreprocessor.processMedia(at: url).get() + let media = try await parameters.mediaUploadingPreprocessor.processMedia(at: url).get() var parameters = createRoomParameters.value parameters.avatarImageMedia = media createRoomParameters.send(parameters) diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index b4ad60281e..83b2bd23dd 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -91,6 +91,7 @@ class TimelineInteractionHandler { } } + // swiftlint:disable:next cyclomatic_complexity func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { @@ -132,8 +133,12 @@ class TimelineInteractionHandler { UIPasteboard.general.url = permalinkURL } case .redact: + guard case let .event(_, eventOrTransactionID) = itemID else { + fatalError() + } + Task { - await timelineController.redact(itemID) + await timelineController.redact(eventOrTransactionID) } case .reply: guard let eventID = eventTimelineItem.id.eventID else { @@ -143,7 +148,7 @@ class TimelineInteractionHandler { let replyInfo = buildReplyInfo(for: eventTimelineItem) let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type) - actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread)))) + actionsSubject.send(.composer(action: .setMode(mode: .reply(eventID: eventID, replyDetails: replyDetails, isThread: replyInfo.isThread)))) case .forward(let itemID): actionsSubject.send(.displayMessageForwarding(itemID: itemID)) case .viewSource: @@ -159,7 +164,13 @@ class TimelineInteractionHandler { case .react: displayEmojiPicker(for: itemID) case .toggleReaction(let key): - Task { await timelineController.toggleReaction(key, to: itemID) } + Task { + guard case let .event(_, eventOrTransactionID) = itemID else { + fatalError() + } + + await timelineController.toggleReaction(key, to: eventOrTransactionID) + } case .endPoll(let pollStartID): endPoll(pollStartID: pollStartID) case .pin: @@ -184,6 +195,11 @@ class TimelineInteractionHandler { } private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) { + guard case let .event(_, eventOrTransactionID) = messageTimelineItem.id else { + MXLog.error("Failed editing message, missing event id") + return + } + let text: String var htmlText: String? switch messageTimelineItem.contentType { @@ -197,7 +213,7 @@ class TimelineInteractionHandler { } // Always update the mode first and then the text so that the composer has time to save the text draft - actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) + actionsSubject.send(.composer(action: .setMode(mode: .edit(originalEventOrTransactionID: eventOrTransactionID)))) actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText))) } @@ -532,30 +548,35 @@ class TimelineInteractionHandler { private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction { var source: MediaSourceProxy? - var body: String + var filename: String + var caption: String? switch timelineItem { case let item as ImageRoomTimelineItem: source = item.content.source - body = item.content.body + filename = item.content.filename + caption = item.content.caption case let item as VideoRoomTimelineItem: source = item.content.source - body = item.content.body + filename = item.content.filename + caption = item.content.caption case let item as FileRoomTimelineItem: source = item.content.source - body = item.content.body + filename = item.content.filename + caption = item.content.caption case let item as AudioRoomTimelineItem: // For now we are just displaying audio messages with the File preview until we create a timeline player for them. source = item.content.source - body = item.content.body + filename = item.content.filename + caption = item.content.caption default: return .none } guard let source else { return .none } - switch await mediaProvider.loadFileFromSource(source, body: body) { + switch await mediaProvider.loadFileFromSource(source, filename: filename) { case .success(let file): - return .displayMediaFile(file: file, title: body) + return .displayMediaFile(file: file, title: caption ?? filename) case .failure: return .none } diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index fc64307211..363ddb9fa1 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -6,6 +6,7 @@ // import Combine +import MatrixRustSDK import OrderedCollections import SwiftUI @@ -97,6 +98,7 @@ struct TimelineViewState: BindableState { var canCurrentUserRedactSelf = false var canCurrentUserPin = false var isViewSourceEnabled: Bool + var hideTimelineMedia: Bool // The `pinnedEventIDs` are used only to determine if an item is already pinned or not. // It's updated from the room info, so it's faster than using the timeline @@ -109,6 +111,8 @@ struct TimelineViewState: BindableState { /// A closure providing the associated audio player state for an item in the timeline. var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? + + var emojiProvider: EmojiProviderProtocol } struct TimelineViewStateBindings { @@ -201,9 +205,9 @@ struct TimelineState { // These can be removed when we have full swiftUI and moved as @State values in the view var scrollToBottomPublisher = PassthroughSubject() - var itemsDictionary = OrderedDictionary() + var itemsDictionary = OrderedDictionary() - var timelineIDs: [String] { + var uniqueIDs: [TimelineUniqueId] { itemsDictionary.keys.elements } diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index b85fbb3fdb..84fde96d3b 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -7,6 +7,7 @@ import Combine import Compound +import MatrixRustSDK import SwiftUI import OrderedCollections @@ -47,7 +48,7 @@ class TimelineTableViewController: UIViewController { private let coordinator: TimelineView.Coordinator private let tableView = UITableView(frame: .zero, style: .plain) - var timelineItemsDictionary = OrderedDictionary() { + var timelineItemsDictionary = OrderedDictionary() { didSet { guard canApplySnapshot else { hasPendingItems = true @@ -125,6 +126,13 @@ class TimelineTableViewController: UIViewController { } } + var hideTimelineMedia = false { + didSet { + guard let snapshot = dataSource?.snapshot() else { return } + dataSource?.applySnapshotUsingReloadData(snapshot) + } + } + /// Used to hold an observable object that the typing indicator can use let typingMembers = TypingMembersObservableObject(members: []) @@ -138,12 +146,12 @@ class TimelineTableViewController: UIViewController { @Binding private var isScrolledToBottom: Bool - private var timelineItemsIDs: [String] { + private var timelineItemsIDs: [TimelineUniqueId] { timelineItemsDictionary.keys.elements.reversed() } /// The table's diffable data source. - private var dataSource: UITableViewDiffableDataSource? + private var dataSource: UITableViewDiffableDataSource? private var cancellables = Set() /// A publisher used to throttle back pagination requests. @@ -239,7 +247,7 @@ class TimelineTableViewController: UIViewController { private func configureDataSource() { dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, id in switch id { - case TimelineTypingIndicatorCell.reuseIdentifier: + case TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier): let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTypingIndicatorCell.reuseIdentifier, for: indexPath) guard let self else { return cell @@ -260,21 +268,19 @@ class TimelineTableViewController: UIViewController { let cell = tableView.dequeueReusableCell(withIdentifier: TimelineItemCell.reuseIdentifier, for: indexPath) guard let self, let cell = cell as? TimelineItemCell else { return cell } - // A local reference to avoid capturing self in the cell configuration. - let coordinator = self.coordinator - let viewState = timelineItemsDictionary[id] cell.item = viewState guard let viewState else { return cell } - cell.contentConfiguration = UIHostingConfiguration { + cell.contentConfiguration = UIHostingConfiguration { [coordinator, hideTimelineMedia] in RoomTimelineItemView(viewState: viewState) .id(id) .frame(maxWidth: .infinity, alignment: .leading) .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu .environment(\.timelineContext, coordinator.context) + .environment(\.shouldAutomaticallyLoadImages, !hideTimelineMedia) } .margins(.all, 0) // Margins are handled in the stylers .minSize(height: 1) @@ -307,12 +313,12 @@ class TimelineTableViewController: UIViewController { private func applySnapshot() { guard let dataSource else { return } - var snapshot = NSDiffableDataSourceSnapshot() + var snapshot = NSDiffableDataSourceSnapshot() // We don't want to display the typing notification in this timeline if !coordinator.context.viewState.isPinnedEventsTimeline { snapshot.appendSections([.typingIndicator]) - snapshot.appendItems([TimelineTypingIndicatorCell.reuseIdentifier]) + snapshot.appendItems([TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier)]) } snapshot.appendSections([.main]) snapshot.appendItems(timelineItemsIDs) @@ -401,8 +407,8 @@ class TimelineTableViewController: UIViewController { // These are already in reverse order because the table view is flipped for indexPath in visibleIndexPaths { - if let visibleItemTimelineID = dataSource?.itemIdentifier(for: indexPath), - let visibleItemID = timelineItemsDictionary[visibleItemTimelineID]?.identifier { + if let visibleItemUniqueID = dataSource?.itemIdentifier(for: indexPath), + let visibleItemID = timelineItemsDictionary[visibleItemUniqueID]?.identifier { coordinator.send(viewAction: .sendReadReceiptIfNeeded(visibleItemID)) return } @@ -488,7 +494,7 @@ extension TimelineTableViewController { /// The current layout of the table, based on the newest timeline item. private func snapshotLayout() -> Layout? { guard let newestItemID = newestVisibleItemID(), - let newestCellFrame = cellFrame(for: newestItemID.timelineID) else { + let newestCellFrame = cellFrame(for: newestItemID.uniqueID) else { return nil } return Layout(id: newestItemID, frame: newestCellFrame) @@ -496,12 +502,12 @@ extension TimelineTableViewController { /// Restores the timeline's layout from an old snapshot. private func restoreLayout(_ layout: Layout) { - if let indexPath = dataSource?.indexPath(for: layout.id.timelineID) { + if let indexPath = dataSource?.indexPath(for: layout.id.uniqueID) { // Scroll the item into view. tableView.scrollToRow(at: indexPath, at: .top, animated: false) // Remove any unwanted offset that was added by scrollToRow. - if let frame = cellFrame(for: layout.id.timelineID) { + if let frame = cellFrame(for: layout.id.uniqueID) { let deltaY = frame.maxY - layout.frame.maxY if deltaY != 0 { tableView.contentOffset.y -= deltaY @@ -511,8 +517,8 @@ extension TimelineTableViewController { } /// Returns the frame of the cell for a particular timeline item. - private func cellFrame(for id: String) -> CGRect? { - guard let timelineCell = tableView.visibleCells.first(where: { ($0 as? TimelineItemCell)?.item?.id == id }) else { + private func cellFrame(for uniqueID: TimelineUniqueId) -> CGRect? { + guard let timelineCell = tableView.visibleCells.first(where: { ($0 as? TimelineItemCell)?.item?.identifier.uniqueID == uniqueID }) else { return nil } @@ -531,13 +537,13 @@ extension TimelineTableViewController { } } -private extension NSDiffableDataSourceSnapshot { +private extension NSDiffableDataSourceSnapshot { var numberOfMainItems: Int { guard sectionIdentifiers.contains(.main) else { return 0 } return numberOfItems(inSection: .main) } - var mainItemIdentifiers: [String] { + var mainItemIdentifiers: [TimelineUniqueId] { guard sectionIdentifiers.contains(.main) else { return [] } return itemIdentifiers(inSection: .main) } diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 623b5d4121..7a7103499d 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -28,6 +28,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analyticsService: AnalyticsService + private let emojiProvider: EmojiProviderProtocol private let timelineInteractionHandler: TimelineInteractionHandler @@ -50,7 +51,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { userIndicatorController: UserIndicatorControllerProtocol, appMediator: AppMediatorProtocol, appSettings: AppSettings, - analyticsService: AnalyticsService) { + analyticsService: AnalyticsService, + emojiProvider: EmojiProviderProtocol) { self.timelineController = timelineController self.mediaPlayerProvider = mediaPlayerProvider self.roomProxy = roomProxy @@ -58,6 +60,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { self.analyticsService = analyticsService self.userIndicatorController = userIndicatorController self.appMediator = appMediator + self.emojiProvider = emojiProvider let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) @@ -78,7 +81,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), ownUserID: roomProxy.ownUserID, isViewSourceEnabled: appSettings.viewSourceEnabled, - bindings: .init(reactionsCollapsed: [:])), + hideTimelineMedia: appSettings.hideTimelineMedia, + pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs, + bindings: .init(reactionsCollapsed: [:]), + emojiProvider: emojiProvider), mediaProvider: mediaProvider) if focussedEventID != nil { @@ -86,10 +92,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { showFocusLoadingIndicator() } - Task { - await updatePinnedEventIDs() - } - setupSubscriptions() setupDirectRoomSubscriptionsIfNeeded() @@ -130,8 +132,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { Task { await handleItemTapped(with: id) } case .itemSendInfoTapped(let itemID): handleItemSendInfoTapped(itemID: itemID) - case .toggleReaction(let emoji, let itemId): - Task { await timelineController.toggleReaction(emoji, to: itemId) } + case .toggleReaction(let emoji, let itemID): + emojiProvider.markEmojiAsFrequentlyUsed(emoji) + + guard case let .event(_, eventOrTransactionID) = itemID else { + fatalError() + } + + Task { await timelineController.toggleReaction(emoji, to: eventOrTransactionID) } case .sendReadReceiptIfNeeded(let lastVisibleItemID): Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) } case .paginateBackwards: @@ -336,8 +344,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { state.canCurrentUserRedactSelf = false } - if appSettings.pinningEnabled, - case let .success(value) = await roomProxy.canUserPinOrUnpin(userID: roomProxy.ownUserID) { + if case let .success(value) = await roomProxy.canUserPinOrUnpin(userID: roomProxy.ownUserID) { state.canCurrentUserPin = value } else { state.canCurrentUserPin = false @@ -370,15 +377,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } .store(in: &cancellables) - let roomInfoSubscription = roomProxy - .actionsPublisher - .filter { $0 == .roomInfoUpdate } + let roomInfoSubscription = roomProxy.infoPublisher Task { [weak self] in - for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { + for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values { guard !Task.isCancelled else { return } - await self?.updatePinnedEventIDs() + self?.state.pinnedEventIDs = roomInfo.pinnedEventIDs await self?.updatePermissions() } } @@ -441,16 +446,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings.$viewSourceEnabled .weakAssign(to: \.state.isViewSourceEnabled, on: self) .store(in: &cancellables) - } - - private func updatePinnedEventIDs() async { - if appSettings.pinningEnabled { - state.pinnedEventIDs = await roomProxy.pinnedEventIDs - } + + appSettings.$hideTimelineMedia + .weakAssign(to: \.state.hideTimelineMedia, on: self) + .store(in: &cancellables) } private func setupDirectRoomSubscriptionsIfNeeded() { - guard roomProxy.isDirect else { + guard roomProxy.infoPublisher.value.isDirect else { return } @@ -459,7 +462,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { .map { [weak self] isFocused in guard let self else { return false } - return isFocused && self.roomProxy.isUserAloneInDirectRoom + return isFocused && self.roomProxy.infoPublisher.value.isUserAloneInDirectRoom } // We want to show the alert just once, so we are taking the first "true" emitted .first { $0 } @@ -589,15 +592,15 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } actionsSubject.send(.composer(action: .clear)) - + switch mode { - case .reply(let itemId, _, _): + case .reply(let eventID, _, _): await timelineController.sendMessage(message, html: html, - inReplyTo: itemId, + inReplyToEventID: eventID, intentionalMentions: intentionalMentions) - case .edit(let originalItemId): - await timelineController.edit(originalItemId, + case .edit(let originalEventOrTransactionID): + await timelineController.edit(originalEventOrTransactionID, message: message, html: html, intentionalMentions: intentionalMentions) @@ -608,6 +611,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .none: await timelineController.sendMessage(message, html: html, + inReplyToEventID: nil, intentionalMentions: intentionalMentions) } case .recordVoiceMessage, .previewVoiceMessage: @@ -635,7 +639,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { // MARK: - Timeline Item Building private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { - var timelineItemsDictionary = OrderedDictionary() + var timelineItemsDictionary = OrderedDictionary() timelineItems.filter { $0 is RedactedRoomTimelineItem }.forEach { timelineItem in // Stops the audio player when a voice message is redacted. @@ -662,19 +666,19 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { if itemGroup.count == 1 { if let firstItem = itemGroup.first { timelineItemsDictionary.updateValue(updateViewState(item: firstItem, groupStyle: .single), - forKey: firstItem.id.timelineID) + forKey: firstItem.id.uniqueID) } } else { for (index, item) in itemGroup.enumerated() { if index == 0 { timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .first), - forKey: item.id.timelineID) + forKey: item.id.uniqueID) } else if index == itemGroup.count - 1 { timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .last), - forKey: item.id.timelineID) + forKey: item.id.uniqueID) } else { timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: state.isPinnedEventsTimeline ? .single : .middle), - forKey: item.id.timelineID) + forKey: item.id.uniqueID) } } } @@ -688,7 +692,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState { - if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.timelineID] { + if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.uniqueID] { timelineItemViewState.groupStyle = groupStyle timelineItemViewState.type = .init(item: item) return timelineItemViewState @@ -729,7 +733,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private let inviteLoadingIndicatorID = UUID().uuidString private func inviteOtherDMUserBack() { - guard roomProxy.isUserAloneInDirectRoom else { + guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else { userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) return } @@ -835,7 +839,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } } -private extension RoomProxyProtocol { +private extension RoomInfoProxy { /// Checks if the other person left the room in a direct chat var isUserAloneInDirectRoom: Bool { isDirect && activeMembersCount == 1 @@ -848,46 +852,33 @@ extension TimelineViewModel { static let mock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, timelineController: MockRoomTimelineController(timelineKind: .pinned), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) -} - -private struct TimelineContextKey: EnvironmentKey { - @MainActor static let defaultValue: TimelineViewModel.Context? = nil -} - -private struct FocussedEventID: EnvironmentKey { - static let defaultValue: String? = nil + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) } extension EnvironmentValues { /// Used to access and inject the room context without observing it - var timelineContext: TimelineViewModel.Context? { - get { self[TimelineContextKey.self] } - set { self[TimelineContextKey.self] = newValue } - } - + @Entry var timelineContext: TimelineViewModel.Context? /// An event ID which will be non-nil when a timeline item should show as focussed. - var focussedEventID: String? { - get { self[FocussedEventID.self] } - set { self[FocussedEventID.self] = newValue } - } + @Entry var focussedEventID: String? } private enum SlashCommand: String, CaseIterable { diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 8f65d0837a..cc0b1e50be 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -309,7 +309,8 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem, let actions = TimelineItemMenuActions(isReactable: true, actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], - debugActions: [.viewSource]) else { + debugActions: [.viewSource], + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) else { return nil } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index 0ef3a10d35..a2e7de4f20 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -8,26 +8,38 @@ import SFSafeSymbols import SwiftUI +@MainActor struct TimelineItemMenuActions { let reactions: [TimelineItemMenuReaction] let actions: [TimelineItemMenuAction] let debugActions: [TimelineItemMenuAction] - init?(isReactable: Bool, actions: [TimelineItemMenuAction], debugActions: [TimelineItemMenuAction]) { + init?(isReactable: Bool, + actions: [TimelineItemMenuAction], + debugActions: [TimelineItemMenuAction], + emojiProvider: EmojiProviderProtocol) { if !isReactable, actions.isEmpty, debugActions.isEmpty { return nil } self.actions = actions self.debugActions = debugActions + + // Only process 5 of the most frequently used emojis instead of all of them + var frequentlyUsed = emojiProvider.frequentlyUsedSystemEmojis().prefix(5).map { TimelineItemMenuReaction(key: $0, symbol: .heart) } + + frequentlyUsed += [ + .init(key: "👍️", symbol: .handThumbsup), + .init(key: "👎️", symbol: .handThumbsdown), + .init(key: "🔥", symbol: .flame), + .init(key: "❤️", symbol: .heart), + .init(key: "👏", symbol: .handsClap) + ] + + frequentlyUsed = Array(frequentlyUsed.prefix(5)) + reactions = if isReactable { - [ - .init(key: "👍️", symbol: .handThumbsup), - .init(key: "👎️", symbol: .handThumbsdown), - .init(key: "🔥", symbol: .flame), - .init(key: "❤️", symbol: .heart), - .init(key: "👏", symbol: .handsClap) - ] + frequentlyUsed } else { [] } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index f3b79fb5df..9fcc47575a 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor struct TimelineItemMenuActionProvider { let timelineItem: RoomTimelineItemProtocol let canCurrentUserRedactSelf: Bool @@ -16,6 +17,7 @@ struct TimelineItemMenuActionProvider { let isDM: Bool let isViewSourceEnabled: Bool let isPinnedEventsTimeline: Bool + let emojiProvider: EmojiProviderProtocol // swiftlint:disable:next cyclomatic_complexity func makeActions() -> TimelineItemMenuActions? { @@ -42,7 +44,10 @@ struct TimelineItemMenuActionProvider { break } - return .init(isReactable: false, actions: [.copyPermalink], debugActions: debugActions) + return .init(isReactable: false, + actions: [.copyPermalink], + debugActions: debugActions, + emojiProvider: emojiProvider) } var actions: [TimelineItemMenuAction] = [] @@ -100,7 +105,10 @@ struct TimelineItemMenuActionProvider { actions = actions.filter(\.canAppearInPinnedEventsTimeline) } - return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, actions: actions, debugActions: debugActions) + return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, + actions: actions, + debugActions: debugActions, + emojiProvider: emojiProvider) } private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool { diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift index 033e3c084e..e1328e605f 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift @@ -61,18 +61,18 @@ struct ReadReceiptCell_Previews: PreviewProvider, TestablePreview { formattedTimestamp: "10:00"), memberState: .init(displayName: "Test", avatarURL: nil), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .previewDisplayName("No Image") ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", formattedTimestamp: "10:00"), memberState: .init(displayName: "Test", avatarURL: URL.documentsDirectory), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .previewDisplayName("With Image") ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", formattedTimestamp: "10:00"), memberState: nil, - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) .previewDisplayName("Loading Member") } } diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 50294df598..2de58b9edd 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -45,13 +45,14 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members)) let mock = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: UserIndicatorControllerMock(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) return mock }() diff --git a/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift index 0a1b88bd8c..6326e84a32 100644 --- a/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift @@ -26,8 +26,8 @@ struct TimelineReplyView: View { switch content { case .audio(let content): ReplyView(sender: sender, - plainBody: content.body, - formattedBody: nil, + plainBody: content.caption ?? content.filename, + formattedBody: content.formattedCaption, icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii)) case .emote(let content): ReplyView(sender: sender, @@ -35,13 +35,13 @@ struct TimelineReplyView: View { formattedBody: content.formattedBody) case .file(let content): ReplyView(sender: sender, - plainBody: content.body, - formattedBody: nil, + plainBody: content.caption ?? content.filename, + formattedBody: content.formattedCaption, icon: .init(kind: .icon(\.document), cornerRadii: iconCornerRadii)) case .image(let content): ReplyView(sender: sender, - plainBody: content.body, - formattedBody: nil, + plainBody: content.caption ?? content.filename, + formattedBody: content.formattedCaption, icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii)) case .notice(let content): ReplyView(sender: sender, @@ -53,8 +53,8 @@ struct TimelineReplyView: View { formattedBody: content.formattedBody) case .video(let content): ReplyView(sender: sender, - plainBody: content.body, - formattedBody: nil, + plainBody: content.caption ?? content.filename, + formattedBody: content.formattedCaption, icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) }) case .voice: ReplyView(sender: sender, @@ -247,7 +247,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", - eventContent: .message(.audio(.init(body: "Some audio", + eventContent: .message(.audio(.init(filename: "audio.m4a", + caption: "Some audio", duration: 0, waveform: nil, source: nil, @@ -256,7 +257,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", - eventContent: .message(.file(.init(body: "Some file", + eventContent: .message(.file(.init(filename: "file.txt", + caption: "Some file", source: nil, thumbnailSource: nil, contentType: nil))))), @@ -264,14 +266,16 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", - eventContent: .message(.image(.init(body: "Some image", + eventContent: .message(.image(.init(filename: "image.jpg", + caption: "Some image", source: imageSource, thumbnailSource: imageSource))))), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", - eventContent: .message(.video(.init(body: "Some video", + eventContent: .message(.video(.init(filename: "video.mp4", + caption: "Some video", duration: 0, source: nil, thumbnailSource: imageSource))))), @@ -283,7 +287,8 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", - eventContent: .message(.voice(.init(body: "Some voice message", + eventContent: .message(.voice(.init(filename: "voice-message.ogg", + caption: "Some voice message", duration: 0, waveform: nil, source: nil, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift index fb4520621e..476ee8465f 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift @@ -19,7 +19,7 @@ struct SwipeToReplyView: View { } struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview { - static let timelineItem = TextRoomTimelineItem(id: .init(timelineID: ""), + static let timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: true, isEditable: true, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 32b33c57a8..61ed78ee82 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -148,7 +148,8 @@ struct TimelineItemBubbledStylerView: View { pinnedEventIDs: context.viewState.pinnedEventIDs, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline, + emojiProvider: context.viewState.emojiProvider) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action)) } @@ -257,7 +258,7 @@ private extension EventBasedTimelineItemProtocol { switch self { case is ImageRoomTimelineItem, is VideoRoomTimelineItem: // In case a reply detail or a thread decorator is present we render the color and the padding - return self.replyDetails != nil || self.isThreaded ? defaultColor : nil + return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultColor : nil default: return defaultColor } @@ -283,8 +284,7 @@ private extension EventBasedTimelineItemProtocol { // In case a reply detail or a thread decorator is present we render the color and the padding case is ImageRoomTimelineItem, is VideoRoomTimelineItem: - return self.replyDetails != nil || - self.isThreaded ? defaultInsets : .zero + return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultInsets : .zero case let locationTimelineItem as LocationRoomTimelineItem: return locationTimelineItem.content.geoURI == nil || self.replyDetails != nil || @@ -355,19 +355,18 @@ private extension View { struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock static let viewModelWithPins: TimelineViewModel = { - var settings = AppSettings() - settings.pinningEnabled = true - let roomProxy = JoinedRoomProxyMock(.init(name: "Preview Room", pinnedEventIDs: [""])) + let roomProxy = JoinedRoomProxyMock(.init(name: "Preview Room", pinnedEventIDs: ["pinned"])) return TimelineViewModel(roomProxy: roomProxy, focussedEventID: nil, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, - appSettings: settings, - analyticsService: ServiceLocator.shared.analytics) + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) }() static var previews: some View { @@ -380,120 +379,14 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview .previewDisplayName("Replies") threads .previewDisplayName("Thread decorator") + .snapshotPreferences(delay: 1) encryptionAuthenticity .previewDisplayName("Encryption Indicators") pinned .previewDisplayName("Pinned messages") - .snapshotPreferences(delay: 1.0) + .snapshotPreferences(delay: 1) } - // These always include a reply - static var threads: some View { - ScrollView { - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: "whoever"), - content: .init(body: "A long message that should be on multiple lines."), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short"))))), - groupStyle: .single)) - - AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: ""), - content: .init(body: "audio.ogg", - duration: 100, - waveform: EstimatedWaveform.mockWaveform, - source: nil, - contentType: nil), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short")))))) - - FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), - timestamp: "10:42", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: ""), - content: .init(body: "File", - source: nil, - thumbnailSource: nil, - contentType: nil), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short")))))) - ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: true, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: ""), - content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short")))))) - LocationRoomTimelineView(timelineItem: .init(id: .random, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: "Bob"), - content: .init(body: "Fallback geo uri description", - geoURI: .init(latitude: 41.902782, - longitude: 12.496366), - description: "Location description description description description description description description description"), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short")))))) - LocationRoomTimelineView(timelineItem: .init(id: .random, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: "Bob"), - content: .init(body: "Fallback geo uri description", - geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short")))))) - - VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: true, - sender: .init(id: ""), - content: .init(body: "audio.ogg", - duration: 100, - waveform: EstimatedWaveform.mockWaveform, - source: nil, - contentType: nil), - replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - eventID: "123", - eventContent: .message(.text(.init(body: "Short"))))), - playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), - title: L10n.commonVoiceMessage, - duration: 10, - waveform: EstimatedWaveform.mockWaveform)) - } - .environmentObject(viewModel.context) - } - static var mockTimeline: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { @@ -504,10 +397,10 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview } .environmentObject(viewModel.context) } - + static var replies: some View { VStack(spacing: 0) { - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: true, isEditable: false, @@ -520,7 +413,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview eventContent: .message(.text(.init(body: "Short"))))), groupStyle: .single)) - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: true, isEditable: false, @@ -535,10 +428,24 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview } .environmentObject(viewModel.context) } - + + static var threads: some View { + ScrollView { + MockTimelineContent(isThreaded: true) + } + .environmentObject(viewModel.context) + } + + static var pinned: some View { + ScrollView { + MockTimelineContent(isPinned: true) + } + .environmentObject(viewModelWithPins.context) + } + static var encryptionAuthenticity: some View { VStack(spacing: 0) { - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: true, isEditable: false, @@ -549,7 +456,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))), groupStyle: .single)) - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: true, isEditable: false, @@ -561,7 +468,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview encryptionAuthenticity: .unsignedDevice(color: .red))), groupStyle: .single)) - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: false, isEditable: false, @@ -572,7 +479,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))), groupStyle: .first)) - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent, timestamp: "10:42", isOutgoing: false, isEditable: false, @@ -583,127 +490,147 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), groupStyle: .last)) - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), + content: .init(filename: "other.png", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), + thumbnailSource: nil), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray)))) - VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), + VoiceMessageRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "10:42", isOutgoing: true, isEditable: false, canBeRepliedTo: true, isThreaded: true, sender: .init(id: ""), - content: .init(body: "audio.ogg", + content: .init(filename: "audio.ogg", duration: 100, waveform: EstimatedWaveform.mockWaveform, source: nil, contentType: nil), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), - playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), + playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: L10n.commonVoiceMessage, duration: 10, waveform: EstimatedWaveform.mockWaveform)) } .environmentObject(viewModel.context) } - - static var pinned: some View { - ScrollView { - RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: "", eventID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "whoever"), - content: .init(body: "A long message that should be on multiple lines."), - replyDetails: nil), - groupStyle: .single)) +} - AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: ""), - content: .init(body: "audio.ogg", - duration: 100, - waveform: EstimatedWaveform.mockWaveform, - source: nil, - contentType: nil), - replyDetails: nil)) - - FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "10:42", +private struct MockTimelineContent: View { + var isThreaded = false + var isPinned = false + + var body: some View { + RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: makeItemIdentifier(), + timestamp: "10:42", + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + isThreaded: isThreaded, + sender: .init(id: "whoever"), + content: .init(body: "A long message that should be on multiple lines."), + replyDetails: replyDetails), + groupStyle: .single)) + + AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "10:42", + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + isThreaded: isThreaded, + sender: .init(id: ""), + content: .init(filename: "audio.ogg", + duration: 100, + waveform: EstimatedWaveform.mockWaveform, + source: nil, + contentType: nil), + replyDetails: replyDetails)) + + FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "10:42", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: isThreaded, + sender: .init(id: ""), + content: .init(filename: "file.txt", + caption: "File", + source: nil, + thumbnailSource: nil, + contentType: nil), + replyDetails: replyDetails)) + + ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "10:42", + isOutgoing: true, + isEditable: true, + canBeRepliedTo: true, + isThreaded: isThreaded, + sender: .init(id: ""), + content: .init(filename: "image.jpg", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), + thumbnailSource: nil), + replyDetails: replyDetails)) + + LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: ""), - content: .init(body: "File", - source: nil, - thumbnailSource: nil, - contentType: nil), - replyDetails: nil)) - ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: true, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: ""), - content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), - replyDetails: nil)) - LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(body: "Fallback geo uri description", - geoURI: .init(latitude: 41.902782, - longitude: 12.496366), - description: "Location description description description description description description description description"), - replyDetails: nil)) - LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "Now", - isOutgoing: false, + isThreaded: isThreaded, + sender: .init(id: "Bob"), + content: .init(body: "Fallback geo uri description", + geoURI: .init(latitude: 41.902782, + longitude: 12.496366), + description: "Location description description description description description description description description"), + replyDetails: replyDetails)) + + LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: isThreaded, + sender: .init(id: "Bob"), + content: .init(body: "Fallback geo uri description", + geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil), + replyDetails: replyDetails)) + + VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), + timestamp: "10:42", + isOutgoing: true, isEditable: false, canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(body: "Fallback geo uri description", - geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil), - replyDetails: nil)) - - VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: "", eventID: ""), - timestamp: "10:42", - isOutgoing: true, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: ""), - content: .init(body: "audio.ogg", - duration: 100, - waveform: EstimatedWaveform.mockWaveform, - source: nil, - contentType: nil), - replyDetails: nil), - playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), - title: L10n.commonVoiceMessage, - duration: 10, - waveform: EstimatedWaveform.mockWaveform)) - } - .environmentObject(viewModelWithPins.context) + isThreaded: isThreaded, + sender: .init(id: ""), + content: .init(filename: "audio.ogg", + duration: 100, + waveform: EstimatedWaveform.mockWaveform, + source: nil, + contentType: nil), + replyDetails: replyDetails), + playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), + title: L10n.commonVoiceMessage, + duration: 10, + waveform: EstimatedWaveform.mockWaveform)) + } + + func makeItemIdentifier() -> TimelineItemIdentifier { + isPinned ? .event(uniqueID: .init(id: ""), eventOrTransactionID: .eventId(eventId: "pinned")) : .randomEvent + } + + var replyDetails: TimelineItemReplyDetails? { + isThreaded ? .loaded(sender: .init(id: "", displayName: "Alice"), + eventID: "123", + eventContent: .message(.text(.init(body: "Short")))) : nil } } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index 04e5544a43..b350a9b2e7 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -152,9 +152,9 @@ private extension TimelineItemSendInfo { layoutType = switch timelineItem { case is TextBasedRoomTimelineItem: .overlay(capsuleStyle: false) - case is ImageRoomTimelineItem, - is VideoRoomTimelineItem, - is StickerRoomTimelineItem: + case let message as EventBasedMessageTimelineItemProtocol where message is ImageRoomTimelineItem || message is VideoRoomTimelineItem: + .overlay(capsuleStyle: !message.hasMediaCaption) + case is StickerRoomTimelineItem: .overlay(capsuleStyle: true) case let locationTimelineItem as LocationRoomTimelineItem: .overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil) @@ -180,22 +180,22 @@ private extension EncryptionAuthenticity { struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview { static var previews: some View { VStack(spacing: 16) { - TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, + TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent, localizedString: "09:47 AM", layoutType: .horizontal())) - TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, + TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent, localizedString: "09:47 AM", status: .sendingFailed, layoutType: .horizontal())) - TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, + TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent, localizedString: "09:47 AM", status: .encryptionAuthenticity(.unsignedDevice(color: .red)), layoutType: .horizontal())) - TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, + TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent, localizedString: "09:47 AM", status: .encryptionAuthenticity(.notGuaranteed(color: .gray)), layoutType: .horizontal())) - TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, + TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent, localizedString: "09:47 AM", status: .encryptionAuthenticity(.sentInClear(color: .red)), layoutType: .horizontal())) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift index b8294a041c..7726cee0f3 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift @@ -26,13 +26,6 @@ enum TimelineGroupStyle: Hashable { // MARK: - Environment -private struct TimelineGroupStyleKey: EnvironmentKey { - static let defaultValue = TimelineGroupStyle.single -} - extension EnvironmentValues { - var timelineGroupStyle: TimelineGroupStyle { - get { self[TimelineGroupStyleKey.self] } - set { self[TimelineGroupStyleKey.self] = newValue } - } + @Entry var timelineGroupStyle: TimelineGroupStyle = .single } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index 4d09e9216b..f69da64db7 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -25,7 +25,7 @@ struct TimelineStyler: View { var body: some View { mainContent - .onChange(of: timelineItem.properties.deliveryStatus) { newStatus in + .onChange(of: timelineItem.properties.deliveryStatus) { _, newStatus in if case .sendingFailed = newStatus { guard task == nil else { return @@ -57,7 +57,7 @@ struct TimelineStyler: View { struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - static let base = TextRoomTimelineItem(id: .random, + static let base = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -79,8 +79,8 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { }() static let sendingLast: TextRoomTimelineItem = { - let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString - var result = TextRoomTimelineItem(id: .init(timelineID: id), + let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString) + var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), timestamp: "Now", isOutgoing: true, isEditable: false, @@ -99,8 +99,8 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { }() static let sentLast: TextRoomTimelineItem = { - let id = viewModel.state.timelineViewState.timelineIDs.last ?? UUID().uuidString - let result = TextRoomTimelineItem(id: .init(timelineID: id), + let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString) + let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), timestamp: "Now", isOutgoing: true, isEditable: false, @@ -111,7 +111,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { return result }() - static let ltrString = TextRoomTimelineItem(id: .random, + static let ltrString = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -119,7 +119,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .test, content: .init(body: "house!")) - static let rtlString = TextRoomTimelineItem(id: .random, + static let rtlString = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -127,7 +127,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .test, content: .init(body: "באמת!")) - static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, + static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "house! -- באמת‏! -- house!")) - static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, + static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -145,7 +145,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "באמת‏! -- house! -- באמת!")) - static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, + static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, @@ -154,7 +154,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "house! -- באמת!")) - static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, + static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift index 5f535f7063..fccaf576c3 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift @@ -13,6 +13,9 @@ struct ReactionsSummaryView: View { let mediaProvider: MediaProviderProtocol? @State var selectedReactionKey: String + private var selectedReaction: AggregatedReaction? { + reactions.first { $0.key == selectedReactionKey } + } var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -40,7 +43,7 @@ struct ReactionsSummaryView: View { scrollView.scrollTo(selectedReactionKey) } } - .onChange(of: selectedReactionKey) { _ in + .onChange(of: selectedReactionKey) { scrollView.scrollTo(selectedReactionKey) } } @@ -49,19 +52,17 @@ struct ReactionsSummaryView: View { .padding(.bottom, 12) } + @ViewBuilder private var sendersList: some View { - TabView(selection: $selectedReactionKey) { - ForEach(reactions) { reaction in - ScrollView { - VStack(alignment: .leading, spacing: 8) { - ForEach(reaction.senders) { sender in - ReactionSummarySenderView(sender: sender, member: members[sender.id], mediaProvider: mediaProvider) - .padding(.horizontal, 16) - } + if let selectedReaction { + ScrollView { + VStack(alignment: .leading, spacing: 8) { + ForEach(selectedReaction.senders) { sender in + ReactionSummarySenderView(sender: sender, member: members[sender.id], mediaProvider: mediaProvider) + .padding(.horizontal, 16) } - .frame(maxWidth: .infinity) } - .tag(reaction.key) + .frame(maxWidth: .infinity) } } } @@ -138,7 +139,7 @@ struct ReactionsSummaryView_Previews: PreviewProvider, TestablePreview { static var previews: some View { ReactionsSummaryView(reactions: AggregatedReaction.mockReactions, members: [:], - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), selectedReactionKey: AggregatedReaction.mockReactions[0].key) } } diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift index 071351cc50..1729d149d6 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift @@ -14,7 +14,7 @@ struct TimelineItemStatusView: View { @EnvironmentObject private var context: TimelineViewModel.Context private var isLastOutgoingMessage: Bool { - timelineItem.isOutgoing && context.viewState.timelineViewState.timelineIDs.last == timelineItem.id.timelineID + timelineItem.isOutgoing && context.viewState.timelineViewState.uniqueIDs.last == timelineItem.id.uniqueID } var body: some View { diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift index 50b2459e15..a49631c3ba 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift @@ -196,20 +196,20 @@ struct TimelineReactionViewPreviewsContainer: View { var body: some View { VStack(spacing: 8) { TimelineReactionsView(context: TimelineViewModel.mock.context, - itemID: .init(timelineID: "1"), + itemID: .randomEvent, reactions: [AggregatedReaction.mockReactionWithLongText, AggregatedReaction.mockReactionWithLongTextRTL]) Divider() TimelineReactionsView(context: TimelineViewModel.mock.context, - itemID: .init(timelineID: "2"), + itemID: .randomEvent, reactions: Array(AggregatedReaction.mockReactions.prefix(3))) Divider() TimelineReactionsView(context: TimelineViewModel.mock.context, - itemID: .init(timelineID: "3"), + itemID: .randomEvent, reactions: AggregatedReaction.mockReactions) Divider() TimelineReactionsView(context: TimelineViewModel.mock.context, - itemID: .init(timelineID: "4"), + itemID: .randomEvent, reactions: AggregatedReaction.mockReactions, isLayoutRTL: true) } diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index 16d63650b6..6f37e7f56e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -83,13 +83,14 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)), timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), @@ -103,7 +104,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { - TextRoomTimelineItem(id: .random, + TextRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: true, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift index a116d81efe..b89ec72f76 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift @@ -34,13 +34,17 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview { } static var body: some View { - AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .random, + AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "audio.ogg", duration: 300, waveform: nil, source: nil, contentType: nil))) + content: .init(filename: "audio.ogg", + duration: 300, + waveform: nil, + source: nil, + contentType: nil))) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift index bb0406cb74..3beab6197c 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift @@ -30,7 +30,7 @@ struct CallInviteRoomTimelineView_Previews: PreviewProvider, TestablePreview { } static var body: some View { - CallInviteRoomTimelineView(timelineItem: .init(id: .random, + CallInviteRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "Now", isEditable: false, canBeRepliedTo: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift index f3eefcd1af..c584b8a34f 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift @@ -60,7 +60,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi } static var body: some View { - CallNotificationRoomTimelineView(timelineItem: .init(id: .random, + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "Now", isEditable: false, canBeRepliedTo: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift index 9e03a81794..d7b85ce801 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift @@ -52,8 +52,8 @@ struct CollapsibleRoomTimelineView: View { struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let item = CollapsibleTimelineItem(items: [ - SeparatorRoomTimelineItem(id: .init(timelineID: "First separator"), text: "This is a separator"), - SeparatorRoomTimelineItem(id: .init(timelineID: "Second separator"), text: "This is another separator") + SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "First separator")), text: "This is a separator"), + SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Second separator")), text: "This is another separator") ]) static var previews: some View { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift index 77ce3bec5a..3fd741ccd4 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift @@ -42,7 +42,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem { - EmoteRoomTimelineItem(id: .random, + EmoteRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift index 5a69efe464..4febbbdc3a 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift @@ -17,7 +17,9 @@ struct EncryptedRoomTimelineView: View { switch cause { case .unknown: return \.time - case .membership: + case .sentBeforeWeJoined, + .verificationViolation, + .insecureDevice: return \.block } default: @@ -77,7 +79,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { - EncryptedRoomTimelineItem(id: .random, + EncryptedRoomTimelineItem(id: .randomEvent, body: text, encryptionType: .unknown, timestamp: timestamp, @@ -88,9 +90,9 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func expectedItemWith(timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { - EncryptedRoomTimelineItem(id: .random, + EncryptedRoomTimelineItem(id: .randomEvent, body: L10n.commonUnableToDecryptNoAccess, - encryptionType: .megolmV1AesSha2(sessionID: "foo", cause: .membership), + encryptionType: .megolmV1AesSha2(sessionID: "foo", cause: .sentBeforeWeJoined), timestamp: timestamp, isOutgoing: isOutgoing, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift index faf03fcf13..d9b3ccd4d5 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift @@ -35,32 +35,41 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var body: some View { VStack(spacing: 20.0) { - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil))) + content: .init(filename: "document.pdf", + source: nil, + thumbnailSource: nil, + contentType: nil))) - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil))) + content: .init(filename: "document.docx", + source: nil, + thumbnailSource: nil, + contentType: nil))) - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "document.txt", source: nil, thumbnailSource: nil, contentType: nil))) + content: .init(filename: "document.txt", + source: nil, + thumbnailSource: nil, + contentType: nil))) } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index 9b61793ff9..cb5695bdff 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -90,13 +90,14 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, focussedEventID: focussedEventID, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift index b872a84973..63cb9b04ef 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift @@ -12,17 +12,35 @@ struct ImageRoomTimelineView: View { @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: ImageRoomTimelineItem + var hasMediaCaption: Bool { timelineItem.content.caption != nil } + var body: some View { TimelineStyler(timelineItem: timelineItem) { - LoadableImage(mediaSource: source, - blurhash: timelineItem.content.blurhash, - mediaProvider: context.mediaProvider) { - placeholder + VStack(alignment: .leading, spacing: 4) { + LoadableImage(mediaSource: source, + mediaType: .timelineItem, + blurhash: timelineItem.content.blurhash, + mediaProvider: context.mediaProvider) { + placeholder + } + .timelineMediaFrame(height: timelineItem.content.height, + aspectRatio: timelineItem.content.aspectRatio) + .accessibilityElement(children: .ignore) + .accessibilityLabel(L10n.commonImage) + // This clip shape is distinct from the one in the styler as that one + // operates on the entire message so wouldn't round the bottom corners. + .clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0)) + + if let attributedCaption = timelineItem.content.formattedCaption { + FormattedBodyText(attributedString: attributedCaption, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostEmojiSize: true) + } else if let caption = timelineItem.content.caption { + FormattedBodyText(text: caption, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostEmojiSize: true) + } } - .timelineMediaFrame(height: timelineItem.content.height, - aspectRatio: timelineItem.content.aspectRatio) - .accessibilityElement(children: .ignore) - .accessibilityLabel(L10n.commonImage) } } @@ -35,14 +53,9 @@ struct ImageRoomTimelineView: View { } var placeholder: some View { - ZStack { - Rectangle() - .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) - .opacity(0.3) - - ProgressView(L10n.commonLoading) - .frame(maxWidth: .infinity) - } + Rectangle() + .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) + .opacity(0.3) } } @@ -56,37 +69,58 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var body: some View { VStack(spacing: 20.0) { - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) + content: .init(filename: "image.jpg", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"), + thumbnailSource: nil))) - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) + content: .init(filename: "other.png", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), + thumbnailSource: nil))) - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Blurhashed image", + content: .init(filename: "Blurhashed.jpg", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil, aspectRatio: 0.7, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", contentType: .gif))) + + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "Bob"), + content: .init(filename: "Blurhashed.jpg", + caption: "This is a great image 😎", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), + thumbnailSource: nil, + width: 50, + height: 50, + aspectRatio: 1, + blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", + contentType: .gif))) } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift index 8c32b487e3..6d3752dbd2 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -86,7 +86,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { @ViewBuilder static var states: some View { - LocationRoomTimelineView(timelineItem: .init(id: .random, + LocationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, @@ -95,7 +95,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { sender: .init(id: "Bob"), content: .init(body: "Fallback geo uri description"))) - LocationRoomTimelineView(timelineItem: .init(id: .random, + LocationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, @@ -104,7 +104,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { sender: .init(id: "Bob"), content: .init(body: "Fallback geo uri description", geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"))) - LocationRoomTimelineView(timelineItem: .init(id: .random, + LocationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift index 9fe8f8fbba..098d8b93cd 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift @@ -54,7 +54,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { - NoticeRoomTimelineItem(id: .random, + NoticeRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift index 5c6f5c58bf..4314885116 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift @@ -29,12 +29,12 @@ struct ReadMarkerRoomTimelineView: View { struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - static let item = ReadMarkerRoomTimelineItem(id: .init(timelineID: .init(UUID().uuidString))) + static let item = ReadMarkerRoomTimelineItem(id: .randomVirtual) static var previews: some View { VStack(alignment: .leading, spacing: 0) { - RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single)) - RoomTimelineItemView(viewState: .init(type: .text(.init(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single)) + RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent, timestamp: "", isOutgoing: true, isEditable: false, @@ -45,8 +45,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { ReadMarkerRoomTimelineView(timelineItem: item) - RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single)) - RoomTimelineItemView(viewState: .init(type: .text(.init(id: .init(timelineID: ""), + RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single)) + RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift index 043c80b88c..81274a73f1 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift @@ -33,7 +33,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { - RedactedRoomTimelineItem(id: .random, + RedactedRoomTimelineItem(id: .randomEvent, body: text, timestamp: timestamp, isOutgoing: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift index 0f74079076..88633b6904 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift @@ -23,7 +23,8 @@ struct SeparatorRoomTimelineView: View { struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var previews: some View { - let item = SeparatorRoomTimelineItem(id: .init(timelineID: "Separator"), text: "This is a separator") + let item = SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Separator")), + text: "This is a separator") SeparatorRoomTimelineView(timelineItem: item) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift index 47d1a0e055..636e89a513 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift @@ -30,7 +30,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider, TestablePreview { StateRoomTimelineView(timelineItem: item) } - static let item = StateRoomTimelineItem(id: .random, + static let item = StateRoomTimelineItem(id: .randomVirtual, body: "Alice joined", timestamp: "Now", isOutgoing: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift index 720d5c49f2..65f155f53a 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift @@ -15,6 +15,7 @@ struct StickerRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { LoadableImage(url: timelineItem.imageURL, + mediaType: .timelineItem, blurhash: timelineItem.blurhash, mediaProvider: context.mediaProvider) { placeholder @@ -27,14 +28,9 @@ struct StickerRoomTimelineView: View { } private var placeholder: some View { - ZStack { - Rectangle() - .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) - .opacity(0.3) - - ProgressView(L10n.commonLoading) - .frame(maxWidth: .infinity) - } + Rectangle() + .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) + .opacity(0.3) } } @@ -47,7 +43,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var body: some View { VStack(spacing: 20.0) { - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, body: "Some image", timestamp: "Now", isOutgoing: false, @@ -56,7 +52,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { sender: .init(id: "Bob"), imageURL: URL.picturesDirectory)) - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, body: "Some other image", timestamp: "Now", isOutgoing: false, @@ -65,7 +61,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { sender: .init(id: "Bob"), imageURL: URL.picturesDirectory)) - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, body: "Blurhashed image", timestamp: "Now", isOutgoing: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift index 9c0b6cecac..ec853d579b 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift @@ -80,7 +80,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { - TextRoomTimelineItem(id: .random, + TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isOutgoing, @@ -94,7 +94,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) let attributedString = builder.fromHTML(html) - return TextRoomTimelineItem(id: .random, + return TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isOutgoing, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift index 0f5da6dee3..2d26302ba5 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift @@ -52,7 +52,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider, TestablePreview { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem { - UnsupportedRoomTimelineItem(id: .random, + UnsupportedRoomTimelineItem(id: .randomEvent, body: text, eventType: "Some Event Type", error: "Something went wrong", diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift index c681f0e3be..27b0ddd69e 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift @@ -12,13 +12,30 @@ struct VideoRoomTimelineView: View { @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: VideoRoomTimelineItem + private var hasMediaCaption: Bool { timelineItem.content.caption != nil } + var body: some View { TimelineStyler(timelineItem: timelineItem) { - thumbnail - .timelineMediaFrame(height: timelineItem.content.height, - aspectRatio: timelineItem.content.aspectRatio) - .accessibilityElement(children: .ignore) - .accessibilityLabel(L10n.commonVideo) + VStack(alignment: .leading, spacing: 4) { + thumbnail + .timelineMediaFrame(height: timelineItem.content.height, + aspectRatio: timelineItem.content.aspectRatio) + .accessibilityElement(children: .ignore) + .accessibilityLabel(L10n.commonVideo) + // This clip shape is distinct from the one in the styler as that one + // operates on the entire message so wouldn't round the bottom corners. + .clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0)) + + if let attributedCaption = timelineItem.content.formattedCaption { + FormattedBodyText(attributedString: attributedCaption, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostEmojiSize: true) + } else if let caption = timelineItem.content.caption { + FormattedBodyText(text: caption, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostEmojiSize: true) + } + } } } @@ -26,6 +43,7 @@ struct VideoRoomTimelineView: View { var thumbnail: some View { if let thumbnailSource = timelineItem.content.thumbnailSource { LoadableImage(mediaSource: thumbnailSource, + mediaType: .timelineItem, blurhash: timelineItem.content.blurhash, mediaProvider: context.mediaProvider) { imageView in imageView @@ -47,14 +65,9 @@ struct VideoRoomTimelineView: View { } var placeholder: some View { - ZStack { - Rectangle() - .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) - .opacity(0.3) - - ProgressView(L10n.commonLoading) - .frame(maxWidth: .infinity) - } + Rectangle() + .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) + .opacity(0.3) } } @@ -67,32 +80,56 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var body: some View { VStack(spacing: 20.0) { - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil))) + content: .init(filename: "video.mp4", + duration: 21, + source: nil, + thumbnailSource: nil))) - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "Bob"), + content: .init(filename: "other.mp4", + duration: 22, + source: nil, + thumbnailSource: nil))) + + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil))) + content: .init(filename: "Blurhashed.mp4", + duration: 23, + source: nil, + thumbnailSource: nil, + aspectRatio: 0.7, + blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))) - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, timestamp: "Now", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "Blurhashed video", duration: 23, source: nil, thumbnailSource: nil, aspectRatio: 0.7, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))) + content: .init(filename: "video.mp4", + caption: "This is a caption", + duration: 21, + source: nil, + thumbnailSource: nil))) } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index c7dbde17d1..5315b57884 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -60,6 +60,9 @@ struct TimelineView: UIViewControllerRepresentable { if tableViewController.focussedEvent != context.viewState.timelineViewState.focussedEvent { tableViewController.focussedEvent = context.viewState.timelineViewState.focussedEvent } + if tableViewController.hideTimelineMedia != context.viewState.hideTimelineMedia { + tableViewController.hideTimelineMedia = context.viewState.hideTimelineMedia + } if tableViewController.typingMembers.members != context.viewState.typingMembers { tableViewController.setTypingMembers(context.viewState.typingMembers) @@ -80,13 +83,14 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { static let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) static let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift b/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift index da3d805e2c..e927cda08d 100644 --- a/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift @@ -19,7 +19,7 @@ struct TypingIndicatorView: View { .truncationMode(.middle) .padding(.horizontal, 4) .animation(.elementDefault, value: typingMembers.members) - .onChange(of: typingMembers.members) { newValue in + .onChange(of: typingMembers.members) { _, newValue in if !newValue.isEmpty { didShowTextOnce = true } diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift index 83a95a65fa..3497c33192 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift @@ -19,10 +19,19 @@ struct UserProfileScreenViewState: BindableState { let isPresentedModally: Bool var userProfile: UserProfileProxy? + var isVerified: Bool? var permalink: URL? var dmRoomID: String? var bindings: UserProfileScreenViewStateBindings + + var showVerifiedBadge: Bool { + isVerified == true // We purposely show the badge on your own account for consistency with Web. + } + + var showVerificationSection: Bool { + isVerified == false && !isOwnUser + } } struct UserProfileScreenViewStateBindings { @@ -33,7 +42,7 @@ struct UserProfileScreenViewStateBindings { } enum UserProfileScreenViewAction { - case displayAvatar + case displayAvatar(URL) case openDirectChat case startCall(roomID: String) case dismiss diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index 8abf9be3e2..01a0f10337 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -42,24 +42,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr showLoadingIndicator(allowsInteraction: true) Task { - defer { - hideLoadingIndicator() - } - - switch await clientProxy.profile(for: userID) { - case .success(let userProfile): - state.userProfile = userProfile - state.permalink = (try? matrixToUserPermalink(userId: userID)).flatMap(URL.init(string:)) - switch await clientProxy.directRoomForUserID(userProfile.userID) { - case .success(let roomID): - state.dmRoomID = roomID - case .failure: - break - } - case .failure(let error): - state.bindings.alertInfo = .init(id: .unknown) - MXLog.error("Failed to find user profile: \(error)") - } + await loadProfile() + hideLoadingIndicator() } } @@ -74,8 +58,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr override func process(viewAction: UserProfileScreenViewAction) { switch viewAction { - case .displayAvatar: - Task { await displayFullScreenAvatar() } + case .displayAvatar(let url): + Task { await displayFullScreenAvatar(url) } case .openDirectChat: Task { await openDirectChat() } case .startCall(let roomID): @@ -87,15 +71,40 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr // MARK: - Private - private func displayFullScreenAvatar() async { + private func loadProfile() async { + async let profileResult = clientProxy.profile(for: state.userID) + async let identityResult = clientProxy.userIdentity(for: state.userID) + + switch await profileResult { + case .success(let userProfile): + state.userProfile = userProfile + state.permalink = (try? matrixToUserPermalink(userId: state.userID)).flatMap(URL.init(string:)) + switch await clientProxy.directRoomForUserID(userProfile.userID) { + case .success(let roomID): + state.dmRoomID = roomID + case .failure: + break + } + case .failure(let error): + state.bindings.alertInfo = .init(id: .unknown) + MXLog.error("Failed to find user profile: \(error)") + } + + if case let .success(.some(identity)) = await identityResult { + state.isVerified = identity.isVerified() + } else { + MXLog.error("Failed to find the user's identity.") + } + } + + private func displayFullScreenAvatar(_ url: URL) async { guard let userProfile = state.userProfile else { fatalError() } - guard let avatarURL = userProfile.avatarURL else { return } showLoadingIndicator(allowsInteraction: false) defer { hideLoadingIndicator() } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName) } } diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift index 43201a3bb4..7e90f0fdde 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -14,6 +14,8 @@ struct UserProfileScreen: View { var body: some View { Form { headerSection + + verificationSection } .compoundList() .navigationTitle(L10n.screenRoomMemberDetailsTitle) @@ -27,6 +29,25 @@ struct UserProfileScreen: View { // MARK: - Private @ViewBuilder + private var headerSection: some View { + if let userProfile = context.viewState.userProfile { + AvatarHeaderView(user: userProfile, + isVerified: context.viewState.showVerifiedBadge, + avatarSize: .user(on: .memberDetails), + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) + } footer: { + otherUserFooter + } + } else { + AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), + isVerified: context.viewState.showVerifiedBadge, + avatarSize: .user(on: .memberDetails), + mediaProvider: context.mediaProvider, + footer: { }) + } + } + private var otherUserFooter: some View { HStack(spacing: 8) { if context.viewState.userProfile != nil, !context.viewState.isOwnUser { @@ -59,20 +80,15 @@ struct UserProfileScreen: View { } @ViewBuilder - private var headerSection: some View { - if let userProfile = context.viewState.userProfile { - AvatarHeaderView(user: userProfile, - avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) - } footer: { - otherUserFooter + var verificationSection: some View { + if context.viewState.showVerificationSection { + Section { + ListRow(label: .default(title: L10n.commonVerifyIdentity, + description: L10n.screenRoomMemberDetailsVerifyButtonSubtitle, + icon: \.lock), + kind: .button { }) + .disabled(true) } - } else { - AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), - avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider, - footer: { }) } } @@ -91,10 +107,14 @@ struct UserProfileScreen: View { // MARK: - Previews struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { - static let otherUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockDan.userID) + static let verifiedUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockDan.userID) + static let otherUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockAlice.userID) static let accountOwnerViewModel = makeViewModel(userID: RoomMemberProxyMock.mockMe.userID) static var previews: some View { + UserProfileScreen(context: verifiedUserViewModel.context) + .previewDisplayName("Verified User") + .snapshotPreferences(delay: 0.25) UserProfileScreen(context: otherUserViewModel.context) .previewDisplayName("Other User") .snapshotPreferences(delay: 0.25) @@ -105,13 +125,17 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { static func makeViewModel(userID: String) -> UserProfileScreenViewModel { let clientProxyMock = ClientProxyMock(.init()) + clientProxyMock.userIdentityForClosure = { userID in + let isVerified = userID == RoomMemberProxyMock.mockDan.userID + return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified))) + } if userID != RoomMemberProxyMock.mockMe.userID { clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") } return UserProfileScreenViewModel(userID: userID, isPresentedModally: false, clientProxy: clientProxyMock, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) } diff --git a/ElementX/Sources/Services/Analytics/Helpers/Analytics+SwiftUI.swift b/ElementX/Sources/Services/Analytics/Helpers/Analytics+SwiftUI.swift index cdc573bc69..2c5247b868 100644 --- a/ElementX/Sources/Services/Analytics/Helpers/Analytics+SwiftUI.swift +++ b/ElementX/Sources/Services/Analytics/Helpers/Analytics+SwiftUI.swift @@ -7,13 +7,6 @@ import SwiftUI -private struct AnalyticsServiceKey: EnvironmentKey { - static let defaultValue: AnalyticsService = ServiceLocator.shared.analytics -} - extension EnvironmentValues { - var analyticsService: AnalyticsService { - get { self[AnalyticsServiceKey.self] } - set { self[AnalyticsServiceKey.self] = newValue } - } + @Entry var analyticsService: AnalyticsService = ServiceLocator.shared.analytics } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift index 350e388246..1c448eae92 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilder.swift @@ -8,8 +8,16 @@ import Foundation import MatrixRustSDK +// sourcery: AutoMockable +protocol AuthenticationClientBuilderProtocol { + func build(homeserverAddress: String) async throws -> ClientProtocol + func buildWithQRCode(qrCodeData: QrCodeData, + oidcConfiguration: OIDCConfigurationProxy, + progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol +} + /// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins. -struct AuthenticationClientBuilder { +struct AuthenticationClientBuilder: AuthenticationClientBuilderProtocol { let sessionDirectories: SessionDirectories let passphrase: String let clientSessionDelegate: ClientSessionDelegate @@ -18,7 +26,7 @@ struct AuthenticationClientBuilder { let appHooks: AppHooks /// Builds a Client for login using OIDC or password authentication. - func build(homeserverAddress: String) async throws -> Client { + func build(homeserverAddress: String) async throws -> ClientProtocol { if appSettings.slidingSyncDiscovery == .forceNative { return try await makeClientBuilder(slidingSync: .forceNative).serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress).build() } @@ -38,7 +46,7 @@ struct AuthenticationClientBuilder { /// Builds a Client, authenticating with the given QR code data. func buildWithQRCode(qrCodeData: QrCodeData, oidcConfiguration: OIDCConfigurationProxy, - progressListener: QrLoginProgressListenerProxy) async throws -> Client { + progressListener: QrLoginProgressListenerProxy) async throws -> ClientProtocol { if appSettings.slidingSyncDiscovery == .forceNative { return try await makeClientBuilder(slidingSync: .forceNative).buildWithQrCode(qrCodeData: qrCodeData, oidcConfiguration: oidcConfiguration.rustValue, @@ -70,7 +78,7 @@ struct AuthenticationClientBuilder { slidingSync: slidingSync, sessionDelegate: clientSessionDelegate, appHooks: appHooks, - invisibleCryptoEnabled: appSettings.invisibleCryptoEnabled) + enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode) .sessionPaths(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath) .passphrase(passphrase: passphrase) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift new file mode 100644 index 0000000000..873a17dfd9 --- /dev/null +++ b/ElementX/Sources/Services/Authentication/AuthenticationClientBuilderFactory.swift @@ -0,0 +1,33 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +// sourcery: AutoMockable +protocol AuthenticationClientBuilderFactoryProtocol { + func makeBuilder(sessionDirectories: SessionDirectories, + passphrase: String, + clientSessionDelegate: ClientSessionDelegate, + appSettings: AppSettings, + appHooks: AppHooks) -> AuthenticationClientBuilderProtocol +} + +/// A wrapper around `ClientBuilder` to share reusable code between Normal and QR logins. +struct AuthenticationClientBuilderFactory: AuthenticationClientBuilderFactoryProtocol { + func makeBuilder(sessionDirectories: SessionDirectories, + passphrase: String, + clientSessionDelegate: ClientSessionDelegate, + appSettings: AppSettings, + appHooks: AppHooks) -> AuthenticationClientBuilderProtocol { + AuthenticationClientBuilder(sessionDirectories: sessionDirectories, + passphrase: passphrase, + clientSessionDelegate: clientSessionDelegate, + appSettings: appSettings, + appHooks: appHooks) + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index c799a8f313..4c3307377a 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -10,31 +10,39 @@ import Foundation import MatrixRustSDK class AuthenticationService: AuthenticationServiceProtocol { - private var client: Client? + private var client: ClientProtocol? private var sessionDirectories: SessionDirectories private let passphrase: String + private let clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol private let userSessionStore: UserSessionStoreProtocol private let appSettings: AppSettings private let appHooks: AppHooks private let homeserverSubject: CurrentValueSubject var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } + private(set) var flow: AuthenticationFlow - init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) { + init(userSessionStore: UserSessionStoreProtocol, + encryptionKeyProvider: EncryptionKeyProviderProtocol, + clientBuilderFactory: AuthenticationClientBuilderFactoryProtocol = AuthenticationClientBuilderFactory(), + appSettings: AppSettings, + appHooks: AppHooks) { sessionDirectories = .init() passphrase = encryptionKeyProvider.generateKey().base64EncodedString() + self.clientBuilderFactory = clientBuilderFactory self.userSessionStore = userSessionStore self.appSettings = appSettings self.appHooks = appHooks - homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, - loginMode: .unknown)) + // When updating these, don't forget to update the reset method too. + homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) + flow = .login } // MARK: - Public - func configure(for homeserverAddress: String) async -> Result { + func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result { do { var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown) @@ -57,7 +65,15 @@ class AuthenticationService: AuthenticationServiceProtocol { case .failure: nil } + if flow == .login, homeserver.loginMode == .unsupported { + return .failure(.loginNotSupported) + } + if flow == .register, !homeserver.supportsRegistration { + return .failure(.registrationNotSupported) + } + self.client = client + self.flow = flow homeserverSubject.send(homeserver) return .success(()) } catch ClientBuildError.WellKnownDeserializationError(let error) { @@ -75,7 +91,7 @@ class AuthenticationService: AuthenticationServiceProtocol { func urlForOIDCLogin() async -> Result { guard let client else { return .failure(.oidcError(.urlFailure)) } do { - let oidcData = try await client.urlForOidcLogin(oidcConfiguration: appSettings.oidcConfiguration.rustValue) + let oidcData = try await client.urlForOidc(oidcConfiguration: appSettings.oidcConfiguration.rustValue, prompt: .consent) return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData)) } catch { MXLog.error("Failed to get URL for OIDC login: \(error)") @@ -86,7 +102,7 @@ class AuthenticationService: AuthenticationServiceProtocol { func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { guard let client else { return } MXLog.info("Aborting OIDC login.") - await client.abortOidcLogin(authorizationData: data.underlyingData) + await client.abortOidcAuth(authorizationData: data.underlyingData) } func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result { @@ -150,18 +166,24 @@ class AuthenticationService: AuthenticationServiceProtocol { } } + func reset() { + homeserverSubject.send(LoginHomeserver(address: appSettings.defaultHomeserverAddress, loginMode: .unknown)) + flow = .login + client = nil + } + // MARK: - Private - private func makeClientBuilder() -> AuthenticationClientBuilder { + private func makeClientBuilder() -> AuthenticationClientBuilderProtocol { // Use a fresh session directory each time the user enters a different server // so that caches (e.g. server versions) are always fresh for the new server. rotateSessionDirectory() - return AuthenticationClientBuilder(sessionDirectories: sessionDirectories, - passphrase: passphrase, - clientSessionDelegate: userSessionStore.clientSessionDelegate, - appSettings: appSettings, - appHooks: appHooks) + return clientBuilderFactory.makeBuilder(sessionDirectories: sessionDirectories, + passphrase: passphrase, + clientSessionDelegate: userSessionStore.clientSessionDelegate, + appSettings: appSettings, + appHooks: appHooks) } private func rotateSessionDirectory() { @@ -169,7 +191,7 @@ class AuthenticationService: AuthenticationServiceProtocol { sessionDirectories = .init() } - private func userSession(for client: Client) async -> Result { + private func userSession(for client: ClientProtocol) async -> Result { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let clientProxy): return .success(clientProxy) @@ -178,3 +200,15 @@ class AuthenticationService: AuthenticationServiceProtocol { } } } + +// MARK: - Mocks + +extension AuthenticationService { + static var mock: AuthenticationService { + AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: AuthenticationClientBuilderFactoryMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + } +} diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index e347bc7b98..0159b7502a 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -16,7 +16,7 @@ enum AuthenticationFlow { case register } -enum AuthenticationServiceError: Error { +enum AuthenticationServiceError: Error, Equatable { /// An error occurred during OIDC authentication. case oidcError(OIDCError) case invalidServer @@ -24,6 +24,8 @@ enum AuthenticationServiceError: Error { case invalidHomeserverAddress case invalidWellKnown(String) case slidingSyncNotAvailable + case loginNotSupported + case registrationNotSupported case accountDeactivated case failedLoggingIn case sessionTokenRefreshNotSupported @@ -33,9 +35,11 @@ enum AuthenticationServiceError: Error { protocol AuthenticationServiceProtocol { /// The currently configured homeserver. var homeserver: CurrentValuePublisher { get } + /// The type of flow the service is currently configured with. + var flow: AuthenticationFlow { get } /// Sets up the service for login on the specified homeserver address. - func configure(for homeserverAddress: String) async -> Result + func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result /// Performs login using OIDC for the current homeserver. func urlForOIDCLogin() async -> Result /// Asks the SDK to abort an ongoing OIDC login if we didn't get a callback to complete the request with. @@ -46,6 +50,9 @@ protocol AuthenticationServiceProtocol { func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result /// Completes registration using the credentials obtained via the helper URL. func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result + + /// Resets the current configuration requiring `configure(for:flow:)` to be called again. + func reset() } // MARK: - OIDC @@ -72,7 +79,7 @@ struct OIDCAuthorizationDataProxy: Equatable { } } -extension OidcAuthorizationData: Equatable { +extension OidcAuthorizationData: @retroactive Equatable { public static func == (lhs: MatrixRustSDK.OidcAuthorizationData, rhs: MatrixRustSDK.OidcAuthorizationData) -> Bool { lhs.loginUrl() == rhs.loginUrl() } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift deleted file mode 100644 index 3cd72631ae..0000000000 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationService.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Combine -import Foundation -import MatrixRustSDK - -class MockAuthenticationService: AuthenticationServiceProtocol { - let validCredentials = (username: "alice", password: "12345678") - - private let homeserverSubject: CurrentValueSubject - var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } - - init(homeserver: LoginHomeserver = .mockMatrixDotOrg) { - homeserverSubject = .init(homeserver) - } - - func configure(for homeserverAddress: String) async -> Result { - // Map the address to the mock homeservers - if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) { - homeserverSubject.send(.mockMatrixDotOrg) - return .success(()) - } else if LoginHomeserver.mockOIDC.address.contains(homeserverAddress) { - homeserverSubject.send(.mockOIDC) - return .success(()) - } else if LoginHomeserver.mockBasicServer.address.contains(homeserverAddress) { - homeserverSubject.send(.mockBasicServer) - return .success(()) - } else if LoginHomeserver.mockUnsupported.address.contains(homeserverAddress) { - homeserverSubject.send(.mockUnsupported) - return .success(()) - } else { - // Otherwise fail with an invalid server. - return .failure(.invalidServer) - } - } - - func urlForOIDCLogin() async -> Result { - .failure(.oidcError(.notSupported)) - } - - func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { } - - func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result { - .failure(.oidcError(.notSupported)) - } - - func login(username: String, password: String, initialDeviceName: String?, deviceID: String?) async -> Result { - // Login only succeeds if the username and password match the valid credentials property - guard username == validCredentials.username, password == validCredentials.password else { - return .failure(.invalidCredentials) - } - - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: username)))) - return .success(userSession) - } - - func completeWebRegistration(using credentials: WebRegistrationCredentials) async -> Result { - .failure(.failedLoggingIn) - } -} diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift index f263f653fb..75037c4b30 100644 --- a/ElementX/Sources/Services/BugReport/BugReportService.swift +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -117,14 +117,14 @@ class BugReportService: NSObject, BugReportServiceProtocol { let (data, response) = try await session.dataWithRetry(for: request, delegate: self) guard let httpResponse = response as? HTTPURLResponse else { - let errorDescription = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + let errorDescription = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error" MXLog.error("Failed to submit bug report: \(errorDescription)") MXLog.error("Response: \(response)") return .failure(.serverError(response, errorDescription)) } guard httpResponse.statusCode == 200 else { - let errorDescription = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + let errorDescription = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error" MXLog.error("Failed to submit bug report: \(errorDescription) (\(httpResponse.statusCode))") MXLog.error("Response: \(httpResponse)") return .failure(.httpError(httpResponse, errorDescription)) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index b06c3e0cee..801ce4e8cb 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -50,6 +50,8 @@ class ClientProxy: ClientProxyProtocol { let secureBackupController: SecureBackupControllerProtocol + private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? + private static var roomCreationPowerLevelOverrides: PowerLevels { .init(usersDefault: nil, eventsDefault: nil, @@ -154,10 +156,10 @@ class ClientProxy: ClientProxyProtocol { self?.ignoredUsersSubject.send(ignoredUsers) }) - updateVerificationState(client.encryption().verificationState()) + await updateVerificationState(client.encryption().verificationState()) verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: VerificationStateListenerProxy { [weak self] verificationState in - self?.updateVerificationState(verificationState) + Task { await self?.updateVerificationState(verificationState) } }) sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SendQueueRoomErrorListenerProxy { [weak self] roomID, error in @@ -295,15 +297,24 @@ class ClientProxy: ClientProxyProtocol { restartTask = nil } + guard let syncService else { + MXLog.warning("No sync service to stop.") + completion?() + return + } + // Capture the sync service strongly as this method is called on deinit and so the // existence of self when the Task executes is questionable and would sometimes crash. + // Note: This isn't strictly necessary now given the unwrap above, but leaving the code as + // documentation. SE-0371 will allow us to fix this by using an async deinit. Task { [syncService] in do { defer { completion?() } - try await syncService?.stop() + try await syncService.stop() + MXLog.info("Sync stopped") } catch { MXLog.error("Failed stopping the sync service with error: \(error)") } @@ -365,8 +376,10 @@ class ClientProxy: ClientProxyProtocol { } } - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result { + // swiftlint:disable:next function_parameter_count + func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result { do { + // TODO: Revisit once the SDK supports the knocking API let parameters = CreateRoomParameters(name: name, topic: topic, isEncrypted: isRoomPrivate, @@ -413,6 +426,28 @@ class ClientProxy: ClientProxyProtocol { } } + func knockRoom(_ roomID: String, via: [String], message: String?) async -> Result { + do { + let _ = try await client.knock(roomIdOrAlias: roomID, reason: message, serverNames: via) + await waitForRoomToSync(roomID: roomID, timeout: .seconds(30)) + return .success(()) + } catch { + MXLog.error("Failed knocking roomID: \(roomID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + + func knockRoomAlias(_ roomAlias: String, message: String?) async -> Result { + do { + let room = try await client.knock(roomIdOrAlias: roomAlias, reason: message, serverNames: []) + await waitForRoomToSync(roomID: room.id(), timeout: .seconds(30)) + return .success(()) + } catch { + MXLog.error("Failed knocking roomAlias: \(roomAlias) with error: \(error)") + return .failure(.sdkError(error)) + } + } + func uploadMedia(_ media: MediaInfo) async -> Result { guard let mimeType = media.mimeType else { MXLog.error("Failed uploading media, invalid mime type: \(media)") @@ -460,7 +495,8 @@ class ClientProxy: ClientProxyProtocol { func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result { do { let roomPreview = try await client.getRoomPreviewFromRoomId(roomId: identifier, viaServers: via) - return .success(.init(roomPreview)) + let roomPreviewInfo = try roomPreview.info() + return .success(.init(roomPreviewInfo)) } catch let error as ClientError where error.code == .forbidden { return .failure(.roomPreviewIsPrivate) } catch { @@ -537,16 +573,6 @@ class ClientProxy: ClientProxyProtocol { } } - func sessionVerificationControllerProxy() async -> Result { - do { - let sessionVerificationController = try await client.getSessionVerificationController() - return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)) - } catch { - MXLog.error("Failed retrieving session verification controller proxy with error: \(error)") - return .failure(.sdkError(error)) - } - } - func logout() async -> URL? { do { return try await client.logout().flatMap(URL.init(string:)) @@ -599,7 +625,9 @@ class ClientProxy: ClientProxyProtocol { func resolveRoomAlias(_ alias: String) async -> Result { do { - let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) + guard let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) else { + return .failure(.failedResolvingRoomAlias) + } // Resolving aliases is done through the directory/room API which returns too many / all known // vias, which in turn results in invalid join requests. Trim them to something manageable @@ -672,7 +700,7 @@ class ClientProxy: ClientProxyProtocol { for roomID in roomIdentifiers { guard case let .joined(roomProxy) = await roomForIdentifier(roomID), - roomProxy.isDirect, + roomProxy.infoPublisher.value.isDirect, let members = await roomProxy.members() else { continue } @@ -692,7 +720,7 @@ class ClientProxy: ClientProxyProtocol { // MARK: - Private - private func updateVerificationState(_ verificationState: VerificationState) { + private func updateVerificationState(_ verificationState: VerificationState) async { let verificationState: SessionVerificationState = switch verificationState { case .unknown: .unknown @@ -702,8 +730,28 @@ class ClientProxy: ClientProxyProtocol { .verified } + // The session verification controller requires the user's identity which + // isn't available before a keys query response. Use the verification + // state updates as an aproximation for when that happens. + await buildSessionVerificationControllerProxyIfPossible(verificationState: verificationState) + + // Only update the session verification state after creating a session + // verification proxy to avoid race conditions verificationStateSubject.send(verificationState) } + + private func buildSessionVerificationControllerProxyIfPossible(verificationState: SessionVerificationState) async { + guard sessionVerificationController == nil, verificationState != .unknown else { + return + } + + do { + let sessionVerificationController = try await client.getSessionVerificationController() + self.sessionVerificationController = SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController) + } catch { + MXLog.error("Failed retrieving session verification controller proxy with error: \(error)") + } + } private func loadUserAvatarURLFromCache() { loadCachedAvatarURLTask = Task { @@ -816,12 +864,13 @@ class ClientProxy: ClientProxyProtocol { }) } - private lazy var eventFilters: TimelineEventTypeFilter = { + private let eventFilters: TimelineEventTypeFilter = { var stateEventFilters: [StateEventType] = [.roomAliases, .roomCanonicalAlias, .roomGuestAccess, .roomHistoryVisibility, .roomJoinRules, + .roomPinnedEvents, .roomPowerLevels, .roomServerAcl, .roomTombstone, @@ -830,12 +879,6 @@ class ClientProxy: ClientProxyProtocol { .policyRuleRoom, .policyRuleServer, .policyRuleUser] - - // Reminder: once the feature flag is not required anymore, change the lazy var back to a let - if !appSettings.pinningEnabled { - stateEventFilters.append(.roomPinnedEvents) - } - return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) }) }() @@ -850,8 +893,16 @@ class ClientProxy: ClientProxyProtocol { switch roomListItem.membership() { case .invited: - return try .invited(InvitedRoomProxy(roomListItem: roomListItem, - room: roomListItem.invitedRoom())) + return try await .invited(InvitedRoomProxy(roomListItem: roomListItem, + room: roomListItem.invitedRoom())) + case .knocked: + if appSettings.knockingEnabled { + return try await .knocked(KnockedRoomProxy(roomListItem: roomListItem, + room: roomListItem.invitedRoom())) + } else { + return try await .invited(InvitedRoomProxy(roomListItem: roomListItem, + room: roomListItem.invitedRoom())) + } case .joined: if roomListItem.isTimelineInitialized() == false { try await roomListItem.initTimeline(eventTypeFilter: eventFilters, internalIdPrefix: nil) @@ -900,6 +951,22 @@ class ClientProxy: ClientProxyProtocol { await client.encryption().curve25519Key() } + func pinUserIdentity(_ userID: String) async -> Result { + MXLog.info("Pinning current identity for user: \(userID)") + + do { + guard let userIdentity = try await client.encryption().userIdentity(userId: userID) else { + MXLog.error("Failed retrieving identity for user: \(userID)") + return .failure(.failedRetrievingUserIdentity) + } + + return try await .success(userIdentity.pin()) + } catch { + MXLog.error("Failed pinning current identity for user: \(error)") + return .failure(.sdkError(error)) + } + } + func resetIdentity() async -> Result { do { return try await .success(client.encryption().resetIdentity()) @@ -907,6 +974,15 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } + + func userIdentity(for userID: String) async -> Result { + do { + return try await .success(client.encryption().userIdentity(userId: userID)) + } catch { + MXLog.error("Failed retrieving user identity: \(error)") + return .failure(.sdkError(error)) + } + } } extension ClientProxy: MediaLoaderProtocol { @@ -918,8 +994,8 @@ extension ClientProxy: MediaLoaderProtocol { try await mediaLoader.loadMediaThumbnailForSource(source, width: width, height: height) } - func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy { - try await mediaLoader.loadMediaFileForSource(source, body: body) + func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy { + try await mediaLoader.loadMediaFileForSource(source, filename: filename) } } @@ -1027,17 +1103,17 @@ private class SendQueueRoomErrorListenerProxy: SendQueueRoomErrorListener { } private extension RoomPreviewDetails { - init(_ roomPreview: RoomPreview) { - self = RoomPreviewDetails(roomID: roomPreview.roomId, - name: roomPreview.name, - canonicalAlias: roomPreview.canonicalAlias, - topic: roomPreview.topic, - avatarURL: roomPreview.avatarUrl.flatMap(URL.init(string:)), - memberCount: UInt(roomPreview.numJoinedMembers), - isHistoryWorldReadable: roomPreview.isHistoryWorldReadable, - isJoined: roomPreview.isJoined, - isInvited: roomPreview.isInvited, - isPublic: roomPreview.isPublic, - canKnock: roomPreview.canKnock) + init(_ roomPreviewInfo: RoomPreviewInfo) { + self = RoomPreviewDetails(roomID: roomPreviewInfo.roomId, + name: roomPreviewInfo.name, + canonicalAlias: roomPreviewInfo.canonicalAlias, + topic: roomPreviewInfo.topic, + avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)), + memberCount: UInt(roomPreviewInfo.numJoinedMembers), + isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable, + isJoined: roomPreviewInfo.membership == .joined, + isInvited: roomPreviewInfo.membership == .invited, + isPublic: roomPreviewInfo.joinRule == .public, + canKnock: roomPreviewInfo.joinRule == .knock) } } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 29a5a856cb..85bd4f7adf 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -35,19 +35,12 @@ enum ClientProxyError: Error { case invalidServerName case failedUploadingMedia(Error, MatrixErrorCode) case roomPreviewIsPrivate + case failedRetrievingUserIdentity + case failedResolvingRoomAlias } enum SlidingSyncConstants { - static let defaultTimelineLimit: UInt32 = 20 static let maximumVisibleRangeSize = 30 - static let defaultRequiredState = [ - RequiredState(key: "m.room.name", value: ""), - RequiredState(key: "m.room.topic", value: ""), - RequiredState(key: "m.room.avatar", value: ""), - RequiredState(key: "m.room.canonical_alias", value: ""), - RequiredState(key: "m.room.join_rules", value: ""), - RequiredState(key: "m.room.pinned_events", value: "") - ] } /// This struct represents the configuration that we are using to register the application through Pusher to Sygnal @@ -123,6 +116,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var secureBackupController: SecureBackupControllerProtocol { get } + var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get } + func isOnlyDeviceLeft() async -> Result func startSync() @@ -137,12 +132,17 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result + // swiftlint:disable:next function_parameter_count + func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result func joinRoom(_ roomID: String, via: [String]) async -> Result func joinRoomAlias(_ roomAlias: String) async -> Result + func knockRoom(_ roomID: String, via: [String], message: String?) async -> Result + + func knockRoomAlias(_ roomAlias: String, message: String?) async -> Result + func uploadMedia(_ media: MediaInfo) async -> Result func roomForIdentifier(_ identifier: String) async -> RoomProxyType? @@ -158,8 +158,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func setUserAvatar(media: MediaInfo) async -> Result func removeUserAvatar() async -> Result - - func sessionVerificationControllerProxy() async -> Result func deactivateAccount(password: String?, eraseData: Bool) async -> Result @@ -196,5 +194,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func ed25519Base64() async -> String? func curve25519Base64() async -> String? + func pinUserIdentity(_ userID: String) async -> Result func resetIdentity() async -> Result + + func userIdentity(for userID: String) async -> Result } diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index d61a7e79ea..d2e03d96dd 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -12,5 +12,6 @@ struct CreateRoomFlowParameters { var name = "" var topic = "" var isRoomPrivate = true + var isKnockingOnly = false var avatarImageMedia: MediaInfo? } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 980e9b5f3e..1ecf1dfa20 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -35,14 +35,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe return CXProvider(configuration: configuration) }() - private weak var clientProxy: ClientProxyProtocol? + private weak var clientProxy: ClientProxyProtocol? { + didSet { + // There's a race condition where a call starts when the app has been killed and the + // observation set in `incomingCallID` occurs *before* the user session is restored. + // So observe when the client proxy is set to fix this (the method guards for the call). + Task { await observeIncomingCallRoomInfo() } + } + } - private var cancellables = Set() + private var incomingCallRoomInfoCancellable: AnyCancellable? private var incomingCallID: CallID? { didSet { - Task { - await observeIncomingCallRoomStateUpdates() - } + Task { await observeIncomingCallRoomInfo() } } } @@ -103,6 +108,17 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe } catch { MXLog.error("Failed requesting start call action with error: \(error)") } + + do { + // Have ElementCall default to the speaker so that the lock button doesn't end the call. + // Could also use `overrideOutputAudioPort` but the documentation is clear about it: + // `Sessions using PlayAndRecord category that always want to prefer the built-in + // speaker output over the receiver, should use AVAudioSessionCategoryOptionDefaultToSpeaker instead.`. + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker]) + try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) + } catch { + MXLog.error("Failed setting up audio session with error: \(error)") + } } func tearDownCallSession() { @@ -154,11 +170,13 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe // https://stackoverflow.com/a/41230020/730924 update.remoteHandle = .init(type: .generic, value: roomID) - callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { error in + callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { [weak self] error in if let error { MXLog.error("Failed reporting new incoming call with error: \(error)") } + self?.actionsSubject.send(.receivedIncomingCallRequest) + completion() } @@ -235,10 +253,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe func provider(_ provider: CXProvider, perform action: CXEndCallAction) { #if targetEnvironment(simulator) // This gets called for no reason on simulators, where CallKit - // isn't even supported. Ignore - return - #endif - + // isn't even supported, ignore it. + #else if let ongoingCallID { actionsSubject.send(.endCall(roomID: ongoingCallID.roomID)) } @@ -246,11 +262,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe tearDownCallSession(sendEndCallAction: false) action.fulfill() + #endif } // MARK: - Private - func tearDownCallSession(sendEndCallAction: Bool = true) { + private func tearDownCallSession(sendEndCallAction: Bool = true) { if sendEndCallAction, let ongoingCallID { let transaction = CXTransaction(action: CXEndCallAction(call: ongoingCallID.callKitID)) callController.request(transaction) { error in @@ -263,35 +280,35 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe ongoingCallID = nil } - func observeIncomingCallRoomStateUpdates() async { - cancellables.removeAll() + private func observeIncomingCallRoomInfo() async { + incomingCallRoomInfoCancellable = nil - guard let clientProxy, let incomingCallID else { + guard let incomingCallID else { + MXLog.info("No incoming call to observe for.") + return + } + + guard let clientProxy else { + MXLog.warning("A ClientProxy is needed to fetch the room.") return } guard case let .joined(roomProxy) = await clientProxy.roomForIdentifier(incomingCallID.roomID) else { + MXLog.warning("Failed to fetch a joined room for the incoming call.") return } roomProxy.subscribeToRoomInfoUpdates() - // There's no incoming event for call cancellations so try to infer - // it from what we have. If the call is running before subscribing then wait - // for it to change to `false` otherwise wait for it to turn `true` before - // changing to `false` - let isCallOngoing = roomProxy.hasOngoingCall - - roomProxy - .actionsPublisher - .map { action -> (Bool, [String]) in - switch action { - case .roomInfoUpdate: - return (roomProxy.hasOngoingCall, roomProxy.activeRoomCallParticipants) - } - } + incomingCallRoomInfoCancellable = roomProxy + .infoPublisher + .compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) } .removeDuplicates { $0 == $1 } - .dropFirst(isCallOngoing ? 0 : 1) + .drop(while: { hasRoomCall, _ in + // Filter all updates before hasRoomCall becomes `true`. Then we can correctly + // detect its change to `false` to stop ringing when the caller hangs up. + !hasRoomCall + }) .sink { [weak self] hasOngoingCall, activeRoomCallParticipants in guard let self else { return } @@ -300,17 +317,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe if !hasOngoingCall { MXLog.info("Call cancelled by remote") - cancellables.removeAll() + incomingCallRoomInfoCancellable = nil endUnansweredCallTask?.cancel() callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded) } else if participants.contains(roomProxy.ownUserID) { - MXLog.info("Call anwered elsewhere") + MXLog.info("Call answered elsewhere") - cancellables.removeAll() + incomingCallRoomInfoCancellable = nil endUnansweredCallTask?.cancel() callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .answeredElsewhere) } } - .store(in: &cancellables) } } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift index 1247541e55..63bdd0c78e 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift @@ -8,6 +8,7 @@ import Combine enum ElementCallServiceAction { + case receivedIncomingCallRequest case startCall(roomID: String) case endCall(roomID: String) case setAudioEnabled(_ enabled: Bool, roomID: String) diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift index 30a41e936f..70f80a09d9 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift @@ -71,28 +71,36 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv let useEncryption = (try? room.isEncrypted()) ?? false - guard let widgetSettings = try? newVirtualElementCallWidget(props: .init(elementCallUrl: baseURL.absoluteString, - widgetId: widgetID, - parentUrl: nil, - hideHeader: nil, - preload: nil, - fontScale: nil, - appPrompt: false, - skipLobby: true, - confineToRoom: true, - font: nil, - analyticsId: nil, - encryption: useEncryption ? .perParticipantKeys : .unencrypted)) else { + let widgetSettings: WidgetSettings + do { + widgetSettings = try newVirtualElementCallWidget(props: .init(elementCallUrl: baseURL.absoluteString, + widgetId: widgetID, + parentUrl: nil, + hideHeader: nil, + preload: nil, + fontScale: nil, + appPrompt: false, + skipLobby: true, + confineToRoom: true, + font: nil, + analyticsId: nil, + encryption: useEncryption ? .perParticipantKeys : .unencrypted)) + } catch { + MXLog.error("Failed to build widget settings: \(error)") return .failure(.failedBuildingWidgetSettings) } let languageTag = "\(Locale.current.language.languageCode ?? "en")-\(Locale.current.language.region ?? "US")" let theme = colorScheme == .light ? "light" : "dark" - guard let urlString = try? await generateWebviewUrl(widgetSettings: widgetSettings, room: room, - props: .init(clientId: clientID, - languageTag: languageTag, - theme: theme)) else { + let urlString: String + do { + urlString = try await generateWebviewUrl(widgetSettings: widgetSettings, room: room, + props: .init(clientId: clientID, + languageTag: languageTag, + theme: theme)) + } catch { + MXLog.error("Failed to generate web view URL: \(error)") return .failure(.failedBuildingCallURL) } @@ -100,7 +108,11 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv return .failure(.failedParsingCallURL) } - guard let widgetDriver = try? makeWidgetDriver(settings: widgetSettings) else { + let widgetDriver: WidgetDriverAndHandle + do { + widgetDriver = try makeWidgetDriver(settings: widgetSettings) + } catch { + MXLog.error("Failed to build widget driver: \(error)") return .failure(.failedBuildingWidgetDriver) } diff --git a/ElementX/Sources/Services/Emojis/EmojiCategory.swift b/ElementX/Sources/Services/Emojis/EmojiCategory.swift deleted file mode 100644 index 7e42a1f819..0000000000 --- a/ElementX/Sources/Services/Emojis/EmojiCategory.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Foundation - -struct EmojiCategory: Equatable, Identifiable { - let id: String - let emojis: [EmojiItem] -} diff --git a/ElementX/Sources/Services/Emojis/EmojiItem.swift b/ElementX/Sources/Services/Emojis/EmojiItem.swift deleted file mode 100644 index d08cbb0abf..0000000000 --- a/ElementX/Sources/Services/Emojis/EmojiItem.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Foundation - -struct EmojiItem: Equatable, Identifiable { - var id: String { - label - } - - let label: String - let unicode: String - let keywords: [String] - let shortcodes: [String] -} diff --git a/ElementX/Sources/Services/Emojis/EmojiProvider.swift b/ElementX/Sources/Services/Emojis/EmojiProvider.swift index 5272902183..80c31ed0d5 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProvider.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProvider.swift @@ -7,31 +7,48 @@ import Emojibase import Foundation - -@MainActor -protocol EmojiProviderProtocol { - func categories(searchString: String?) async -> [EmojiCategory] -} - -private enum EmojiProviderState { - case notLoaded - case inProgress(Task<[EmojiCategory], Never>) - case loaded([EmojiCategory]) -} +import OrderedCollections class EmojiProvider: EmojiProviderProtocol { + private let maxFrequentEmojis = 20 private let loader: EmojiLoaderProtocol - private var state: EmojiProviderState = .notLoaded + private let appSettings: AppSettings + + private(set) var state: EmojiProviderState = .notLoaded - init(loader: EmojiLoaderProtocol = EmojibaseDatasource()) { + init(loader: EmojiLoaderProtocol = EmojibaseDatasource(), appSettings: AppSettings) { self.loader = loader + self.appSettings = appSettings + Task { await loadIfNeeded() } } func categories(searchString: String? = nil) async -> [EmojiCategory] { - let emojiCategories = await loadIfNeeded() + var emojiCategories = await loadIfNeeded() + + let allEmojis = emojiCategories.reduce([]) { partialResult, category in + partialResult + category.emojis + } + + // Map frequently used system unicode emojis to our emoji provider ones and preserve the order + let frequentlyUsedEmojis = frequentlyUsedSystemEmojis().prefix(maxFrequentEmojis) + let emojis = allEmojis + .filter { frequentlyUsedEmojis.contains($0.unicode) } + .sorted { first, second in + guard let firstIndex = frequentlyUsedEmojis.firstIndex(of: first.unicode), + let secondIndex = frequentlyUsedEmojis.firstIndex(of: second.unicode) else { + return false + } + + return firstIndex < secondIndex + } + + if !emojis.isEmpty { + emojiCategories.insert(.init(id: EmojiCategory.frequentlyUsedCategoryIdentifier, emojis: emojis), at: 0) + } + if let searchString, searchString.isEmpty == false { return search(searchString: searchString, emojiCategories: emojiCategories) } else { @@ -39,6 +56,40 @@ class EmojiProvider: EmojiProviderProtocol { } } + func frequentlyUsedSystemEmojis() -> [String] { + guard appSettings.frequentEmojisEnabled, !ProcessInfo.processInfo.isiOSAppOnMac else { + return [] + } + + guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"), + let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"), + let recents = defaults["EMFRecentsKey"] as? [String] + else { + return [] + } + + return recents + } + + func markEmojiAsFrequentlyUsed(_ emoji: String) { + guard appSettings.frequentEmojisEnabled else { + return + } + + guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"), + let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"), + let recents = defaults["EMFRecentsKey"] as? [String] else { + return + } + + var uniqueOrderedRecents = OrderedSet(recents) + uniqueOrderedRecents.insert(emoji, at: 0) + + preferences.setValue(["EMFRecentsKey": Array(uniqueOrderedRecents)], forKey: "EMFDefaultsKey") + } + + // MARK: - Private + private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] { emojiCategories.compactMap { category in let emojis = category.emojis.filter { emoji in diff --git a/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift new file mode 100644 index 0000000000..b0137716e8 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift @@ -0,0 +1,42 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation + +struct EmojiItem: Equatable, Identifiable { + var id: String { + label + } + + let label: String + let unicode: String + let keywords: [String] + let shortcodes: [String] +} + +struct EmojiCategory: Equatable, Identifiable { + static let frequentlyUsedCategoryIdentifier = "io.element.elementx.frequently_used" + + let id: String + let emojis: [EmojiItem] +} + +enum EmojiProviderState { + case notLoaded + case inProgress(Task<[EmojiCategory], Never>) + case loaded([EmojiCategory]) +} + +@MainActor +protocol EmojiProviderProtocol { + var state: EmojiProviderState { get } + + func categories(searchString: String?) async -> [EmojiCategory] + + func frequentlyUsedSystemEmojis() -> [String] + func markEmojiAsFrequentlyUsed(_ emoji: String) +} diff --git a/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift b/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift index cec36f19c7..2c77e549fe 100644 --- a/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift +++ b/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift @@ -82,9 +82,12 @@ private struct VideoProcessingInfo { } struct MediaUploadingPreprocessor { + let appSettings: AppSettings + enum Constants { static let maximumThumbnailSize = CGSize(width: 800, height: 600) - static let thumbnailCompressionQuality = 0.8 + static let optimizedMaxPixelSize = 2048.0 + static let jpegCompressionQuality = 0.78 static let videoThumbnailTime = 5.0 // seconds } @@ -96,7 +99,7 @@ struct MediaUploadingPreprocessor { // Start by copying the file to a unique temporary location in order to avoid conflicts if processing it multiple times // All the other operations will be made relative to it let uniqueFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - let newURL = uniqueFolder.appendingPathComponent(url.lastPathComponent) + var newURL = uniqueFolder.appendingPathComponent(url.lastPathComponent) do { try FileManager.default.createDirectory(at: uniqueFolder, withIntermediateDirectories: true) try FileManager.default.copyItem(at: url, to: newURL) @@ -107,17 +110,17 @@ struct MediaUploadingPreprocessor { // Process unknown types as plain files guard let type = UTType(filenameExtension: newURL.pathExtension), let mimeType = type.preferredMIMEType else { - return await processFile(at: newURL, mimeType: "application/octet-stream") + return processFile(at: newURL, mimeType: "application/octet-stream") } if type.conforms(to: .image) { - return await processImage(at: newURL, type: type, mimeType: mimeType) + return processImage(at: &newURL, type: type, mimeType: mimeType) } else if type.conforms(to: .movie) || type.conforms(to: .video) { return await processVideo(at: newURL) } else if type.conforms(to: .audio) { return await processAudio(at: newURL, mimeType: mimeType) } else { - return await processFile(at: newURL, mimeType: mimeType) + return processFile(at: newURL, mimeType: mimeType) } } @@ -129,34 +132,55 @@ struct MediaUploadingPreprocessor { /// - type: its UTType /// - mimeType: the mimeType extracted from the UTType /// - Returns: Returns a `MediaInfo.image` containing the URLs for the modified image and its thumbnail plus the corresponding `ImageInfo` - private func processImage(at url: URL, type: UTType, mimeType: String) async -> Result { - switch await stripLocationFromImage(at: url, type: type, mimeType: mimeType) { - case .success(let result): - switch await generateThumbnailForImage(at: url) { - case .success(let thumbnailResult): - let imageSize = (try? UInt64(FileManager.default.sizeForItem(at: result.url))) ?? 0 - let thumbnailSize = (try? UInt64(FileManager.default.sizeForItem(at: thumbnailResult.url))) ?? 0 - - let thumbnailInfo = ThumbnailInfo(height: UInt64(thumbnailResult.height), - width: UInt64(thumbnailResult.width), - mimetype: thumbnailResult.mimeType, - size: thumbnailSize) - - let imageInfo = ImageInfo(height: UInt64(result.height), - width: UInt64(result.width), - mimetype: result.mimeType, - size: imageSize, - thumbnailInfo: thumbnailInfo, - thumbnailSource: nil, - blurhash: thumbnailResult.blurhash) - - let mediaInfo = MediaInfo.image(imageURL: result.url, thumbnailURL: thumbnailResult.url, imageInfo: imageInfo) + private func processImage(at url: inout URL, type: UTType, mimeType: String) -> Result { + do { + try stripLocationFromImage(at: url, type: type) + + var mimeType = mimeType + if appSettings.optimizeMediaUploads, !type.conforms(to: .gif) { + let outputType = type.conforms(to: .png) ? UTType.png : .jpeg + mimeType = outputType.preferredMIMEType ?? "application/octet-stream" + try resizeImage(at: url, maxPixelSize: Constants.optimizedMaxPixelSize, destination: url, type: outputType) - return .success(mediaInfo) - case .failure(let error): - return .failure(.failedProcessingImage(error)) + if let preferredFilenameExtension = outputType.preferredFilenameExtension, + url.pathExtension != preferredFilenameExtension { + let convertedURL = url.deletingPathExtension().appendingPathExtension(preferredFilenameExtension) + do { + try FileManager.default.moveItem(at: url, to: convertedURL) + } catch { + return .failure(.failedResizingImage) + } + url = convertedURL + } + } + + let thumbnailResult = try generateThumbnailForImage(at: url) + + guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil), + let imageSize = imageSource.size else { + return .failure(.failedProcessingImage(.failedStrippingLocationData)) } - case .failure(let error): + + let fileSize = (try? UInt64(FileManager.default.sizeForItem(at: url))) ?? 0 + let thumbnailFileSize = (try? UInt64(FileManager.default.sizeForItem(at: thumbnailResult.url))) ?? 0 + + let thumbnailInfo = ThumbnailInfo(height: UInt64(thumbnailResult.height), + width: UInt64(thumbnailResult.width), + mimetype: thumbnailResult.mimeType, + size: thumbnailFileSize) + + let imageInfo = ImageInfo(height: UInt64(imageSize.height), + width: UInt64(imageSize.width), + mimetype: mimeType, + size: fileSize, + thumbnailInfo: thumbnailInfo, + thumbnailSource: nil, + blurhash: thumbnailResult.blurhash) + + let mediaInfo = MediaInfo.image(imageURL: url, thumbnailURL: thumbnailResult.url, imageInfo: imageInfo) + + return .success(mediaInfo) + } catch { return .failure(.failedProcessingImage(error)) } } @@ -168,34 +192,31 @@ struct MediaUploadingPreprocessor { /// - mimeType: the mimeType extracted from the UTType /// - Returns: Returns a `MediaInfo.video` containing the URLs for the modified video and its thumbnail plus the corresponding `VideoInfo` private func processVideo(at url: URL) async -> Result { - switch await convertVideoToMP4(url) { - case .success(let result): - switch await generateThumbnailForVideoAt(result.url) { - case .success(let thumbnailResult): - let videoSize = (try? UInt64(FileManager.default.sizeForItem(at: result.url))) ?? 0 - let thumbnailSize = (try? UInt64(FileManager.default.sizeForItem(at: thumbnailResult.url))) ?? 0 - - let thumbnailInfo = ThumbnailInfo(height: UInt64(thumbnailResult.height), - width: UInt64(thumbnailResult.width), - mimetype: thumbnailResult.mimeType, - size: thumbnailSize) - - let videoInfo = VideoInfo(duration: result.duration, - height: UInt64(result.height), - width: UInt64(result.width), - mimetype: result.mimeType, - size: videoSize, - thumbnailInfo: thumbnailInfo, - thumbnailSource: nil, - blurhash: thumbnailResult.blurhash) - - let mediaInfo = MediaInfo.video(videoURL: result.url, thumbnailURL: thumbnailResult.url, videoInfo: videoInfo) - - return .success(mediaInfo) - case .failure(let error): - return .failure(.failedProcessingVideo(error)) - } - case .failure(let error): + do { + let result = try await convertVideoToMP4(url) + let thumbnailResult = try await generateThumbnailForVideoAt(result.url) + + let videoSize = (try? UInt64(FileManager.default.sizeForItem(at: result.url))) ?? 0 + let thumbnailSize = (try? UInt64(FileManager.default.sizeForItem(at: thumbnailResult.url))) ?? 0 + + let thumbnailInfo = ThumbnailInfo(height: UInt64(thumbnailResult.height), + width: UInt64(thumbnailResult.width), + mimetype: thumbnailResult.mimeType, + size: thumbnailSize) + + let videoInfo = VideoInfo(duration: result.duration, + height: UInt64(result.height), + width: UInt64(result.width), + mimetype: result.mimeType, + size: videoSize, + thumbnailInfo: thumbnailInfo, + thumbnailSource: nil, + blurhash: thumbnailResult.blurhash) + + let mediaInfo = MediaInfo.video(videoURL: result.url, thumbnailURL: thumbnailResult.url, videoInfo: videoInfo) + + return .success(mediaInfo) + } catch { return .failure(.failedProcessingVideo(error)) } } @@ -223,31 +244,30 @@ struct MediaUploadingPreprocessor { /// - type: its UTType /// - mimeType: the mimeType extracted from the UTType /// - Returns: Returns a `MediaInfo.file` containing the file URL plus the corresponding `FileInfo` - private func processFile(at url: URL, mimeType: String?) async -> Result { + private func processFile(at url: URL, mimeType: String?) -> Result { let fileSize = (try? UInt64(FileManager.default.sizeForItem(at: url))) ?? 0 let fileInfo = FileInfo(mimetype: mimeType, size: fileSize, thumbnailInfo: nil, thumbnailSource: nil) return .success(.file(fileURL: url, fileInfo: fileInfo)) } - // MARK: Images + // MARK: Image Helpers /// Removes the GPS dictionary from an image's metadata /// - Parameters: /// - url: the URL for the original image /// - type: its UTType /// - Returns: the URL for the modified image and its size as an `ImageProcessingResult` - private func stripLocationFromImage(at url: URL, type: UTType, mimeType: String) async -> Result { + private func stripLocationFromImage(at url: URL, type: UTType) throws(MediaUploadingPreprocessorError) { guard let originalData = NSData(contentsOf: url), - let originalImage = UIImage(data: originalData as Data), let imageSource = CGImageSourceCreateWithData(originalData, nil) else { - return .failure(.failedStrippingLocationData) + throw .failedStrippingLocationData } guard let originalMetadata = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil), (originalMetadata as NSDictionary).value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") != nil else { - MXLog.info("No GPS metadata found. Returning original image") - return .success(.init(url: url, height: Double(originalImage.size.height), width: Double(originalImage.size.width), mimeType: mimeType, blurhash: nil)) + MXLog.info("No GPS metadata found. Nothing to do.") + return } let count = CGImageSourceGetCount(imageSource) @@ -255,110 +275,111 @@ struct MediaUploadingPreprocessor { let data = NSMutableData() guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, type.identifier as CFString, count, nil) else { - return .failure(.failedStrippingLocationData) + throw .failedStrippingLocationData } CGImageDestinationAddImageFromSource(destination, imageSource, 0, metadataKeysToRemove as NSDictionary) CGImageDestinationFinalize(destination) do { try data.write(to: url) - return .success(.init(url: url, height: Double(originalImage.size.height), width: Double(originalImage.size.width), mimeType: mimeType, blurhash: nil)) } catch { - return .failure(.failedStrippingLocationData) + throw .failedStrippingLocationData } } /// Generates a thumbnail for an image /// - Parameter url: the original image URL /// - Returns: the URL for the resulting thumbnail and its sizing info as an `ImageProcessingResult` - private func generateThumbnailForImage(at url: URL) async -> Result { - switch await resizeImage(at: url, targetSize: Constants.maximumThumbnailSize) { - case .success(let thumbnail): - guard let data = thumbnail.jpegData(compressionQuality: Constants.thumbnailCompressionQuality) else { - return .failure(.failedGeneratingImageThumbnail(nil)) - } - - let blurhash = thumbnail.blurHash(numberOfComponents: (3, 3)) - - do { - let fileName = "thumbnail-\((url.lastPathComponent as NSString).deletingPathExtension).jpeg" - let thumbnailURL = url.deletingLastPathComponent().appendingPathComponent(fileName) - try data.write(to: thumbnailURL) - return .success(.init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash)) - } catch { - return .failure(.failedGeneratingImageThumbnail(error)) - } - - case .failure(let error): - return .failure(.failedGeneratingImageThumbnail(error)) + private func generateThumbnailForImage(at url: URL) throws(MediaUploadingPreprocessorError) -> ImageProcessingInfo { + let thumbnailFileName = "thumbnail-\((url.lastPathComponent as NSString).deletingPathExtension).jpeg" + let thumbnailURL = url.deletingLastPathComponent().appendingPathComponent(thumbnailFileName) + let thumbnailMaxPixelSize = max(Constants.maximumThumbnailSize.height, Constants.maximumThumbnailSize.width) + + do { + try resizeImage(at: url, maxPixelSize: thumbnailMaxPixelSize, destination: thumbnailURL, type: .jpeg) + } catch { + throw .failedGeneratingImageThumbnail(error) + } + + guard let thumbnail = try? UIImage(contentsOf: thumbnailURL, cachePolicy: .useProtocolCachePolicy) else { + throw .failedGeneratingImageThumbnail(nil) } + let blurhash = thumbnail.blurHash(numberOfComponents: (3, 3)) + + return .init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash) } - private func resizeImage(at url: URL, targetSize: CGSize) async -> Result { - let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) - guard let imageSource else { - return .failure(.failedResizingImage) + private func resizeImage(at url: URL, maxPixelSize: CGFloat, destination: URL, type: UTType) throws(MediaUploadingPreprocessorError) { + guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else { + throw .failedResizingImage } - return await resizeImage(withSource: imageSource, targetSize: targetSize) + try resizeImage(withSource: imageSource, maxPixelSize: maxPixelSize, destination: destination, type: type) } /// Aspect ratio resizes an image so it fits in the given size. This is useful for resizing images without loading them directly into memory /// - Parameters: /// - imageSource: the original image `CGImageSource` - /// - targetSize: maximum resulting size + /// - maxPixelSize: maximum resulting size for the largest dimension of the image. /// - Returns: the resized image - private func resizeImage(withSource imageSource: CGImageSource, targetSize: CGSize) async -> Result { - let maximumSize = max(targetSize.height, targetSize.width) - + private func resizeImage(withSource imageSource: CGImageSource, maxPixelSize: CGFloat, destination destinationURL: URL, type: UTType) throws(MediaUploadingPreprocessorError) { let options: [NSString: Any] = [ // The maximum width and height in pixels of a thumbnail. - kCGImageSourceThumbnailMaxPixelSize: maximumSize, + kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, kCGImageSourceCreateThumbnailFromImageAlways: true, // Should include kCGImageSourceCreateThumbnailWithTransform: true in the options dictionary. Otherwise, the image result will appear rotated when an image is taken from camera in the portrait orientation. kCGImageSourceCreateThumbnailWithTransform: true ] - guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { - return .failure(.failedResizingImage) + guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as NSDictionary), + let destination = CGImageDestinationCreateWithURL(destinationURL as CFURL, type.identifier as CFString, 1, nil) else { + throw .failedResizingImage } - - return .success(UIImage(cgImage: scaledImage)) + let properties = [kCGImageDestinationLossyCompressionQuality: Constants.jpegCompressionQuality] + + CGImageDestinationAddImage(destination, scaledImage, properties as NSDictionary) + CGImageDestinationFinalize(destination) } - // MARK: Videos + // MARK: Video Helpers /// Generates a thumbnail for the video at the given URL /// - Parameter url: the video URL /// - Returns: the URL for the resulting thumbnail and its sizing info as an `ImageProcessingResult` - private func generateThumbnailForVideoAt(_ url: URL) async -> Result { + private func generateThumbnailForVideoAt(_ url: URL) async throws(MediaUploadingPreprocessorError) -> ImageProcessingInfo { let assetImageGenerator = AVAssetImageGenerator(asset: AVAsset(url: url)) assetImageGenerator.appliesPreferredTrackTransform = true assetImageGenerator.maximumSize = Constants.maximumThumbnailSize + // Avoid the first frames as on a lot of videos they're black. + // If the specified seconds are longer than the actual video a frame close to the end of the video will be used, at AVFoundation's discretion + let location = CMTime(seconds: Constants.videoThumbnailTime, preferredTimescale: 1) + + let cgImage: CGImage + do { + cgImage = try await assetImageGenerator.image(at: location).image + } catch { + throw .failedGeneratingVideoThumbnail(error) + } + + let thumbnail = UIImage(cgImage: cgImage) + + guard let data = thumbnail.jpegData(compressionQuality: Constants.jpegCompressionQuality) else { + throw .failedGeneratingVideoThumbnail(nil) + } + + let blurhash = thumbnail.blurHash(numberOfComponents: (3, 3)) + + let fileName = "\((url.lastPathComponent as NSString).deletingPathExtension).jpeg" + let thumbnailURL = url.deletingLastPathComponent().appendingPathComponent(fileName) + do { - // Avoid the first frames as on a lot of videos they're black. - // If the specified seconds are longer than the actual video a frame close to the end of the video will be used, at AVFoundation's discretion - let location = CMTime(seconds: Constants.videoThumbnailTime, preferredTimescale: 1) - let cgImage = try await assetImageGenerator.image(at: location).image - - let thumbnail = UIImage(cgImage: cgImage) - - guard let data = thumbnail.jpegData(compressionQuality: Constants.thumbnailCompressionQuality) else { - return .failure(.failedGeneratingVideoThumbnail(nil)) - } - - let blurhash = thumbnail.blurHash(numberOfComponents: (3, 3)) - - let fileName = "\((url.lastPathComponent as NSString).deletingPathExtension).jpeg" - let thumbnailURL = url.deletingLastPathComponent().appendingPathComponent(fileName) try data.write(to: thumbnailURL) - - return .success(.init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash)) - } catch { - return .failure(.failedGeneratingVideoThumbnail(error)) + throw .failedGeneratingVideoThumbnail(error) } + + return .init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash) } /// Converts the given video to an 1080p mp4 @@ -366,11 +387,12 @@ struct MediaUploadingPreprocessor { /// - url: the original video URL /// - targetFileSize: the maximum resulting file size. 90% of this will be used /// - Returns: the URL for the resulting video and its media info as a `VideoProcessingResult` - private func convertVideoToMP4(_ url: URL, targetFileSize: UInt = 0) async -> Result { + private func convertVideoToMP4(_ url: URL, targetFileSize: UInt = 0) async throws(MediaUploadingPreprocessorError) -> VideoProcessingInfo { let asset = AVURLAsset(url: url) + let presetName = appSettings.optimizeMediaUploads ? AVAssetExportPreset1280x720 : AVAssetExportPreset1920x1080 - guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1920x1080) else { - return .failure(.failedConvertingVideo) + guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else { + throw .failedConvertingVideo } // AVAssetExportSession will fail if the output URL already exists @@ -384,7 +406,7 @@ struct MediaUploadingPreprocessor { exportSession.outputFileType = AVFileType.mp4 guard exportSession.supportedFileTypes.contains(AVFileType.mp4) else { - return .failure(.failedConvertingVideo) + throw .failedConvertingVideo } if targetFileSize > 0 { @@ -394,33 +416,56 @@ struct MediaUploadingPreprocessor { await exportSession.export() - switch exportSession.status { - case .completed: - do { - // Delete the original - try? FileManager.default.removeItem(at: url) - // Strip the UUID from the new version - let newOutputURL = url.deletingLastPathComponent().appendingPathComponent("\(originalFilenameWithoutExtension).mp4") - try FileManager.default.moveItem(at: outputURL, to: newOutputURL) - - let newAsset = AVURLAsset(url: newOutputURL) - guard let track = try? await newAsset.loadTracks(withMediaType: .video).first, - let durationInSeconds = try? await newAsset.load(.duration).seconds, - let adjustedNaturalSize = try? await track.size else { - return .failure(.failedConvertingVideo) - } - - return .success(.init(url: newOutputURL, - height: adjustedNaturalSize.height, - width: adjustedNaturalSize.width, - duration: durationInSeconds, - mimeType: "video/mp4")) - } catch { - return .failure(.failedConvertingVideo) + guard exportSession.status == .completed else { + throw .failedConvertingVideo + } + + // Delete the original + try? FileManager.default.removeItem(at: url) + // Strip the UUID from the new version + let newOutputURL = url.deletingLastPathComponent().appendingPathComponent("\(originalFilenameWithoutExtension).mp4") + + do { try FileManager.default.moveItem(at: outputURL, to: newOutputURL) } catch { + throw .failedConvertingVideo + } + + let newAsset = AVURLAsset(url: newOutputURL) + guard let track = try? await newAsset.loadTracks(withMediaType: .video).first, + let durationInSeconds = try? await newAsset.load(.duration).seconds, + let adjustedNaturalSize = try? await track.size else { + throw .failedConvertingVideo + } + + return .init(url: newOutputURL, + height: adjustedNaturalSize.height, + width: adjustedNaturalSize.width, + duration: durationInSeconds, + mimeType: "video/mp4") + } +} + +// MARK: - Extensions + +private extension CGImageSource { + var size: CGSize? { + guard let properties = CGImageSourceCopyPropertiesAtIndex(self, 0, nil) as? [NSString: Any], + var width = properties[kCGImagePropertyPixelWidth] as? Int, + var height = properties[kCGImagePropertyPixelHeight] as? Int else { + return nil + } + + // Make sure the width and height are the correct way around if an orientation is set. + if let orientationValue = properties[kCGImagePropertyOrientation] as? UInt32, + let orientation = CGImagePropertyOrientation(rawValue: orientationValue) { + switch orientation { + case .up, .down, .upMirrored, .downMirrored: + break + case .left, .right, .leftMirrored, .rightMirrored: + swap(&width, &height) } - default: - return .failure(.failedConvertingVideo) } + + return CGSize(width: width, height: height) } } diff --git a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift index 427aeb77c5..127dd66d50 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift @@ -34,8 +34,12 @@ actor MediaLoader: MediaLoaderProtocol { } } - func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy { - let result = try await client.getMediaFile(mediaSource: source.underlyingSource, body: body, mimeType: source.mimeType ?? "application/octet-stream", useCache: true, tempDir: nil) + func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy { + let result = try await client.getMediaFile(mediaSource: source.underlyingSource, + filename: filename, + mimeType: source.mimeType ?? "application/octet-stream", + useCache: true, + tempDir: nil) return MediaFileHandleProxy(handle: result) } diff --git a/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift b/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift index a19ef95a81..7600f47358 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift @@ -13,5 +13,5 @@ protocol MediaLoaderProtocol { func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data - func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy + func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy } diff --git a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift index b87f42603e..a3c3ab7d28 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift @@ -38,8 +38,8 @@ struct MediaProvider: MediaProviderProtocol { } let cacheKey = cacheKeyForURL(source.url, size: size) - - if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey), + + if let cacheResult = try? await imageCache.retrieveImage(forKey: cacheKey, options: nil), let image = cacheResult.image { return .success(image) } @@ -57,7 +57,7 @@ struct MediaProvider: MediaProviderProtocol { return .failure(.invalidImageData) } - imageCache.store(image, forKey: cacheKey) + try await imageCache.store(image, forKey: cacheKey) return .success(image) } catch { @@ -117,9 +117,9 @@ struct MediaProvider: MediaProviderProtocol { // MARK: Files - func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result { + func loadFileFromSource(_ source: MediaSourceProxy, filename: String?) async -> Result { do { - let file = try await mediaLoader.loadMediaFileForSource(source, body: body) + let file = try await mediaLoader.loadMediaFileForSource(source, filename: filename) return .success(file) } catch { MXLog.error("Failed retrieving file with error: \(error)") @@ -149,13 +149,3 @@ struct MediaProvider: MediaProviderProtocol { } } } - -private extension ImageCache { - func retrieveImage(forKey key: String) async -> Result { - await withCheckedContinuation { continuation in - retrieveImage(forKey: key) { result in - continuation.resume(returning: result) - } - } - } -} diff --git a/ElementX/Sources/Services/Media/Provider/MediaProviderProtocol.swift b/ElementX/Sources/Services/Media/Provider/MediaProviderProtocol.swift index 7306b458e7..6a59172eca 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaProviderProtocol.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaProviderProtocol.swift @@ -16,6 +16,7 @@ enum MediaProviderError: Error { case cancelled } +// sourcery: AutoMockable protocol MediaProviderProtocol { func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result @@ -24,7 +25,7 @@ protocol MediaProviderProtocol { func loadThumbnailForSource(source: MediaSourceProxy, size: CGSize) async -> Result - func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result + func loadFileFromSource(_ source: MediaSourceProxy, filename: String?) async -> Result } extension MediaProviderProtocol { @@ -37,6 +38,6 @@ extension MediaProviderProtocol { } func loadFileFromSource(_ source: MediaSourceProxy) async -> Result { - await loadFileFromSource(source, body: nil) + await loadFileFromSource(source, filename: nil) } } diff --git a/ElementX/Sources/Services/Media/Provider/MockMediaProvider.swift b/ElementX/Sources/Services/Media/Provider/MockMediaProvider.swift deleted file mode 100644 index 3a77954da2..0000000000 --- a/ElementX/Sources/Services/Media/Provider/MockMediaProvider.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Foundation -import UIKit - -struct MockMediaProvider: MediaProviderProtocol { - func loadThumbnailForSource(source: MediaSourceProxy, size: CGSize) async -> Result { - fatalError("Not implemented") - } - - func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? { - guard source != nil else { - return nil - } - - if source?.url == .picturesDirectory { - return Asset.Images.appLogo.image - } - - return UIImage(systemName: "photo") - } - - func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result { - guard let image = UIImage(systemName: "photo") else { - fatalError() - } - - return .success(image) - } - - func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result { - guard let image = UIImage(systemName: "photo"), - let data = image.pngData() else { - fatalError() - } - - return .success(data) - } - - var loadFileFromSourceReturnValue: MediaFileHandleProxy? - func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result { - if let loadFileFromSourceReturnValue { - return .success(loadFileFromSourceReturnValue) - } - return .failure(.failedRetrievingFile) - } - - func loadImageRetryingOnReconnection(_ source: MediaSourceProxy, size: CGSize?) -> Task { - Task { - guard let image = UIImage(systemName: "photo") else { - fatalError() - } - - return image - } - } -} diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift index 015a6a9d09..d1f4eaad2f 100644 --- a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift +++ b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift @@ -81,7 +81,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol { sessionDirectories = .init() } - private func userSession(for client: Client) async -> Result { + private func userSession(for client: ClientProtocol) async -> Result { switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let session): return .success(session) diff --git a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift index 291bfa3304..4e2be76cc0 100644 --- a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift @@ -17,68 +17,15 @@ class InvitedRoomProxy: InvitedRoomProxyProtocol { // multiple times over FFI lazy var id: String = room.id() - var canonicalAlias: String? { - room.canonicalAlias() - } - - var ownUserID: String { - room.ownUserId() - } - - var name: String? { - roomListItem.displayName() - } - - var topic: String? { - room.topic() - } + var ownUserID: String { room.ownUserId() } - var avatarURL: URL? { - roomListItem.avatarUrl().flatMap(URL.init(string:)) - } - - var avatar: RoomAvatar { - if isDirect, avatarURL == nil { - let heroes = room.heroes() - - if heroes.count == 1 { - return .heroes(heroes.map(UserProfileProxy.init)) - } - } - - return .room(id: id, name: name, avatarURL: avatarURL) - } - - var isDirect: Bool { - room.isDirect() - } - - var isPublic: Bool { - room.isPublic() - } - - var isSpace: Bool { - room.isSpace() - } - - var joinedMembersCount: Int { - Int(room.joinedMembersCount()) - } - - var activeMembersCount: Int { - Int(room.activeMembersCount()) - } - - var inviter: RoomMemberProxyProtocol? { - get async { - await (try? roomListItem.roomInfo().inviter).map(RoomMemberProxy.init) - } - } + let info: RoomInfoProxy init(roomListItem: RoomListItemProtocol, - room: RoomProtocol) { + room: RoomProtocol) async throws { self.roomListItem = roomListItem self.room = room + info = try await RoomInfoProxy(roomInfo: room.roomInfo()) } func acceptInvitation() async -> Result { diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index e23cd573a1..30211cc367 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -58,8 +58,15 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { private var roomInfoObservationToken: TaskHandle? // periphery:ignore - required for instance retention in the rust codebase private var typingNotificationObservationToken: TaskHandle? + // periphery:ignore - required for instance retention in the rust codebase + private var identityStatusChangesObservationToken: TaskHandle? private var subscribedForUpdates = false + + private let infoSubject: CurrentValueSubject + var infoPublisher: CurrentValuePublisher { + infoSubject.asCurrentValuePublisher() + } private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { @@ -70,95 +77,22 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { var typingMembersPublisher: CurrentValuePublisher<[String], Never> { typingMembersSubject.asCurrentValuePublisher() } - - private let actionsSubject = PassthroughSubject() - var actionsPublisher: AnyPublisher { - actionsSubject.eraseToAnyPublisher() + + private let identityStatusChangesSubject = CurrentValueSubject<[IdentityStatusChange], Never>([]) + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { + identityStatusChangesSubject.asCurrentValuePublisher() } // A room identifier is constant and lazy stops it from being fetched // multiple times over FFI lazy var id: String = room.id() - - var canonicalAlias: String? { - room.canonicalAlias() - } - - var ownUserID: String { - room.ownUserId() - } - - var name: String? { - roomListItem.displayName() - } - - var topic: String? { - room.topic() - } - - var avatarURL: URL? { - roomListItem.avatarUrl().flatMap(URL.init(string:)) - } - - var avatar: RoomAvatar { - if isDirect, avatarURL == nil { - let heroes = room.heroes() - - if heroes.count == 1 { - return .heroes(heroes.map(UserProfileProxy.init)) - } - } - - return .room(id: id, name: name, avatarURL: avatarURL) - } - - var isDirect: Bool { - room.isDirect() - } - - var isPublic: Bool { - room.isPublic() - } - - var isSpace: Bool { - room.isSpace() - } - - var joinedMembersCount: Int { - Int(room.joinedMembersCount()) - } - - var activeMembersCount: Int { - Int(room.activeMembersCount()) - } + var ownUserID: String { room.ownUserId() } + var info: RoomInfoProxy { infoSubject.value } var isEncrypted: Bool { (try? room.isEncrypted()) ?? false } - var isFavourite: Bool { - get async { - await (try? room.roomInfo().isFavourite) ?? false - } - } - - var pinnedEventIDs: Set { - get async { - guard let pinnedEventIDs = try? await room.roomInfo().pinnedEventIds else { - return [] - } - return .init(pinnedEventIDs) - } - } - - var hasOngoingCall: Bool { - room.hasActiveRoomCall() - } - - var activeRoomCallParticipants: [String] { - room.activeRoomCallParticipants() - } - init(roomListService: RoomListServiceProtocol, roomListItem: RoomListItemProtocol, room: RoomProtocol) async throws { @@ -166,6 +100,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { self.roomListItem = roomListItem self.room = room + infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo())) timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live) Task { @@ -180,11 +115,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } subscribedForUpdates = true - let settings = RoomSubscription(requiredState: SlidingSyncConstants.defaultRequiredState, - timelineLimit: SlidingSyncConstants.defaultTimelineLimit, - includeHeroes: false) // We don't need heroes here as they're already included in the `all_rooms` list + do { - try roomListService.subscribeToRooms(roomIds: [id], settings: settings) + try roomListService.subscribeToRooms(roomIds: [id]) } catch { MXLog.error("Failed subscribing to room with error: \(error)") } @@ -193,6 +126,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { subscribeToRoomInfoUpdates() + if isEncrypted { + subscribeToIdentityStatusChanges() + } + subscribeToTypingNotifications() } @@ -201,9 +138,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { return } - roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in + roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] roomInfo in MXLog.info("Received room info update") - self?.actionsSubject.send(.roomInfoUpdate) + self?.infoSubject.send(.init(roomInfo: roomInfo)) }) } @@ -369,8 +306,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } func sendTypingNotification(isTyping: Bool) async -> Result { - MXLog.info("Sending typing notification isTyping: \(isTyping)") - do { try await room.typingNotice(isTyping: isTyping) return .success(()) @@ -710,17 +645,27 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { typingMembersSubject.send(typingMembers) }) } + + private func subscribeToIdentityStatusChanges() { + identityStatusChangesObservationToken = room.subscribeToIdentityStatusChanges(listener: RoomIdentityStatusChangeListener { [weak self] changes in + guard let self else { return } + + MXLog.info("Received identity status changes: \(changes)") + + identityStatusChangesSubject.send(changes) + }) + } } private final class RoomInfoUpdateListener: RoomInfoListener { - private let onUpdateClosure: () -> Void + private let onUpdateClosure: (RoomInfo) -> Void - init(_ onUpdateClosure: @escaping () -> Void) { + init(_ onUpdateClosure: @escaping (RoomInfo) -> Void) { self.onUpdateClosure = onUpdateClosure } func call(roomInfo: RoomInfo) { - onUpdateClosure() + onUpdateClosure(roomInfo) } } @@ -735,3 +680,15 @@ private final class RoomTypingNotificationUpdateListener: TypingNotificationsLis onUpdateClosure(typingUserIds) } } + +private final class RoomIdentityStatusChangeListener: IdentityStatusChangeListener { + private let onUpdateClosure: ([IdentityStatusChange]) -> Void + + init(_ onUpdateClosure: @escaping ([IdentityStatusChange]) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func call(identityStatusChange: [IdentityStatusChange]) { + onUpdateClosure(identityStatusChange) + } +} diff --git a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift new file mode 100644 index 0000000000..74b640dc54 --- /dev/null +++ b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift @@ -0,0 +1,39 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK +import UIKit + +class KnockedRoomProxy: KnockedRoomProxyProtocol { + private let roomListItem: RoomListItemProtocol + private let room: RoomProtocol + + // A room identifier is constant and lazy stops it from being fetched + // multiple times over FFI + lazy var id: String = room.id() + + var ownUserID: String { room.ownUserId() } + + let info: RoomInfoProxy + + init(roomListItem: RoomListItemProtocol, + room: RoomProtocol) async throws { + self.roomListItem = roomListItem + self.room = room + info = try await RoomInfoProxy(roomInfo: room.roomInfo()) + } + + func cancelKnock() async -> Result { + do { + return try await .success(room.leave()) + } catch { + MXLog.error("Failed cancelling the knock with error: \(error)") + return .failure(.sdkError(error)) + } + } +} diff --git a/ElementX/Sources/Services/Room/RoomInfoProxy.swift b/ElementX/Sources/Services/Room/RoomInfoProxy.swift new file mode 100644 index 0000000000..e402031310 --- /dev/null +++ b/ElementX/Sources/Services/Room/RoomInfoProxy.swift @@ -0,0 +1,56 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +struct RoomInfoProxy { + let roomInfo: RoomInfo + + var id: String { roomInfo.id } + var creator: String? { roomInfo.creator } + var displayName: String? { roomInfo.displayName } + var rawName: String? { roomInfo.rawName } + var topic: String? { roomInfo.topic } + /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. + var avatarURL: URL? { roomInfo.avatarUrl.flatMap(URL.init) } + /// The room's avatar info for use in a ``RoomAvatarImage``. + var avatar: RoomAvatar { + if isDirect, avatarURL == nil { + if heroes.count == 1 { + return .heroes(heroes.map(UserProfileProxy.init)) + } + } + + return .room(id: id, name: displayName, avatarURL: avatarURL) + } + + var isDirect: Bool { roomInfo.isDirect } + var isPublic: Bool { roomInfo.isPublic } + var isSpace: Bool { roomInfo.isSpace } + var isTombstoned: Bool { roomInfo.isTombstoned } + var isFavourite: Bool { roomInfo.isFavourite } + var canonicalAlias: String? { roomInfo.canonicalAlias } + var alternativeAliases: [String] { roomInfo.alternativeAliases } + var membership: Membership { roomInfo.membership } + var inviter: RoomMemberProxy? { roomInfo.inviter.map(RoomMemberProxy.init) } + var heroes: [RoomHero] { roomInfo.heroes } + var activeMembersCount: Int { Int(roomInfo.activeMembersCount) } + var invitedMembersCount: Int { Int(roomInfo.invitedMembersCount) } + var joinedMembersCount: Int { Int(roomInfo.joinedMembersCount) } + var userPowerLevels: [String: Int] { roomInfo.userPowerLevels.mapValues(Int.init) } + var highlightCount: Int { Int(roomInfo.highlightCount) } + var notificationCount: Int { Int(roomInfo.notificationCount) } + var cachedUserDefinedNotificationMode: RoomNotificationMode? { roomInfo.cachedUserDefinedNotificationMode } + var hasRoomCall: Bool { roomInfo.hasRoomCall } + var activeRoomCallParticipants: [String] { roomInfo.activeRoomCallParticipants } + var isMarkedUnread: Bool { roomInfo.isMarkedUnread } + var unreadMessagesCount: UInt { UInt(roomInfo.numUnreadMessages) } + var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) } + var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) } + var pinnedEventIDs: Set { Set(roomInfo.pinnedEventIds) } +} diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift index 856ec67ae2..e583a83c14 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift @@ -16,12 +16,24 @@ final class RoomMemberProxy: RoomMemberProxyProtocol { } var userID: String { member.userId } + var displayName: String? { member.displayName } + + var disambiguatedDisplayName: String? { + guard let displayName else { + return nil + } + + return member.isNameAmbiguous ? "\(displayName) (\(userID))" : displayName + } + var avatarURL: URL? { member.avatarUrl.flatMap(URL.init(string:)) } var membership: MembershipState { member.membership } + var isIgnored: Bool { member.isIgnored } var powerLevel: Int { Int(member.powerLevel) } + var role: RoomMemberRole { member.suggestedRoleForPowerLevel } } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index 6c1fe0afe2..817f6b57d4 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -11,13 +11,18 @@ import MatrixRustSDK // sourcery: AutoMockable protocol RoomMemberProxyProtocol: AnyObject { var userID: String { get } + var displayName: String? { get } + var disambiguatedDisplayName: String? { get } + var avatarURL: URL? { get } var membership: MembershipState { get } + var isIgnored: Bool { get } var powerLevel: Int { get } + var role: RoomMemberRole { get } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index d2d99406cb..cf9c01fd8e 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -21,59 +21,44 @@ enum RoomProxyError: Error { enum RoomProxyType { case joined(JoinedRoomProxyProtocol) case invited(InvitedRoomProxyProtocol) + case knocked(KnockedRoomProxyProtocol) case left } // sourcery: AutoMockable protocol RoomProxyProtocol { var id: String { get } - var canonicalAlias: String? { get } - var ownUserID: String { get } - - var name: String? { get } - var topic: String? { get } - - /// The room's avatar info for use in a ``RoomAvatarImage``. - var avatar: RoomAvatar { get } - /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. - var avatarURL: URL? { get } - - var isPublic: Bool { get } - var isDirect: Bool { get } - var isSpace: Bool { get } - - var joinedMembersCount: Int { get } - - var activeMembersCount: Int { get } } // sourcery: AutoMockable protocol InvitedRoomProxyProtocol: RoomProxyProtocol { - var inviter: RoomMemberProxyProtocol? { get async } - + var info: RoomInfoProxy { get } func rejectInvitation() async -> Result func acceptInvitation() async -> Result } -enum JoinedRoomProxyAction { +// sourcery: AutoMockable +protocol KnockedRoomProxyProtocol: RoomProxyProtocol { + var info: RoomInfoProxy { get } + func cancelKnock() async -> Result +} + +enum JoinedRoomProxyAction: Equatable { case roomInfoUpdate } // sourcery: AutoMockable protocol JoinedRoomProxyProtocol: RoomProxyProtocol { var isEncrypted: Bool { get } - var isFavourite: Bool { get async } - var pinnedEventIDs: Set { get async } - var hasOngoingCall: Bool { get } - var activeRoomCallParticipants: [String] { get } + var infoPublisher: CurrentValuePublisher { get } var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get } - var actionsPublisher: AnyPublisher { get } + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get } var timeline: TimelineProxyProtocol { get } @@ -168,21 +153,15 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { extension JoinedRoomProxyProtocol { var details: RoomDetails { RoomDetails(id: id, - name: name, - avatar: avatar, - canonicalAlias: canonicalAlias, + name: infoPublisher.value.displayName, + avatar: infoPublisher.value.avatar, + canonicalAlias: infoPublisher.value.canonicalAlias, isEncrypted: isEncrypted, - isPublic: isPublic) - } - - // Avoids to duplicate the same logic around in the app - // Probably this should be done in rust. - var roomTitle: String { - name ?? "Unknown room 💥" + isPublic: infoPublisher.value.isPublic) } var isEncryptedOneToOneRoom: Bool { - isDirect && isEncrypted && activeMembersCount <= 2 + infoPublisher.value.isDirect && isEncrypted && infoPublisher.value.activeMembersCount <= 2 } func members() async -> [RoomMemberProxyProtocol]? { diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index beed3f206a..a5948fc4e7 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -23,10 +23,12 @@ struct RoomEventStringBuilder { sender.displayName ?? sender.id } - switch eventItemProxy.content.kind() { + switch eventItemProxy.content { case .unableToDecrypt(let encryptedMessage): let errorMessage = switch encryptedMessage { - case .megolmV1AesSha2(_, .membership): L10n.commonUnableToDecryptNoAccess + case .megolmV1AesSha2(_, .sentBeforeWeJoined): L10n.commonUnableToDecryptNoAccess + case .megolmV1AesSha2(_, .verificationViolation): L10n.commonUnableToDecryptVerificationViolation + case .megolmV1AesSha2(_, .unknownDevice), .megolmV1AesSha2(_, .unsignedDevice): L10n.commonUnableToDecryptInsecureDevice default: L10n.commonWaitingForDecryptionKey } return prefix(errorMessage, with: displayName) @@ -41,13 +43,8 @@ struct RoomEventStringBuilder { return prefix(L10n.commonSticker, with: displayName) case .failedToParseMessageLike, .failedToParseState: return prefix(L10n.commonUnsupportedEvent, with: displayName) - case .message: - guard let messageContent = eventItemProxy.content.asMessage() else { - fatalError("Invalid message timeline item: \(eventItemProxy)") - } - - let messageType = messageContent.msgtype() - return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName) + case .message(let messageContent): + return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName) case .state(_, let state): return stateEventStringBuilder .buildString(for: state, sender: sender, isOutgoing: isOutgoing) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 942a78704f..429cba58ad 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -9,12 +9,32 @@ import Foundation import MatrixRustSDK struct RoomSummary { + enum JoinRequestType { + case invite(inviter: RoomMemberProxyProtocol?) + case knock + + var isInvite: Bool { + if case .invite = self { + return true + } else { + return false + } + } + + var isKnock: Bool { + if case .knock = self { + return true + } else { + return false + } + } + } + let roomListItem: RoomListItem let id: String - let isInvite: Bool - let inviter: RoomMemberProxyProtocol? + let joinRequestType: JoinRequestType? let name: String let isDirect: Bool @@ -67,10 +87,9 @@ extension RoomSummary { unreadNotificationsCount = hasUnreadNotifications ? 1 : 0 notificationMode = settingsMode canonicalAlias = nil - inviter = nil hasOngoingCall = false - isInvite = false + joinRequestType = nil isMarkedUnread = false isFavourite = false } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 0d9921a707..4622c5661e 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -176,10 +176,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { guard let self else { return } do { - try roomListService.subscribeToRooms(roomIds: roomIDs, - settings: .init(requiredState: SlidingSyncConstants.defaultRequiredState, - timelineLimit: SlidingSyncConstants.defaultTimelineLimit, - includeHeroes: false)) + try roomListService.subscribeToRooms(roomIds: roomIDs) } catch { MXLog.error("Failed subscribing to rooms with error: \(error)") } @@ -246,7 +243,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { var lastMessageFormattedTimestamp: String? if let latestRoomMessage = roomDetails.latestEvent { - let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: "0") + let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, uniqueID: .init(id: "0")) lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal() attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage) } @@ -258,10 +255,15 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) } + let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership { + case .invited: .invite(inviter: inviterProxy) + case .knocked: .knock + default: nil + } + return RoomSummary(roomListItem: roomListItem, id: roomInfo.id, - isInvite: roomInfo.membership == .invited, - inviter: inviterProxy, + joinRequestType: joinRequestType, name: roomInfo.displayName ?? roomInfo.id, isDirect: roomInfo.isDirect, avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)), diff --git a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift index a07767bce1..7b1a74b63e 100644 --- a/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift +++ b/ElementX/Sources/Services/RoomDirectorySearch/RoomDirectorySearchProxy.swift @@ -47,7 +47,7 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol { func search(query: String?) async -> Result { do { - try await roomDirectorySearch.search(filter: query, batchSize: 50) + try await roomDirectorySearch.search(filter: query, batchSize: 50, viaServerName: nil) return .success(()) } catch { return .failure(.searchFailed) diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift index fc6c81256f..ab514800de 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -121,7 +121,7 @@ class SecureBackupController: SecureBackupControllerProtocol { MXLog.info("Enabling recovery") var keyUploadErrored = false - let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in + let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: nil, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in guard let self else { return } switch state { diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift index 4ae2052ef2..72c77e1f12 100644 --- a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift @@ -18,6 +18,10 @@ private class WeakSessionVerificationControllerProxy: SessionVerificationControl // MARK: - SessionVerificationControllerDelegate + func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) { + proxy?.didReceiveVerificationRequest(details: details) + } + func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) { switch data { // We can handle only emojis for now @@ -54,86 +58,142 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt init(sessionVerificationController: SessionVerificationController) { self.sessionVerificationController = sessionVerificationController + sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self)) } deinit { sessionVerificationController.setDelegate(delegate: nil) } - let callbacks = PassthroughSubject() + let actions = PassthroughSubject() + + func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result { + MXLog.info("Acknowledging verification request") + + do { + try await sessionVerificationController.acknowledgeVerificationRequest(senderId: details.senderID, flowId: details.flowID) + return .success(()) + } catch { + MXLog.error("Failed requesting session verification with error: \(error)") + return .failure(.failedAcknowledgingVerificationRequest) + } + } + + func acceptVerificationRequest() async -> Result { + MXLog.info("Accepting verification request") + + do { + try await sessionVerificationController.acceptVerificationRequest() + return .success(()) + } catch { + MXLog.error("Failed requesting session verification with error: \(error)") + return .failure(.failedAcceptingVerificationRequest) + } + } func requestVerification() async -> Result { - sessionVerificationController.setDelegate(delegate: WeakSessionVerificationControllerProxy(proxy: self)) + MXLog.info("Requesting session verification") do { try await sessionVerificationController.requestVerification() return .success(()) } catch { + MXLog.error("Failed requesting session verification with error: \(error)") return .failure(.failedRequestingVerification) } } func startSasVerification() async -> Result { + MXLog.info("Starting SAS verification") + do { try await sessionVerificationController.startSasVerification() return .success(()) } catch { + MXLog.error("Failed starting SAS verification with error: \(error)") return .failure(.failedStartingSasVerification) } } func approveVerification() async -> Result { + MXLog.info("Approving verification") + do { try await sessionVerificationController.approveVerification() return .success(()) } catch { + MXLog.error("Failed approving verification with error: \(error)") return .failure(.failedApprovingVerification) } } func declineVerification() async -> Result { + MXLog.info("Declining verification") + do { try await sessionVerificationController.declineVerification() return .success(()) } catch { + MXLog.error("Failed declining verification with error: \(error)") return .failure(.failedDecliningVerification) } } func cancelVerification() async -> Result { + MXLog.info("Cancelling verification") + do { try await sessionVerificationController.cancelVerification() return .success(()) } catch { + MXLog.error("Failed cancelling verification with error: \(error)") return .failure(.failedCancellingVerification) } } // MARK: - Private + fileprivate func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) { + MXLog.info("Received verification request \(details)") + + let details = SessionVerificationRequestDetails(senderID: details.senderId, + flowID: details.flowId, + deviceID: details.deviceId, + displayName: details.displayName, + firstSeenDate: Date(timeIntervalSince1970: TimeInterval(details.firstSeenTimestamp / 1000))) + + actions.send(.receivedVerificationRequest(details: details)) + } + fileprivate func didAcceptVerificationRequest() { - callbacks.send(.acceptedVerificationRequest) + MXLog.info("Accepted verification request") + + actions.send(.acceptedVerificationRequest) } fileprivate func didStartSasVerification() { - callbacks.send(.startedSasVerification) + MXLog.info("Started SAS verification") + + actions.send(.startedSasVerification) } fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) { - callbacks.send(.receivedVerificationData(data.map { emoji in + MXLog.info("Received verification data") + + actions.send(.receivedVerificationData(data.map { emoji in SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description()) })) } fileprivate func didFail() { - callbacks.send(.failed) + actions.send(.failed) } fileprivate func didFinish() { - callbacks.send(.finished) + actions.send(.finished) } fileprivate func didCancel() { - callbacks.send(.cancelled) + actions.send(.cancelled) } } diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift index 9d1e95e22c..8f2555245b 100644 --- a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift @@ -7,8 +7,11 @@ import Combine import Foundation +import MatrixRustSDK enum SessionVerificationControllerProxyError: Error { + case failedAcknowledgingVerificationRequest + case failedAcceptingVerificationRequest case failedRequestingVerification case failedStartingSasVerification case failedApprovingVerification @@ -16,7 +19,8 @@ enum SessionVerificationControllerProxyError: Error { case failedCancellingVerification } -enum SessionVerificationControllerProxyCallback { +enum SessionVerificationControllerProxyAction { + case receivedVerificationRequest(details: SessionVerificationRequestDetails) case acceptedVerificationRequest case startedSasVerification case receivedVerificationData([SessionVerificationEmoji]) @@ -25,6 +29,14 @@ enum SessionVerificationControllerProxyCallback { case failed } +struct SessionVerificationRequestDetails { + let senderID: String + let flowID: String + let deviceID: String + let displayName: String? + let firstSeenDate: Date +} + struct SessionVerificationEmoji: Hashable { let symbol: String let description: String @@ -36,7 +48,11 @@ struct SessionVerificationEmoji: Hashable { // sourcery: AutoMockable protocol SessionVerificationControllerProxyProtocol { - var callbacks: PassthroughSubject { get } + var actions: PassthroughSubject { get } + + func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result + + func acceptVerificationRequest() async -> Result func requestVerification() async -> Result diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index c994d8a82e..cf77fa6da9 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -10,9 +10,9 @@ import Foundation enum RoomTimelineItemFixtures { /// The default timeline items used in Xcode previews etc. static var `default`: [RoomTimelineItemProtocol] = [ - SeparatorRoomTimelineItem(id: .init(timelineID: "Yesterday"), text: "Yesterday"), - TextRoomTimelineItem(id: .init(timelineID: ".RoomTimelineItemFixtures.default.0", - eventID: "RoomTimelineItemFixtures.default.0"), + SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Yesterday")), text: "Yesterday"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: ".RoomTimelineItemFixtures.default.0"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")), timestamp: "10:10 AM", isOutgoing: false, isEditable: false, @@ -21,8 +21,8 @@ enum RoomTimelineItemFixtures { sender: .init(id: "", displayName: "Jacob"), content: .init(body: "That looks so good!"), properties: RoomTimelineItemProperties(isEdited: true)), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.1", - eventID: "RoomTimelineItemFixtures.default.1"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.1"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")), timestamp: "10:11 AM", isOutgoing: false, isEditable: false, @@ -33,8 +33,8 @@ enum RoomTimelineItemFixtures { properties: RoomTimelineItemProperties(reactions: [ AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())]) ])), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.2", - eventID: "RoomTimelineItemFixtures.default.2"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.2"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")), timestamp: "10:11 AM", isOutgoing: false, isEditable: false, @@ -52,9 +52,9 @@ enum RoomTimelineItemFixtures { ReactionSender(id: "jacob", timestamp: Date()) ]) ])), - SeparatorRoomTimelineItem(id: .init(timelineID: "Today"), text: "Today"), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.3", - eventID: "RoomTimelineItemFixtures.default.3"), + SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Today")), text: "Today"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.3"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")), timestamp: "5 PM", isOutgoing: false, isEditable: false, @@ -63,8 +63,8 @@ enum RoomTimelineItemFixtures { sender: .init(id: "", displayName: "Helena"), content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"), properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.4", - eventID: "RoomTimelineItemFixtures.default.4"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.4"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")), timestamp: "5 PM", isOutgoing: true, isEditable: true, @@ -72,8 +72,8 @@ enum RoomTimelineItemFixtures { isThreaded: false, sender: .init(id: "", displayName: "Bob"), content: .init(body: "And John's speech was amazing!")), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.5", - eventID: "RoomTimelineItemFixtures.default.5"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.5"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")), timestamp: "5 PM", isOutgoing: true, isEditable: true, @@ -86,8 +86,8 @@ enum RoomTimelineItemFixtures { ReadReceipt(userID: "bob", formattedTimestamp: nil), ReadReceipt(userID: "charlie", formattedTimestamp: nil), ReadReceipt(userID: "dan", formattedTimestamp: nil)])), - TextRoomTimelineItem(id: .init(timelineID: "RoomTimelineItemFixtures.default.6", - eventID: "RoomTimelineItemFixtures.default.6"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.6"), + eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")), timestamp: "5 PM", isOutgoing: false, isEditable: false, @@ -242,16 +242,50 @@ enum RoomTimelineItemFixtures { static var permalinkChunk: [RoomTimelineItemProtocol] { (1...20).map { index in - TextRoomTimelineItem(id: .init(timelineID: "\(index)", eventID: "$\(index)"), + TextRoomTimelineItem(id: .event(uniqueID: .init(id: "\(index)"), eventOrTransactionID: .eventId(eventId: "$\(index)")), text: "Message ID \(index)", senderDisplayName: index > 10 ? "Alice" : "Bob") } } + + static var mediaChunk: [RoomTimelineItemProtocol] { + [ + VideoRoomTimelineItem(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: ""), + content: .init(filename: "video.mp4", + duration: 100, + source: .init(url: .picturesDirectory, mimeType: nil), + thumbnailSource: .init(url: .picturesDirectory, mimeType: nil), + width: 1920, + height: 1080, + aspectRatio: 1.78, + blurhash: "KtI~70X5V?yss9oyrYs:t6")), + ImageRoomTimelineItem(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: ""), + content: .init(filename: "image.jpg", + source: .init(url: .picturesDirectory, mimeType: nil), + thumbnailSource: nil, + width: 5120, + height: 3412, + aspectRatio: 1.5, + blurhash: "KpE4oyayR5|GbHb];3j@of")) + ] + } } private extension TextRoomTimelineItem { init(id: TimelineItemIdentifier? = nil, text: String, senderDisplayName: String) { - self.init(id: id ?? .random, + self.init(id: id ?? .randomEvent, timestamp: "10:47 am", isOutgoing: senderDisplayName == "Alice", isEditable: false, @@ -260,9 +294,7 @@ private extension TextRoomTimelineItem { sender: .init(id: "", displayName: senderDisplayName), content: .init(body: text)) } -} - -private extension TextRoomTimelineItem { + func withReadReceipts(_ receipts: [ReadReceipt]) -> TextRoomTimelineItem { var newSelf = self newSelf.properties.orderedReadReceipts = receipts diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index ce7c96fa33..e446a40ee5 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -184,38 +184,11 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { } } -private extension TimelineItem { - var debugIdentifier: DebugIdentifier { - if let virtualTimelineItem = asVirtual() { - return .virtual(timelineID: String(uniqueId()), dscription: virtualTimelineItem.description) - } else if let eventTimelineItem = asEvent() { - return .event(timelineID: String(uniqueId()), - eventID: eventTimelineItem.eventId(), - transactionID: eventTimelineItem.transactionId()) - } - - return .unknown(timelineID: String(uniqueId())) - } -} - private extension TimelineItemProxy { - var debugIdentifier: DebugIdentifier { - switch self { - case .event(let eventTimelineItem): - return .event(timelineID: eventTimelineItem.id.timelineID, - eventID: eventTimelineItem.id.eventID, - transactionID: eventTimelineItem.id.transactionID) - case .virtual(let virtualTimelineItem, let timelineID): - return .virtual(timelineID: timelineID, dscription: virtualTimelineItem.description) - case .unknown(let item): - return .unknown(timelineID: String(item.uniqueId())) - } - } - var isMembershipChange: Bool { switch self { case .event(let eventTimelineItemProxy): - switch eventTimelineItemProxy.content.kind() { + switch eventTimelineItemProxy.content { case .roomMembership: true default: @@ -238,12 +211,6 @@ private extension VirtualTimelineItem { } } -enum DebugIdentifier { - case event(timelineID: String, eventID: String?, transactionID: String?) - case virtual(timelineID: String, dscription: String) - case unknown(timelineID: String) -} - private final class RoomTimelineListener: TimelineListener { private let onUpdateClosure: ([TimelineDiff]) -> Void diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index d4844c8810..e615165936 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -81,17 +81,17 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func sendMessage(_ message: String, html: String?, - inReplyTo itemID: TimelineItemIdentifier?, + inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async { } - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { } + func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async { } - func edit(_ timelineItemID: TimelineItemIdentifier, + func edit(_ eventOrTransactionID: EventOrTransactionId, message: String, html: String?, intentionalMentions: IntentionalMentions) async { } - func redact(_ itemID: TimelineItemIdentifier) async { } + func redact(_ eventOrTransactionID: EventOrTransactionId) async { } func pin(eventID: String) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 457020d50d..09681aa418 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -143,22 +143,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol { func sendMessage(_ message: String, html: String?, - inReplyTo itemID: TimelineItemIdentifier?, + inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async { - var inReplyTo: String? - if itemID == nil { - MXLog.info("Send message in \(roomID)") - } else if let eventID = itemID?.eventID { - inReplyTo = eventID - MXLog.info("Send reply in \(roomID)") - } else { - MXLog.error("Send reply in \(roomID) failed: missing event ID") - return - } + MXLog.info("Send message in \(roomID)") switch await activeTimeline.sendMessage(message, html: html, - inReplyTo: inReplyTo, + inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions) { case .success: MXLog.info("Finished sending message") @@ -167,10 +158,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { - MXLog.info("Toggle reaction in \(roomID)") + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async { + MXLog.info("Toggle reaction \(reaction) to \(eventOrTransactionID)") - switch await activeTimeline.toggleReaction(reaction, to: itemID) { + switch await activeTimeline.toggleReaction(reaction, to: eventOrTransactionID) { case .success: MXLog.info("Finished toggling reaction") case .failure(let error): @@ -178,50 +169,29 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func edit(_ timelineItemID: TimelineItemIdentifier, + func edit(_ eventOrTransactionID: EventOrTransactionId, message: String, html: String?, intentionalMentions: IntentionalMentions) async { MXLog.info("Edit message in \(roomID)") - MXLog.info("Editing timeline item: \(timelineItemID)") - - let editMode: EditMode - if !timelineItemID.timelineID.isEmpty, - let timelineItem = liveTimelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID) { - editMode = .byEvent(timelineItem) - } else if let eventID = timelineItemID.eventID { - editMode = .byID(eventID) - } else { - MXLog.error("Unknown timeline item: \(timelineItemID)") - return - } + MXLog.info("Editing timeline item: \(eventOrTransactionID)") let messageContent = activeTimeline.buildMessageContentFor(message, html: html, intentionalMentions: intentionalMentions.toRustMentions()) - switch editMode { - case let .byEvent(item): - switch await activeTimeline.edit(item, newContent: messageContent) { - case .success: - MXLog.info("Finished editing message by event") - case let .failure(error): - MXLog.error("Failed editing message by event with error: \(error)") - } - case let .byID(eventID): - switch await roomProxy.edit(eventID: eventID, newContent: messageContent) { - case .success: - MXLog.info("Finished editing message by event ID") - case let .failure(error): - MXLog.error("Failed editing message by event ID with error: \(error)") - } + switch await activeTimeline.edit(eventOrTransactionID, newContent: messageContent) { + case .success: + MXLog.info("Finished editing message by event") + case let .failure(error): + MXLog.error("Failed editing message by event with error: \(error)") } } - func redact(_ timelineItemID: TimelineItemIdentifier) async { + func redact(_ eventOrTransactionID: EventOrTransactionId) async { MXLog.info("Send redaction in \(roomID)") - switch await activeTimeline.redact(timelineItemID, reason: nil) { + switch await activeTimeline.redact(eventOrTransactionID, reason: nil) { case .success: MXLog.info("Finished redacting message") case .failure(let error): @@ -356,7 +326,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch paginationState.backward { case .timelineEndReached: if timelineKind != .pinned, !roomProxy.isEncryptedOneToOneRoom { - let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name) + let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.infoPublisher.value.displayName) newTimelineItems.insert(timelineStart, at: 0) } case .paginating: @@ -392,15 +362,15 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } return timelineItem - case .virtual(let virtualItem, let timelineID): + case .virtual(let virtualItem, let uniqueID): switch virtualItem { case .dayDivider(let timestamp): let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) let dateString = date.formatted(date: .complete, time: .omitted) - return SeparatorRoomTimelineItem(id: .init(timelineID: dateString), text: dateString) + return SeparatorRoomTimelineItem(id: .virtual(uniqueID: uniqueID), text: dateString) case .readMarker: - return ReadMarkerRoomTimelineItem(id: .init(timelineID: timelineID)) + return ReadMarkerRoomTimelineItem(id: .virtual(uniqueID: uniqueID)) } case .unknown: return nil @@ -409,7 +379,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool { if case let .event(eventItem) = item { - switch eventItem.content.kind() { + switch eventItem.content { case .profileChange, .roomMembership, .state: return true default: @@ -453,8 +423,3 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return nil } } - -private enum EditMode { - case byEvent(EventTimelineItem) - case byID(String) -} diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 4ea6719dde..125454d3cc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -48,17 +48,17 @@ protocol RoomTimelineControllerProtocol { func sendMessage(_ message: String, html: String?, - inReplyTo itemID: TimelineItemIdentifier?, + inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async - func edit(_ timelineItemID: TimelineItemIdentifier, + func edit(_ eventOrTransactionID: EventOrTransactionId, message: String, html: String?, intentionalMentions: IntentionalMentions) async - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async - func redact(_ itemID: TimelineItemIdentifier) async + func redact(_ eventOrTransactionID: EventOrTransactionId) async func pin(eventID: String) async @@ -72,14 +72,3 @@ protocol RoomTimelineControllerProtocol { func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? } - -extension RoomTimelineControllerProtocol { - func sendMessage(_ message: String, - html: String?, - intentionalMentions: IntentionalMentions) async { - await sendMessage(message, - html: html, - inReplyTo: nil, - intentionalMentions: intentionalMentions) - } -} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/CustomStringConvertible.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/CustomStringConvertible.swift index 5017baaa57..bb4799a110 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/CustomStringConvertible.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/CustomStringConvertible.swift @@ -9,43 +9,43 @@ import MatrixRustSDK // MARK: Redact message content from logs -extension EmoteMessageContent: CustomStringConvertible { +extension EmoteMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension FileMessageContent: CustomStringConvertible { +extension FileMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension ImageMessageContent: CustomStringConvertible { +extension ImageMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension NoticeMessageContent: CustomStringConvertible { +extension NoticeMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension TextMessageContent: CustomStringConvertible { +extension TextMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension VideoMessageContent: CustomStringConvertible { +extension VideoMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } } -extension AudioMessageContent: CustomStringConvertible { +extension AudioMessageContent: @retroactive CustomStringConvertible { public var description: String { String(describing: Self.self) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift index 847e0ce9d5..67b50ea408 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift @@ -19,7 +19,7 @@ enum EncryptionAuthenticity: Hashable { case unknownDevice(color: Color) case unsignedDevice(color: Color) case unverifiedIdentity(color: Color) - case previouslyVerified(color: Color) + case verificationViolation(color: Color) case sentInClear(color: Color) var message: String { @@ -32,7 +32,7 @@ enum EncryptionAuthenticity: Hashable { L10n.eventShieldReasonUnsignedDevice case .unverifiedIdentity: L10n.eventShieldReasonUnverifiedIdentity - case .previouslyVerified: + case .verificationViolation: L10n.eventShieldReasonPreviouslyVerified case .sentInClear: L10n.eventShieldReasonSentInClear @@ -45,7 +45,7 @@ enum EncryptionAuthenticity: Hashable { .unknownDevice(let color), .unsignedDevice(let color), .unverifiedIdentity(let color), - .previouslyVerified(let color), + .verificationViolation(let color), .sentInClear(let color): color } @@ -54,7 +54,7 @@ enum EncryptionAuthenticity: Hashable { var icon: KeyPath { switch self { case .notGuaranteed: \.info - case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .previouslyVerified: \.helpSolid + case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .verificationViolation: \.helpSolid case .sentInClear: \.lockOff } } @@ -82,8 +82,8 @@ extension EncryptionAuthenticity { self = .unsignedDevice(color: color) case .unverifiedIdentity: self = .unverifiedIdentity(color: color) - case .previouslyVerified: - self = .previouslyVerified(color: color) + case .verificationViolation: + self = .verificationViolation(color: color) case .sentInClear: self = .sentInClear(color: color) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemIdentifier.swift b/ElementX/Sources/Services/Timeline/TimelineItemIdentifier.swift new file mode 100644 index 0000000000..a6efccb371 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItemIdentifier.swift @@ -0,0 +1,66 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +/// A timeline item identifier +/// - uniqueID: Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline. +/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances. +/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or +/// a local transaction id, never both or none. +enum TimelineItemIdentifier: Hashable { + case event(uniqueID: TimelineUniqueId, eventOrTransactionID: EventOrTransactionId) + case virtual(uniqueID: TimelineUniqueId) + + var uniqueID: TimelineUniqueId { + switch self { + case .event(let uniqueID, _): + return uniqueID + case .virtual(let uniqueID): + return uniqueID + } + } + + var eventID: String? { + guard case let .event(_, eventOrTransactionID) = self else { + return nil + } + + switch eventOrTransactionID { + case .eventId(let eventID): + return eventID + default: + return nil + } + } + + var transactionID: String? { + guard case let .event(_, eventOrTransactionID) = self else { + return nil + } + + switch eventOrTransactionID { + case .transactionId(let transactionID): + return transactionID + default: + return nil + } + } +} + +// MARK: - Mocks + +extension TimelineItemIdentifier { + static var randomEvent: Self { + .event(uniqueID: .init(id: UUID().uuidString), eventOrTransactionID: .eventId(eventId: UUID().uuidString)) + } + + static var randomVirtual: Self { + .virtual(uniqueID: .init(id: UUID().uuidString)) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 6a94a3614b..346fad5845 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -8,38 +8,17 @@ import Foundation import MatrixRustSDK -struct TimelineItemIdentifier: Hashable { - /// Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline. - /// It's value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances. - let timelineID: String - - /// Uniquely identifies the timeline item from the server side. - /// Only available for EventTimelineItem and only when the item is returned by the server. - var eventID: String? - - /// Uniquely identifies the local echo of the timeline item. - /// Only available for sent EventTimelineItem that have not been returned by the server yet. - var transactionID: String? -} - -extension TimelineItemIdentifier { - /// Use only for mocks/tests - static var random: Self { - .init(timelineID: UUID().uuidString, eventID: UUID().uuidString) - } -} - /// A light wrapper around timeline items returned from Rust. enum TimelineItemProxy { case event(EventTimelineItemProxy) - case virtual(MatrixRustSDK.VirtualTimelineItem, timelineID: String) + case virtual(MatrixRustSDK.VirtualTimelineItem, uniqueID: TimelineUniqueId) case unknown(MatrixRustSDK.TimelineItem) init(item: MatrixRustSDK.TimelineItem) { if let eventItem = item.asEvent() { - self = .event(EventTimelineItemProxy(item: eventItem, id: String(item.uniqueId()))) + self = .event(EventTimelineItemProxy(item: eventItem, uniqueID: item.uniqueId())) } else if let virtualItem = item.asVirtual() { - self = .virtual(virtualItem, timelineID: String(item.uniqueId())) + self = .virtual(virtualItem, uniqueID: item.uniqueId()) } else { self = .unknown(item) } @@ -92,69 +71,71 @@ class EventTimelineItemProxy { let item: MatrixRustSDK.EventTimelineItem let id: TimelineItemIdentifier - init(item: MatrixRustSDK.EventTimelineItem, id: String) { + init(item: MatrixRustSDK.EventTimelineItem, uniqueID: TimelineUniqueId) { self.item = item - self.id = TimelineItemIdentifier(timelineID: id, eventID: item.eventId(), transactionID: item.transactionId()) + + id = .event(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId) } lazy var deliveryStatus: TimelineItemDeliveryStatus? = { - guard let localSendState = item.localSendState() else { + guard let localSendState = item.localSendState else { return nil } switch localSendState { - case .sendingFailed(_, let isRecoverable): - return isRecoverable ? .sending : .sendingFailed(.unknown) + case .sendingFailed(let error, let isRecoverable): + switch error { + case .identityViolations(let users): + return .sendingFailed(.verifiedUser(.changedIdentity(users: users))) + case .insecureDevices(let userDeviceMap): + return .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: userDeviceMap))) + default: + return .sendingFailed(.unknown) + } case .notSentYet: return .sending case .sent: return .sent - case .verifiedUserHasUnsignedDevice(devices: let devices): - return .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: devices))) - case .verifiedUserChangedIdentity(users: let users): - return .sendingFailed(.verifiedUser(.changedIdentity(users: users))) - case .crossSigningNotSetup, .sendingFromUnverifiedDevice: - return .sendingFailed(.unknown) } }() - lazy var canBeRepliedTo = item.canBeRepliedTo() + lazy var canBeRepliedTo = item.canBeRepliedTo - lazy var content = item.content() + lazy var content = item.content - lazy var isOwn = item.isOwn() + lazy var isOwn = item.isOwn - lazy var isEditable = item.isEditable() + lazy var isEditable = item.isEditable lazy var sender: TimelineItemSender = { - let profile = item.senderProfile() + let profile = item.senderProfile switch profile { case let .ready(displayName, isDisplayNameAmbiguous, avatarUrl): - return .init(id: item.sender(), + return .init(id: item.sender, displayName: displayName, isDisplayNameAmbiguous: isDisplayNameAmbiguous, avatarURL: avatarUrl.flatMap(URL.init(string:))) default: - return .init(id: item.sender(), + return .init(id: item.sender, displayName: nil, isDisplayNameAmbiguous: false, avatarURL: nil) } }() - lazy var reactions = item.reactions() + lazy var reactions = item.reactions - lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp() / 1000)) + lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000)) lazy var debugInfo: TimelineItemDebugInfo = { - let debugInfo = item.debugInfo() + let debugInfo = item.lazyProvider.debugInfo() return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson) }() - lazy var shieldState = item.getShield(strict: false) + lazy var shieldState = item.lazyProvider.getShields(strict: false) - lazy var readReceipts = item.readReceipts() + lazy var readReceipts = item.readReceipts } struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible { @@ -194,7 +175,7 @@ struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible { return nil } - return String(decoding: jsonData, as: UTF8.self) + return String(data: jsonData, encoding: .utf8) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index ce91fe8df0..6b4ae9f5ea 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -24,3 +24,20 @@ protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { var contentType: EventBasedMessageTimelineItemContentType { get } var isThreaded: Bool { get } } + +extension EventBasedMessageTimelineItemProtocol { + var hasMediaCaption: Bool { + switch contentType { + case .audio(let content): + content.caption != nil + case .file(let content): + content.caption != nil + case .image(let content): + content.caption != nil + case .video(let content): + content.caption != nil + case .emote, .notice, .text, .location, .voice: + false + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift index 6d99259b46..6b556dcbe0 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift @@ -23,7 +23,7 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { var properties = RoomTimelineItemProperties() var body: String { - content.body + content.caption ?? content.filename } var contentType: EventBasedMessageTimelineItemContentType { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift index 6353b619e1..4ca8d5c1f1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift @@ -9,7 +9,11 @@ import UIKit import UniformTypeIdentifiers struct AudioRoomTimelineItemContent: Hashable { - let body: String + let filename: String + var caption: String? + var formattedCaption: AttributedString? + /// The original textual representation of the formatted caption directly from the event (usually HTML code) + var formattedCaptionHTMLString: String? let duration: TimeInterval let waveform: EstimatedWaveform? let source: MediaSourceProxy? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift index 3b531a7e18..bb025ee776 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift @@ -26,7 +26,7 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { var properties = RoomTimelineItemProperties() var body: String { - content.body + content.caption ?? content.filename } var contentType: EventBasedMessageTimelineItemContentType { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift index 9c0cfe9798..67f00ee1ba 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift @@ -9,7 +9,11 @@ import Foundation import UniformTypeIdentifiers struct FileRoomTimelineItemContent: Hashable { - let body: String + let filename: String + var caption: String? + var formattedCaption: AttributedString? + /// The original textual representation of the formatted caption directly from the event (usually HTML code) + var formattedCaptionHTMLString: String? let source: MediaSourceProxy? let thumbnailSource: MediaSourceProxy? let contentType: UTType? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift index 759e9d4567..b556b1e187 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift @@ -25,7 +25,7 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { var properties = RoomTimelineItemProperties() var body: String { - content.body + content.caption ?? content.filename } var contentType: EventBasedMessageTimelineItemContentType { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift index 0ef2e27aa2..01eb841510 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift @@ -9,7 +9,11 @@ import Foundation import UniformTypeIdentifiers struct ImageRoomTimelineItemContent: Hashable { - let body: String + let filename: String + var caption: String? + var formattedCaption: AttributedString? + /// The original textual representation of the formatted caption directly from the event (usually HTML code) + var formattedCaptionHTMLString: String? let source: MediaSourceProxy let thumbnailSource: MediaSourceProxy? var width: CGFloat? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index 896d6a81ff..5247fd6cd2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -25,7 +25,7 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { var properties = RoomTimelineItemProperties() var body: String { - content.body + content.caption ?? content.filename } var contentType: EventBasedMessageTimelineItemContentType { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift index 905008a6f3..0790970edf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift @@ -9,7 +9,11 @@ import Foundation import UniformTypeIdentifiers struct VideoRoomTimelineItemContent: Hashable { - let body: String + let filename: String + var caption: String? + var formattedCaption: AttributedString? + /// The original textual representation of the formatted caption directly from the event (usually HTML code) + var formattedCaptionHTMLString: String? let duration: TimeInterval let source: MediaSourceProxy? let thumbnailSource: MediaSourceProxy? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift index 22837d2f5e..d8e0769988 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift @@ -23,7 +23,7 @@ struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equa var properties = RoomTimelineItemProperties() var body: String { - content.body + content.caption ?? content.filename } var contentType: EventBasedMessageTimelineItemContentType { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift index 0b5d3354b3..1928a55b4a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift @@ -51,8 +51,8 @@ struct VoiceMessageRoomPlaybackView: View { } .padding(.leading, 2) .padding(.trailing, 8) - .onChange(of: isDragging) { isDragging in - onScrubbing(isDragging) + .onChange(of: isDragging) { _, newValue in + onScrubbing(newValue) } } @@ -97,7 +97,7 @@ struct VoiceMessageRoomPlaybackView_Previews: PreviewProvider, TestablePreview { 294, 131, 19, 2, 3, 3, 1, 2, 0, 0, 0, 0, 0, 0, 0, 3]) - static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.random), + static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: L10n.commonVoiceMessage, duration: 10.0, waveform: waveform, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift index 1cde05ca25..0e70287ac5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift @@ -55,7 +55,7 @@ struct VoiceMessageRoomTimelineView: View { struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - static let timelineItemIdentifier = TimelineItemIdentifier.random + static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier, timestamp: "Now", isOutgoing: false, @@ -63,7 +63,7 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, isThreaded: false, sender: .init(id: "Bob"), - content: .init(body: "audio.ogg", + content: .init(filename: "audio.ogg", duration: 300, waveform: EstimatedWaveform.mockWaveform, source: nil, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift index 8eefe273fe..eaed672901 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift @@ -15,7 +15,9 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { } enum UTDCause: Hashable { - case membership + case sentBeforeWeJoined + case verificationViolation + case insecureDevice case unknown } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift index af817de161..139a50eb3e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift @@ -22,6 +22,6 @@ struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equa } init(position: Position) { - id = TimelineItemIdentifier(timelineID: position.id) + id = .virtual(uniqueID: .init(id: position.id)) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift index 3aab2b6ba3..44237552fe 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift @@ -8,6 +8,6 @@ import Foundation struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { - let id = TimelineItemIdentifier(timelineID: UUID().uuidString) + let id: TimelineItemIdentifier = .virtual(uniqueID: .init(id: UUID().uuidString)) let name: String? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 018f47b837..7a3d1e9ba3 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -27,7 +27,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol? { let isOutgoing = eventItemProxy.isOwn - switch eventItemProxy.content.kind() { + switch eventItemProxy.content { case .unableToDecrypt(let encryptedMessage): return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing) case .redactedMessage: @@ -43,8 +43,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing) case .failedToParseState(let eventType, _, let error): return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing) - case .message: - return buildMessageTimelineItem(eventItemProxy, isOutgoing) + case .message(let messageContent): + return buildMessageTimelineItem(eventItemProxy, messageContent, isOutgoing) case .state(_, let content): if isDM, content == .roomCreate { return nil @@ -72,34 +72,29 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } // MARK: - Message Events - - private func buildMessageTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ isOutgoing: Bool) -> RoomTimelineItemProtocol? { - guard let messageTimelineItem = eventItemProxy.content.asMessage() else { - fatalError("Invalid message timeline item: \(eventItemProxy)") - } - - let isThreaded = messageTimelineItem.isThreaded() - switch messageTimelineItem.msgtype() { - case .text(content: let content): - return buildTextTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .image(content: let content): - return buildImageTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .video(let content): - return buildVideoTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .file(let content): - return buildFileTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .notice(content: let content): - return buildNoticeTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .emote(content: let content): - return buildEmoteTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) - case .audio(let content): - if content.voice != nil { - return buildVoiceTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) + + private func buildMessageTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ messageContent: MessageContent, _ isOutgoing: Bool) -> RoomTimelineItemProtocol? { + switch messageContent.msgType { + case .text(content: let textMessageContent): + return buildTextTimelineItem(for: eventItemProxy, messageContent, textMessageContent, isOutgoing) + case .image(content: let imageMessageContent): + return buildImageTimelineItem(for: eventItemProxy, messageContent, imageMessageContent, isOutgoing) + case .video(let videoMessageContent): + return buildVideoTimelineItem(for: eventItemProxy, messageContent, videoMessageContent, isOutgoing) + case .file(let fileMessageContent): + return buildFileTimelineItem(for: eventItemProxy, messageContent, fileMessageContent, isOutgoing) + case .notice(content: let noticeMessageContent): + return buildNoticeTimelineItem(for: eventItemProxy, messageContent, noticeMessageContent, isOutgoing) + case .emote(content: let emoteMessageContent): + return buildEmoteTimelineItem(for: eventItemProxy, messageContent, emoteMessageContent, isOutgoing) + case .audio(let audioMessageContent): + if audioMessageContent.voice != nil { + return buildVoiceTimelineItem(for: eventItemProxy, messageContent, audioMessageContent, isOutgoing) } else { - return buildAudioTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) + return buildAudioTimelineItem(for: eventItemProxy, messageContent, audioMessageContent, isOutgoing) } - case .location(let content): - return buildLocationTimelineItem(for: eventItemProxy, messageTimelineItem, content, isOutgoing, isThreaded) + case .location(let locationMessageContent): + return buildLocationTimelineItem(for: eventItemProxy, messageContent, locationMessageContent, isOutgoing) case .other: return nil } @@ -162,8 +157,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .unknown: encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .unknown) errorLabel = L10n.commonWaitingForDecryptionKey - case .membership: - encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .membership) + case .verificationViolation: + encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .verificationViolation) + errorLabel = L10n.commonUnableToDecryptVerificationViolation + case .unsignedDevice, .unknownDevice: + encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .insecureDevice) + errorLabel = L10n.commonUnableToDecryptInsecureDevice + case .sentBeforeWeJoined: + encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .sentBeforeWeJoined) errorLabel = L10n.commonUnableToDecryptNoAccess } case .olmV1Curve25519AesSha2(let senderKey): @@ -196,20 +197,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildTextTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: TextMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ textMessageContent: TextMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { TextRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildTextTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildTextTimelineItemContent(textMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -217,20 +217,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: ImageMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ imageMessageContent: ImageMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { ImageRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildImageTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildImageTimelineItemContent(imageMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -238,20 +237,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: VideoMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ videoMessageContent: VideoMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { VideoRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildVideoTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildVideoTimelineItemContent(videoMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -259,20 +257,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: AudioMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ audioMessageContent: AudioMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { AudioRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildAudioTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildAudioTimelineItemContent(audioMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -280,20 +277,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildVoiceTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: AudioMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ audioMessageContent: AudioMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { VoiceMessageRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildAudioTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildAudioTimelineItemContent(audioMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -301,20 +297,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: FileMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ fileMessageContent: FileMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { FileRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildFileTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildFileTimelineItemContent(fileMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -322,20 +317,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: NoticeMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ noticeMessageContent: NoticeMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { NoticeRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildNoticeTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildNoticeTimelineItemContent(noticeMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -343,20 +337,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: EmoteMessageContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ emoteMessageContent: EmoteMessageContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { EmoteRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: emoteMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -364,20 +357,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy, - _ messageTimelineItem: Message, - _ messageContent: LocationContent, - _ isOutgoing: Bool, - _ isThreaded: Bool) -> RoomTimelineItemProtocol { + _ messageContent: MessageContent, + _ locationMessageContent: LocationContent, + _ isOutgoing: Bool) -> RoomTimelineItemProtocol { LocationRoomTimelineItem(id: eventItemProxy.id, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), isOutgoing: isOutgoing, isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, - isThreaded: isThreaded, + isThreaded: messageContent.threadRoot != nil, sender: eventItemProxy.sender, - content: buildLocationTimelineItemContent(messageContent), - replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()), - properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), + content: buildLocationTimelineItemContent(locationMessageContent), + replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo), + properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited, reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -504,12 +496,18 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildAudioTimelineItemContent(_ messageContent: AudioMessageContent) -> AudioRoomTimelineItemContent { + let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil + let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) + var waveform: EstimatedWaveform? if let audioWaveform = messageContent.audio?.waveform { waveform = EstimatedWaveform(data: audioWaveform) } - return AudioRoomTimelineItemContent(body: messageContent.body, + return AudioRoomTimelineItemContent(filename: messageContent.filename, + caption: messageContent.caption, + formattedCaption: formattedCaption, + formattedCaptionHTMLString: htmlCaption, duration: messageContent.audio?.duration ?? 0, waveform: waveform, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), @@ -517,6 +515,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildImageTimelineItemContent(_ messageContent: ImageMessageContent) -> ImageRoomTimelineItemContent { + let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil + let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) + let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let width = messageContent.info?.width.map(CGFloat.init) let height = messageContent.info?.height.map(CGFloat.init) @@ -526,7 +527,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { aspectRatio = width / height } - return .init(body: messageContent.body, + return .init(filename: messageContent.filename, + caption: messageContent.caption, + formattedCaption: formattedCaption, + formattedCaptionHTMLString: htmlCaption, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), thumbnailSource: thumbnailSource, width: width, @@ -537,6 +541,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildVideoTimelineItemContent(_ messageContent: VideoMessageContent) -> VideoRoomTimelineItemContent { + let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil + let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) + let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } let width = messageContent.info?.width.map(CGFloat.init) let height = messageContent.info?.height.map(CGFloat.init) @@ -546,7 +553,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { aspectRatio = width / height } - return .init(body: messageContent.body, + return .init(filename: messageContent.filename, + caption: messageContent.caption, + formattedCaption: formattedCaption, + formattedCaptionHTMLString: htmlCaption, duration: messageContent.info?.duration ?? 0, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), thumbnailSource: thumbnailSource, @@ -564,9 +574,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } private func buildFileTimelineItemContent(_ messageContent: FileMessageContent) -> FileRoomTimelineItemContent { + let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil + let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) + let thumbnailSource = messageContent.info?.thumbnailSource.map { MediaSourceProxy(source: $0, mimeType: messageContent.info?.thumbnailInfo?.mimetype) } - return .init(body: messageContent.body, + return .init(filename: messageContent.filename, + caption: messageContent.caption, + formattedCaption: formattedCaption, + formattedCaptionHTMLString: htmlCaption, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), thumbnailSource: thumbnailSource, contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) @@ -652,12 +668,12 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { // MARK: - Reply details func buildReply(details: InReplyToDetails) -> TimelineItemReply { - let isThreaded = details.event.isThreaded - switch details.event { + let isThreaded = details.event().isThreaded + switch details.event() { case .unavailable: - return .init(details: .notLoaded(eventID: details.eventId), isThreaded: isThreaded) + return .init(details: .notLoaded(eventID: details.eventId()), isThreaded: isThreaded) case .pending: - return .init(details: .loading(eventID: details.eventId), isThreaded: isThreaded) + return .init(details: .loading(eventID: details.eventId()), isThreaded: isThreaded) case let .ready(timelineItem, senderID, senderProfile): let sender: TimelineItemSender switch senderProfile { @@ -675,9 +691,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { let replyContent: TimelineEventContent - switch timelineItem.kind() { - case .message: - return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype()), isThreaded: isThreaded) + switch timelineItem { + case .message(let messageContent): + return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId(), messageType: messageContent.msgType), isThreaded: isThreaded) case .poll(let question, _, _, _, _, _, _): replyContent = .poll(question: question) case .sticker(let body, _, _): @@ -688,9 +704,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent))) } - return .init(details: .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent), isThreaded: isThreaded) + return .init(details: .loaded(sender: sender, eventID: details.eventId(), eventContent: replyContent), isThreaded: isThreaded) case let .error(message): - return .init(details: .error(eventID: details.eventId, message: message), isThreaded: isThreaded) + return .init(details: .error(eventID: details.eventId(), message: message), isThreaded: isThreaded) } } @@ -751,7 +767,11 @@ private extension RepliedToEventDetails { var isThreaded: Bool { switch self { case .ready(let content, _, _): - return content.asMessage()?.isThreaded() ?? false + guard case let .message(content) = content else { + return false + } + + return content.threadRoot != nil default: return false } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift index d3ae948287..94654d2df4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift @@ -6,25 +6,17 @@ // import Foundation +import MatrixRustSDK final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject { - static func == (lhs: RoomTimelineItemViewState, rhs: RoomTimelineItemViewState) -> Bool { - lhs.type == rhs.type && lhs.groupStyle == rhs.groupStyle - } - @Published var type: RoomTimelineItemType @Published var groupStyle: TimelineGroupStyle - /// Contains all the identification info of the item, `timelineID`, `eventID` and `transactionID` + /// Contains all the identification info of the item, `uniqueID`, `eventID` and `transactionID` var identifier: TimelineItemIdentifier { type.id } - /// The `timelineID` of the item, used for the timeline view level identification, do not use for any business logic use `identifier` instead - var id: String { - identifier.timelineID - } - init(type: RoomTimelineItemType, groupStyle: TimelineGroupStyle) { self.type = type self.groupStyle = groupStyle @@ -33,6 +25,19 @@ final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject convenience init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) { self.init(type: .init(item: item), groupStyle: groupStyle) } + + // MARK: - Equatable + + static func == (lhs: RoomTimelineItemViewState, rhs: RoomTimelineItemViewState) -> Bool { + lhs.type == rhs.type && lhs.groupStyle == rhs.groupStyle + } + + // MARK: Identifiable + + /// The `timelineID` of the item, used for the timeline view level identification, do not use for any business logic use `identifier` instead + var id: TimelineUniqueId { + identifier.uniqueID + } } enum RoomTimelineItemType: Equatable { diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index a3bee43fdc..64b5152d69 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -72,7 +72,17 @@ final class TimelineProxy: TimelineProxyProtocol { } func messageEventContent(for timelineItemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? { - await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content().asMessage()?.content() + guard let content = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content, + case let .message(messageContent) = content else { + return nil + } + + do { + return try contentWithoutRelationFromMessage(message: messageContent) + } catch { + MXLog.error("Failed retrieving message event content for timelineItemID=\(timelineItemID)") + return nil + } } func paginateBackwards(requestSize: UInt16) async -> Result { @@ -154,42 +164,30 @@ final class TimelineProxy: TimelineProxyProtocol { } } - func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { do { - guard try await timeline.edit(item: timelineItem, newContent: .roomMessage(content: newContent)) == true else { - return .failure(.failedEditing) - } - - MXLog.info("Finished editing timeline item: \(timelineItem.eventId() ?? timelineItem.transactionId() ?? "unknown")") + try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: .roomMessage(content: newContent)) + MXLog.info("Finished editing timeline item: \(eventOrTransactionID)") + return .success(()) } catch { - MXLog.error("Failed editing timeline item: \(timelineItem.eventId() ?? timelineItem.transactionId() ?? "unknown") with error: \(error)") + MXLog.error("Failed editing timeline item: \(eventOrTransactionID) with error: \(error)") return .failure(.sdkError(error)) } } - func redact(_ timelineItemID: TimelineItemIdentifier, reason: String?) async -> Result { - MXLog.info("Redacting timeline item: \(timelineItemID)") - - guard let eventTimelineItem = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID) else { - MXLog.error("Unknown timeline item: \(timelineItemID)") - return .failure(.failedRedacting) - } + func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result { + MXLog.info("Redacting timeline item: \(eventOrTransactionID)") do { - let success = try await timeline.redactEvent(item: eventTimelineItem, reason: reason) + try await timeline.redactEvent(eventOrTransactionId: eventOrTransactionID, reason: reason) - guard success else { - MXLog.error("Failed redacting timeline item: \(timelineItemID)") - return .failure(.failedRedacting) - } - - MXLog.info("Redacted timeline item: \(timelineItemID)") + MXLog.info("Redacted timeline item: \(eventOrTransactionID)") return .success(()) } catch { - MXLog.error("Failed redacting timeline item: \(timelineItemID) with error: \(error)") + MXLog.error("Failed redacting timeline item: \(eventOrTransactionID) with error: \(error)") return .failure(.sdkError(error)) } } @@ -229,9 +227,14 @@ final class TimelineProxy: TimelineProxyProtocol { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending audio") - let handle = timeline.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) + let handle = timeline.sendAudio(url: url.path(percentEncoded: false), + audioInfo: audioInfo, + caption: nil, + formattedCaption: nil, + progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }, + useSendQueue: false) await requestHandle(handle) @@ -252,9 +255,12 @@ final class TimelineProxy: TimelineProxyProtocol { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending file") - let handle = timeline.sendFile(url: url.path(percentEncoded: false), fileInfo: fileInfo, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) + let handle = timeline.sendFile(url: url.path(percentEncoded: false), + fileInfo: fileInfo, + progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }, + useSendQueue: false) await requestHandle(handle) @@ -276,9 +282,15 @@ final class TimelineProxy: TimelineProxyProtocol { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending image") - let handle = timeline.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) + let handle = timeline.sendImage(url: url.path(percentEncoded: false), + thumbnailUrl: thumbnailURL.path(percentEncoded: false), + imageInfo: imageInfo, + caption: nil, + formattedCaption: nil, + progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }, + useSendQueue: false) await requestHandle(handle) @@ -318,9 +330,15 @@ final class TimelineProxy: TimelineProxyProtocol { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending video") - let handle = timeline.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) + let handle = timeline.sendVideo(url: url.path(percentEncoded: false), + thumbnailUrl: thumbnailURL.path(percentEncoded: false), + videoInfo: videoInfo, + caption: nil, + formattedCaption: nil, + progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }, + useSendQueue: false) await requestHandle(handle) @@ -342,9 +360,15 @@ final class TimelineProxy: TimelineProxyProtocol { requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending voice message") - let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, caption: nil, formattedCaption: nil, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) + let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), + audioInfo: audioInfo, + waveform: waveform, + caption: nil, + formattedCaption: nil, + progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }, + useSendQueue: false) await requestHandle(handle) @@ -361,10 +385,10 @@ final class TimelineProxy: TimelineProxyProtocol { func sendMessage(_ message: String, html: String?, - inReplyTo eventID: String? = nil, + inReplyToEventID: String? = nil, intentionalMentions: IntentionalMentions) async -> Result { - if let eventID { - MXLog.info("Sending reply to eventID: \(eventID)") + if let inReplyToEventID { + MXLog.info("Sending reply to eventID: \(inReplyToEventID)") } else { MXLog.info("Sending message") } @@ -374,16 +398,16 @@ final class TimelineProxy: TimelineProxyProtocol { intentionalMentions: intentionalMentions.toRustMentions()) do { - if let eventID { - try await timeline.sendReply(msg: messageContent, eventId: eventID) - MXLog.info("Finished sending reply to eventID: \(eventID)") + if let inReplyToEventID { + try await timeline.sendReply(msg: messageContent, eventId: inReplyToEventID) + MXLog.info("Finished sending reply to eventID: \(inReplyToEventID)") } else { _ = try await timeline.send(msg: messageContent) MXLog.info("Finished sending message") } } catch { - if let eventID { - MXLog.error("Failed sending reply to eventID: \(eventID) with error: \(error)") + if let inReplyToEventID { + MXLog.error("Failed sending reply to eventID: \(inReplyToEventID) with error: \(error)") } else { MXLog.error("Failed sending message with error: \(error)") } @@ -421,15 +445,15 @@ final class TimelineProxy: TimelineProxyProtocol { } } - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result { - MXLog.info("Toggling reaction for event: \(itemID)") + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async -> Result { + MXLog.info("Toggling reaction \(reaction) for event: \(eventOrTransactionID)") do { - try await timeline.toggleReaction(uniqueId: itemID.timelineID, key: reaction) - MXLog.info("Finished toggling reaction for event: \(itemID)") + try await timeline.toggleReaction(itemId: eventOrTransactionID, key: reaction) + MXLog.info("Finished toggling reaction for event: \(eventOrTransactionID)") return .success(()) } catch { - MXLog.error("Failed toggling reaction for event: \(itemID)") + MXLog.error("Failed toggling reaction for event: \(eventOrTransactionID)") return .failure(.sdkError(error)) } } @@ -460,13 +484,11 @@ final class TimelineProxy: TimelineProxyProtocol { do { let originalEvent = try await timeline.getEventTimelineItemByEventId(eventId: eventID) - guard try await timeline.edit(item: originalEvent, - newContent: .pollStart(pollData: .init(question: question, - answers: answers, - maxSelections: 1, - pollKind: .init(pollKind: pollKind)))) else { - return .failure(.failedEditing) - } + try await timeline.edit(eventOrTransactionId: originalEvent.eventOrTransactionId, + newContent: .pollStart(pollData: .init(question: question, + answers: answers, + maxSelections: 1, + pollKind: .init(pollKind: pollKind)))) MXLog.info("Finished editing poll with eventID: \(eventID)") @@ -482,7 +504,7 @@ final class TimelineProxy: TimelineProxyProtocol { return await Task.dispatch(on: .global()) { do { - try self.timeline.endPoll(pollStartId: pollStartID, text: text) + try self.timeline.endPoll(pollStartEventId: pollStartID, text: text) MXLog.info("Finished ending poll with eventID: \(pollStartID)") @@ -498,7 +520,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending response for poll with eventID: \(pollStartID)") do { - try await timeline.sendPollResponse(pollStartId: pollStartID, answers: answers) + try await timeline.sendPollResponse(pollStartEventId: pollStartID, answers: answers) MXLog.info("Finished sending response for poll with eventID: \(pollStartID)") @@ -618,7 +640,7 @@ extension Array where Element == TimelineItemProxy { func firstEventTimelineItemUsingStableID(_ id: TimelineItemIdentifier) -> EventTimelineItem? { for item in self { if case let .event(eventTimelineItem) = item { - if eventTimelineItem.id.timelineID == id.timelineID { + if eventTimelineItem.id.uniqueID == id.uniqueID { return eventTimelineItem.item } } @@ -626,4 +648,16 @@ extension Array where Element == TimelineItemProxy { return nil } + + func firstEventTimelineItemUsingEventOrTransactionID(_ eventOrTransactionID: EventOrTransactionId) -> EventTimelineItem? { + for item in self { + if case let .event(eventTimelineItem) = item, + case let .event(_, identifier) = eventTimelineItem.id, + identifier == eventOrTransactionID { + return eventTimelineItem.item + } + } + + return nil + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index c081388487..e1c8b5f6fe 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -18,7 +18,6 @@ enum TimelineKind { enum TimelineProxyError: Error { case sdkError(Error) - case failedEditing case failedRedacting case failedPaginatingEndReached } @@ -38,9 +37,10 @@ protocol TimelineProxyProtocol { func paginateBackwards(requestSize: UInt16) async -> Result func paginateForwards(requestSize: UInt16) async -> Result - func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result + func edit(_ eventOrTransactionID: EventOrTransactionId, + newContent: RoomMessageEventContentWithoutRelation) async -> Result - func redact(_ timelineItemID: TimelineItemIdentifier, + func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result func pin(eventID: String) async -> Result @@ -89,12 +89,11 @@ protocol TimelineProxyProtocol { func sendMessage(_ message: String, html: String?, - inReplyTo eventID: String?, + inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async -> Result - func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result + func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async -> Result - // Polls func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result func editPoll(original eventID: String, @@ -112,14 +111,3 @@ protocol TimelineProxyProtocol { html: String?, intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation } - -extension TimelineProxyProtocol { - func sendMessage(_ message: String, - html: String?, - intentionalMentions: IntentionalMentions) async -> Result { - await sendMessage(message, - html: html, - inReplyTo: nil, - intentionalMentions: intentionalMentions) - } -} diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 6b7f94908c..310475032a 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -60,7 +60,7 @@ class UserSessionStore: UserSessionStoreProtocol { } } - func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { do { let session = try client.session() let userID = try client.userId() @@ -125,7 +125,7 @@ class UserSessionStore: UserSessionStoreProtocol { slidingSync: .restored, sessionDelegate: keychainController, appHooks: appHooks, - invisibleCryptoEnabled: appSettings.invisibleCryptoEnabled) + enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode) .sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath, cachePath: credentials.restorationToken.sessionDirectories.cachePath) .username(username: credentials.userID) @@ -146,7 +146,7 @@ class UserSessionStore: UserSessionStoreProtocol { } } - private func setupProxyForClient(_ client: Client) async -> ClientProxyProtocol { + private func setupProxyForClient(_ client: ClientProtocol) async -> ClientProxyProtocol { await ClientProxy(client: client, networkMonitor: networkMonitor, appSettings: appSettings) diff --git a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift index fd841ae699..64772c3672 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift @@ -14,6 +14,7 @@ enum UserSessionStoreError: Error { case failedSettingUpSession } +// sourcery: AutoMockable protocol UserSessionStoreProtocol { /// Deletes all data stored in the shared container and keychain func reset() @@ -31,7 +32,7 @@ protocol UserSessionStoreProtocol { func restoreUserSession() async -> Result /// Creates a user session for a new client from the SDK along with the passphrase used for the data stores. - func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result + func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result /// Logs out of the specified session. func logout(userSession: UserSessionProtocol) diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift index e3ae2931cc..b1606ef486 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift @@ -51,7 +51,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol { } // Otherwise, load the file from source - guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, body: body) else { + guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, filename: body) else { throw MediaProviderError.failedRetrievingFile } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ac29d717a4..01bea2b808 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -30,7 +30,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol, SecureWindowManagerDelegate windowManager.delegate = self - MXLog.configure(logLevel: .debug) + MXLog.configure(currentTarget: "uitests", filePrefix: nil, logLevel: .debug) ServiceLocator.shared.register(userIndicatorController: UserIndicatorController()) @@ -109,22 +109,16 @@ class MockScreen: Identifiable { lazy var coordinator: CoordinatorProtocol? = { switch id { - case .login: - let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = LoginScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), - analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController)) - navigationStackCoordinator.setRootCoordinator(coordinator) - return navigationStackCoordinator case .serverSelection: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: MockAuthenticationService(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - isModallyPresented: true)) + let coordinator = ServerSelectionScreenCoordinator(parameters: .init(authenticationService: AuthenticationService.mock, + authenticationFlow: .login, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .authenticationFlow: - let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationService(), + let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock, qrCodeLoginService: QRCodeLoginServiceMock(), bugReportService: BugReportServiceMock(), navigationRootCoordinator: navigationRootCoordinator, @@ -239,12 +233,13 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomPlainNoAvatar: let navigationStackCoordinator = NavigationStackCoordinator() - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -257,12 +252,13 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -275,12 +271,13 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.default - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -293,12 +290,13 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -314,12 +312,13 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -335,12 +334,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -356,12 +356,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -378,12 +379,13 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -399,12 +401,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -419,12 +422,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -453,12 +457,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -474,12 +479,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -495,12 +501,13 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -512,7 +519,8 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .sessionVerification: var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5)) - let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy) + let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy, + flow: .initiator) return SessionVerificationScreenCoordinator(parameters: parameters) case .userSessionScreen, .userSessionScreenReply: let appSettings: AppSettings = ServiceLocator.shared.settings @@ -548,7 +556,7 @@ class MockScreen: Identifiable { case .roomMembersListScreenPendingInvites: let navigationStackCoordinator = NavigationStackCoordinator() let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] - let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(), + let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MediaProviderMock(configuration: .init()), roomProxy: JoinedRoomProxyMock(.init(name: "test", members: members)), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics)) @@ -558,7 +566,7 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) let coordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: .init(roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics)) @@ -574,7 +582,8 @@ class MockScreen: Identifiable { userSession: userSession, userIndicatorController: UserIndicatorControllerMock(), navigationStackCoordinator: navigationStackCoordinator, - userDiscoveryService: userDiscoveryMock) + userDiscoveryService: userDiscoveryMock, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings)) let coordinator = StartChatScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -588,7 +597,8 @@ class MockScreen: Identifiable { userSession: userSession, userIndicatorController: UserIndicatorControllerMock(), navigationStackCoordinator: navigationStackCoordinator, - userDiscoveryService: userDiscoveryMock)) + userDiscoveryService: userDiscoveryMock, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .createRoom: @@ -620,6 +630,40 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new)) navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .encryptionSettings, .encryptionSettingsOutOfSync: + let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete + let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + let navigationStackCoordinator = NavigationStackCoordinator() + navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator()) + + let coordinator = EncryptionSettingsFlowCoordinator(parameters: .init(userSession: userSession, + appSettings: ServiceLocator.shared.settings, + userIndicatorController: UserIndicatorControllerMock(), + navigationStackCoordinator: navigationStackCoordinator)) + retainedState.append(coordinator) + coordinator.start() + + return navigationStackCoordinator + case .encryptionReset: + let recoveryState: SecureBackupRecoveryState = id == .encryptionSettings ? .enabled : .incomplete + let clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", recoveryState: recoveryState)) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + let userIndicatorController = UserIndicatorController() + userIndicatorController.window = windowManager.overlayWindow + let navigationStackCoordinator = NavigationStackCoordinator() + + let coordinator = EncryptionResetFlowCoordinator(parameters: .init(userSession: userSession, + userIndicatorController: userIndicatorController, + navigationStackCoordinator: navigationStackCoordinator, + windowManger: windowManager)) + + retainedState.append(coordinator) + coordinator.start() + return navigationStackCoordinator case .autoUpdatingTimeline: let appSettings: AppSettings = ServiceLocator.shared.settings diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 043b0b3778..74764ee47e 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -20,7 +20,9 @@ enum UITestsScreenIdentifier: String { case createPoll case createRoom case createRoomNoUsers - case login + case encryptionSettings + case encryptionSettingsOutOfSync + case encryptionReset case roomLayoutBottom case roomLayoutMiddle case roomLayoutTop diff --git a/ElementX/Sources/UITests/UITestsSignalling.swift b/ElementX/Sources/UITests/UITestsSignalling.swift index 84f054e2ef..ece07bfb21 100644 --- a/ElementX/Sources/UITests/UITestsSignalling.swift +++ b/ElementX/Sources/UITests/UITestsSignalling.swift @@ -137,10 +137,11 @@ enum UITestsSignalling { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys - guard let data = try? encoder.encode(self) else { + guard let data = try? encoder.encode(self), + let rawMessage = String(data: data, encoding: .utf8) else { return "unknown" } - return String(decoding: data, as: UTF8.self) + return rawMessage } init?(rawValue: String) { @@ -165,9 +166,8 @@ enum UITestsSignalling { /// Processes string data from the file and publishes its signal. private func processFileData(_ data: Data) { - let rawMessage = String(decoding: data, as: UTF8.self) - - guard let message = Message(rawValue: rawMessage), + guard let rawMessage = String(data: data, encoding: .utf8), + let message = Message(rawValue: rawMessage), message.mode != mode // Filter out messages sent by this client. else { return } diff --git a/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist b/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist index a68a19719c..6d4deb5f16 100644 --- a/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist +++ b/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist @@ -18,6 +18,14 @@ Type PSChildPaneSpecifier + + File + Packages/compound-ios + Title + compound-ios + Type + PSChildPaneSpecifier + File Packages/DeviceKit diff --git a/ElementX/SupportingFiles/Settings.bundle/Packages/compound-ios.plist b/ElementX/SupportingFiles/Settings.bundle/Packages/compound-ios.plist new file mode 100644 index 0000000000..696374fabc --- /dev/null +++ b/ElementX/SupportingFiles/Settings.bundle/Packages/compound-ios.plist @@ -0,0 +1,676 @@ + + + + + PreferenceSpecifiers + + + FooterText + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. + + Type + PSGroupSpecifier + + + + diff --git a/ElementX/SupportingFiles/Settings.bundle/Packages/matrix-rich-text-editor-swift.plist b/ElementX/SupportingFiles/Settings.bundle/Packages/matrix-rich-text-editor-swift.plist index 9495e4d817..696374fabc 100644 --- a/ElementX/SupportingFiles/Settings.bundle/Packages/matrix-rich-text-editor-swift.plist +++ b/ElementX/SupportingFiles/Settings.bundle/Packages/matrix-rich-text-editor-swift.plist @@ -6,207 +6,667 @@ FooterText - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. Type PSGroupSpecifier diff --git a/Gemfile.lock b/Gemfile.lock index 95663e2412..f196f469c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,20 +16,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.958.0) - aws-sdk-core (3.201.3) + aws-partitions (1.992.0) + aws-sdk-core (3.210.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.156.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.0) + aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -44,8 +44,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -71,10 +71,10 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -90,6 +90,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -123,6 +124,8 @@ GEM fastlane-plugin-xcconfig (2.1.0) fastlane-plugin-xcodegen (1.1.0) fastlane-plugin-brew (~> 0.1.1) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -140,7 +143,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -162,12 +165,12 @@ GEM signet (>= 0.16, < 2.a) highline (2.0.3) http-accept (1.7.0) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) json (2.7.2) - jwt (2.8.2) + jwt (2.9.3) base64 mime-types (3.5.2) mime-types-data (~> 3.2015) @@ -195,8 +198,7 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) - rexml (3.2.9) - strscan + rexml (3.3.8) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -210,7 +212,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -220,18 +222,18 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/IntegrationTests/Sources/Common.swift b/IntegrationTests/Sources/Common.swift index 51c334cd34..b540a3b11d 100644 --- a/IntegrationTests/Sources/Common.swift +++ b/IntegrationTests/Sources/Common.swift @@ -12,21 +12,20 @@ extension XCUIApplication { let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] XCTAssertTrue(getStartedButton.waitForExistence(timeout: 10.0)) - getStartedButton.tap() + getStartedButton.tapCenter() - // Get started is network bound, wait for the change homeserver button for longer let changeHomeserverButton = buttons[A11yIdentifiers.serverConfirmationScreen.changeServer] - XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 30.0)) - changeHomeserverButton.tap() + XCTAssertTrue(changeHomeserverButton.waitForExistence(timeout: 10.0)) + changeHomeserverButton.tapCenter() let homeserverTextField = textFields[A11yIdentifiers.changeServerScreen.server] XCTAssertTrue(homeserverTextField.waitForExistence(timeout: 10.0)) - homeserverTextField.clearAndTypeText(homeserver) + homeserverTextField.clearAndTypeText(homeserver, app: self) let confirmButton = buttons[A11yIdentifiers.changeServerScreen.continue] XCTAssertTrue(confirmButton.waitForExistence(timeout: 10.0)) - confirmButton.tap() + confirmButton.tapCenter() // Wait for server confirmation to finish let doesNotExistPredicate = NSPredicate(format: "exists == 0") @@ -35,23 +34,23 @@ extension XCUIApplication { let continueButton = buttons[A11yIdentifiers.serverConfirmationScreen.continue] XCTAssertTrue(continueButton.waitForExistence(timeout: 30.0)) - continueButton.tap() + continueButton.tapCenter() let usernameTextField = textFields[A11yIdentifiers.loginScreen.emailUsername] XCTAssertTrue(usernameTextField.waitForExistence(timeout: 10.0)) - usernameTextField.clearAndTypeText(username) + usernameTextField.clearAndTypeText(username, app: self) let passwordTextField = secureTextFields[A11yIdentifiers.loginScreen.password] XCTAssertTrue(passwordTextField.waitForExistence(timeout: 10.0)) - passwordTextField.clearAndTypeText(password) + passwordTextField.clearAndTypeText(password, app: self) let nextButton = buttons[A11yIdentifiers.loginScreen.continue] XCTAssertTrue(nextButton.waitForExistence(timeout: 10.0)) XCTAssertTrue(nextButton.isEnabled) - nextButton.tap() + nextButton.tapCenter() // Wait for login to finish currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: usernameTextField) @@ -63,7 +62,7 @@ extension XCUIApplication { // Tapping the sheet button while animating upwards fails. Wait for it to settle sleep(1) - savePasswordButton.tap() + savePasswordButton.tapCenter() } // Wait for the home screen to become visible. @@ -80,17 +79,17 @@ extension XCUIApplication { let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar] // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 - profileButton.forceTap() + profileButton.tapCenter() // Logout let logoutButton = buttons[A11yIdentifiers.settingsScreen.logout] XCTAssertTrue(logoutButton.waitForExistence(timeout: 10.0)) - logoutButton.tap() + logoutButton.tapCenter() // Confirm logout let alertLogoutButton = alerts.firstMatch.buttons["Sign out"] XCTAssertTrue(alertLogoutButton.waitForExistence(timeout: 10.0)) - alertLogoutButton.tap() + alertLogoutButton.tapCenter() // Check that we're back on the login screen let getStartedButton = buttons[A11yIdentifiers.authenticationStartScreen.signIn] diff --git a/IntegrationTests/Sources/UserFlowTests.swift b/IntegrationTests/Sources/UserFlowTests.swift index 46e4e0f085..04edd6bcc5 100644 --- a/IntegrationTests/Sources/UserFlowTests.swift +++ b/IntegrationTests/Sources/UserFlowTests.swift @@ -8,6 +8,9 @@ import XCTest class UserFlowTests: XCTestCase { + private static let integrationTestsRoomName = "Element X iOS Integration Tests" + private static let integrationTestsMessage = "Go down in flames!" + private var app: XCUIApplication! override func setUp() { @@ -16,14 +19,27 @@ class UserFlowTests: XCTestCase { } func testUserFlow() { + checkRoomFlows() + checkSettings() checkRoomCreation() - // Open the first room in the list. - let firstRoom = app.buttons.matching(NSPredicate(format: "identifier BEGINSWITH %@", A11yIdentifiers.homeScreen.roomNamePrefix)).firstMatch + app.logout() + } + + // Assumes app is on the home screen + private func checkRoomFlows() { + // Search for the special test room + let searchField = app.searchFields.firstMatch + searchField.clearAndTypeText(Self.integrationTestsRoomName, app: app) + + // And open it + let firstRoom = app.buttons.matching(NSPredicate(format: "identifier CONTAINS %@", Self.integrationTestsRoomName)).firstMatch XCTAssertTrue(firstRoom.waitForExistence(timeout: 10.0)) - firstRoom.tap() + firstRoom.tapCenter() + + sendMessages() checkPhotoSharing() @@ -35,20 +51,57 @@ class UserFlowTests: XCTestCase { checkRoomDetails() - app.logout() + // Go back to the room list + tapOnBackButton("Chats") + + // Cancel initial the room search + let searchCancelButton = app.buttons["Cancel"].firstMatch + XCTAssertTrue(searchCancelButton.waitForExistence(timeout: 10.0)) + searchCancelButton.tapCenter() + } + + private func sendMessages() { + var composerTextField = app.textViews[A11yIdentifiers.roomScreen.messageComposer].firstMatch + XCTAssertTrue(composerTextField.waitForExistence(timeout: 10.0)) + composerTextField.clearAndTypeText(Self.integrationTestsMessage, app: app) + + var sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch + XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) + sendButton.tapCenter() + + sleep(10) // Wait for the message to be sent + + // Switch to the rich text editor + tapOnMenu(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions) + tapOnButton(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting) + + composerTextField = app.textViews[A11yIdentifiers.roomScreen.messageComposer].firstMatch + XCTAssertTrue(composerTextField.waitForExistence(timeout: 10.0)) + composerTextField.clearAndTypeText(Self.integrationTestsMessage, app: app) + + sendButton = app.buttons[A11yIdentifiers.roomScreen.sendButton].firstMatch + XCTAssertTrue(sendButton.waitForExistence(timeout: 10.0)) + sendButton.tapCenter() + + sleep(5) // Wait for the message to be sent + + // Close the formatting options + app.buttons[A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions].tapCenter() } private func checkPhotoSharing() { - // Open attachments picker tapOnMenu(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions) - - // Open photo library picker tapOnButton(A11yIdentifiers.roomScreen.attachmentPickerPhotoLibrary) + sleep(10) // Wait for the picker to load + // Tap on the second image. First one is always broken on simulators. let secondImage = app.scrollViews.images.element(boundBy: 1) - XCTAssertTrue(secondImage.waitForExistence(timeout: 10.0)) // Photo library takes a bit to load - secondImage.tap() + XCTAssertTrue(secondImage.waitForExistence(timeout: 20.0)) // Photo library takes a bit to load + secondImage.tapCenter() + + // Wait for the image to be processed and the new screen to appear + sleep(10) // Cancel the upload flow tapOnButton("Cancel", waitForDisappearance: true) @@ -58,6 +111,8 @@ class UserFlowTests: XCTestCase { tapOnMenu(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions) tapOnButton(A11yIdentifiers.roomScreen.attachmentPickerDocuments) + sleep(10) // Wait for the picker to load + tapOnButton("Cancel", waitForDisappearance: true) } @@ -65,6 +120,8 @@ class UserFlowTests: XCTestCase { tapOnMenu(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions) tapOnButton(A11yIdentifiers.roomScreen.attachmentPickerLocation) + sleep(10) // Wait for the picker to load + // The order of the alerts is a bit of a mistery so try twice allowLocationPermissionOnce() @@ -72,7 +129,7 @@ class UserFlowTests: XCTestCase { // Handle map loading errors (missing credentials) let alertOkButton = app.alerts.firstMatch.buttons["OK"].firstMatch if alertOkButton.waitForExistence(timeout: 10.0) { - alertOkButton.tap() + alertOkButton.tapCenter() } allowLocationPermissionOnce() @@ -84,7 +141,7 @@ class UserFlowTests: XCTestCase { let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let notificationAlertAllowButton = springboard.buttons["Allow Once"].firstMatch if notificationAlertAllowButton.waitForExistence(timeout: 10.0) { - notificationAlertAllowButton.tap() + notificationAlertAllowButton.tapCenter() } } @@ -121,7 +178,7 @@ class UserFlowTests: XCTestCase { // Open the room details let roomHeader = app.staticTexts[A11yIdentifiers.roomScreen.name] XCTAssertTrue(roomHeader.waitForExistence(timeout: 10.0)) - roomHeader.tap() + roomHeader.tapCenter() // Open the room member details tapOnButton(A11yIdentifiers.roomDetailsScreen.people) @@ -129,7 +186,7 @@ class UserFlowTests: XCTestCase { // Open the first member's details. Loading members for big rooms can take a while. let firstRoomMember = app.scrollViews.buttons.firstMatch XCTAssertTrue(firstRoomMember.waitForExistence(timeout: 1000.0)) - firstRoomMember.tap() + firstRoomMember.tapCenter() // Go back to the room member details tapOnBackButton("People") @@ -139,9 +196,6 @@ class UserFlowTests: XCTestCase { // Go back to the room tapOnBackButton("Chat") - - // Go back to the room list - tapOnBackButton("Chats") } private func checkSettings() { @@ -152,7 +206,7 @@ class UserFlowTests: XCTestCase { let profileButton = app.buttons[A11yIdentifiers.homeScreen.userAvatar] // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 - profileButton.forceTap() + profileButton.tapCenter() // Open analytics tapOnButton(A11yIdentifiers.settingsScreen.analytics) @@ -179,7 +233,7 @@ class UserFlowTests: XCTestCase { private func tapOnButton(_ identifier: String, waitForDisappearance: Bool = false) { let button = app.buttons[identifier] XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.tap() + button.tapCenter() if waitForDisappearance { let doesNotExistPredicate = NSPredicate(format: "exists == 0") @@ -191,7 +245,7 @@ class UserFlowTests: XCTestCase { private func tapOnMenu(_ identifier: String) { let button = app.buttons[identifier] XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.forceTap() + button.tapCenter() } /// Taps on a back button that the system configured with a label but no identifier. @@ -201,6 +255,6 @@ class UserFlowTests: XCTestCase { private func tapOnBackButton(_ label: String = "Back") { let button = app.buttons.matching(NSPredicate(format: "label == %@ && identifier == ''", label)).firstMatch XCTAssertTrue(button.waitForExistence(timeout: 10.0)) - button.tap() + button.tapCenter() } } diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index ad01691b4d..f767a9538d 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -11,6 +11,7 @@ import UserNotifications struct NotificationContentBuilder { let messageEventStringBuilder: RoomMessageEventStringBuilder + let settings: CommonSettingsProtocol /// Process the given notification item proxy /// - Parameters: @@ -100,6 +101,8 @@ struct NotificationContentBuilder { let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName).characters) + guard !settings.hideTimelineMedia else { return notification } + switch messageType { case .image(content: let content): notification = await notification.addMediaAttachment(using: mediaProvider, diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 5443c1ffc3..3ea1ed8a6c 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -33,7 +33,8 @@ import UserNotifications // database, logging, etc. are only ever setup once per *process* private let settings: CommonSettingsProtocol = AppSettings() -private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), prefix: .none)) +private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), prefix: .none), + settings: settings) private let keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) diff --git a/NSE/Sources/Other/NSELogger.swift b/NSE/Sources/Other/NSELogger.swift index c0f7cc4f28..985038136a 100644 --- a/NSE/Sources/Other/NSELogger.swift +++ b/NSE/Sources/Other/NSELogger.swift @@ -72,7 +72,7 @@ enum NSELogger { } isConfigured = true - MXLog.configure(target: "nse", logLevel: logLevel) + MXLog.configure(currentTarget: "nse", filePrefix: "nse", logLevel: logLevel) } static func logMemory(with tag: String) { diff --git a/NSE/Sources/Other/NSEUserSession.swift b/NSE/Sources/Other/NSEUserSession.swift index 54e915f193..6a2d99dbc2 100644 --- a/NSE/Sources/Other/NSEUserSession.swift +++ b/NSE/Sources/Other/NSEUserSession.swift @@ -34,7 +34,7 @@ final class NSEUserSession { slidingSync: .restored, sessionDelegate: clientSessionDelegate, appHooks: appHooks, - invisibleCryptoEnabled: appSettings.invisibleCryptoEnabled) + enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode) .sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath, cachePath: credentials.restorationToken.sessionDirectories.cachePath) .username(username: credentials.userID) diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index fae28e976b..417469aa13 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -87,6 +87,7 @@ targets: - path: ../../ElementX/Sources/Other/Extensions/ImageCache.swift - path: ../../ElementX/Sources/Other/Extensions/LayoutDirection.swift - path: ../../ElementX/Sources/Other/Extensions/NSRegularExpresion.swift + - path: ../../ElementX/Sources/Other/Extensions/ProcessInfo.swift - path: ../../ElementX/Sources/Other/Extensions/String.swift - path: ../../ElementX/Sources/Other/Extensions/Task.swift - path: ../../ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -112,3 +113,4 @@ targets: - path: ../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift - path: ../../ElementX/Sources/Services/UserSession/SessionDirectories.swift + - path: ../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 47fb1f5988..c42a3a3024 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -17,12 +17,6 @@ extension PreviewTests { } } - func test_analyticsPromptScreenCheckmarkItem() { - for preview in AnalyticsPromptScreenCheckmarkItem_Previews._allPreviews { - assertSnapshots(matching: preview) - } - } - func test_analyticsPromptScreen() { for preview in AnalyticsPromptScreen_Previews._allPreviews { assertSnapshots(matching: preview) @@ -83,6 +77,12 @@ extension PreviewTests { } } + func test_bigIcon() { + for preview in BigIcon_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_blockedUsersScreen() { for preview in BlockedUsersScreen_Previews._allPreviews { assertSnapshots(matching: preview) @@ -221,12 +221,6 @@ extension PreviewTests { } } - func test_heroImage() { - for preview in HeroImage_Previews._allPreviews { - assertSnapshots(matching: preview) - } - } - func test_highlightedTimelineItemModifier() { for preview in HighlightedTimelineItemModifier_Previews._allPreviews { assertSnapshots(matching: preview) @@ -245,6 +239,12 @@ extension PreviewTests { } } + func test_homeScreenKnockedCell() { + for preview in HomeScreenKnockedCell_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_homeScreenRecoveryKeyConfirmationBanner() { for preview in HomeScreenRecoveryKeyConfirmationBanner_Previews._allPreviews { assertSnapshots(matching: preview) @@ -311,6 +311,12 @@ extension PreviewTests { } } + func test_loadableImage() { + for preview in LoadableImage_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_locationMarkerView() { for preview in LocationMarkerView_Previews._allPreviews { assertSnapshots(matching: preview) @@ -683,6 +689,12 @@ extension PreviewTests { } } + func test_roomScreenFooterView() { + for preview in RoomScreenFooterView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_roomScreen() { for preview in RoomScreen_Previews._allPreviews { assertSnapshots(matching: preview) @@ -737,6 +749,12 @@ extension PreviewTests { } } + func test_sessionVerificationRequestDetailsView() { + for preview in SessionVerificationRequestDetailsView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_sessionVerification() { for preview in SessionVerification_Previews._allPreviews { assertSnapshots(matching: preview) @@ -929,6 +947,12 @@ extension PreviewTests { } } + func test_visualListItem() { + for preview in VisualListItem_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_voiceMessageButton() { for preview in VoiceMessageButton_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/PreviewTests.swift b/PreviewTests/Sources/PreviewTests.swift index 4d7148508d..5fd769481a 100644 --- a/PreviewTests/Sources/PreviewTests.swift +++ b/PreviewTests/Sources/PreviewTests.swift @@ -11,6 +11,7 @@ import XCTest @testable import ElementX @testable import SnapshotTesting +@MainActor class PreviewTests: XCTestCase { private let deviceConfig: ViewImageConfig = .iPhoneX private let simulatorDevice: String? = "iPhone14,6" // iPhone SE 3rd Generation @@ -50,6 +51,22 @@ class PreviewTests: XCTestCase { // MARK: - Snapshots func assertSnapshots(matching preview: _Preview, testName: String = #function) { + let preferences = SnapshotPreferences() + + let preferenceReadingView = preview.content + .onPreferenceChange(SnapshotDelayPreferenceKey.self) { preferences.delay = $0 } + .onPreferenceChange(SnapshotPrecisionPreferenceKey.self) { preferences.precision = $0 } + .onPreferenceChange(SnapshotPerceptualPrecisionPreferenceKey.self) { preferences.perceptualPrecision = $0 } + + // Render an image of the view in order to trigger the preference updates to occur. + let imageRenderer = ImageRenderer(content: preferenceReadingView) + _ = imageRenderer.uiImage + + // Delay the test now - a delay after creating the `snapshotView` results in the underlying view not getting updated for snapshotting. + if preferences.delay != .zero { + wait(for: preferences.delay) + } + for deviceName in snapshotDevices { guard var device = PreviewDevice(rawValue: deviceName).snapshotDevice() else { fatalError("Unknown device name: \(deviceName)") @@ -58,12 +75,13 @@ class PreviewTests: XCTestCase { device.safeArea = .one // Ignore specific device display scale let traits = UITraitCollection(displayScale: 2.0) - if let failure = assertSnapshots(matching: AnyView(preview.content), + if let failure = assertSnapshots(matching: preview.content, name: preview.displayName, isScreen: preview.layout == .device, device: device, testName: testName + deviceName + "-" + localeCode, - traits: traits) { + traits: traits, + preferences: preferences) { XCTFail(failure) } } @@ -85,19 +103,12 @@ class PreviewTests: XCTestCase { } private func assertSnapshots(matching view: AnyView, - name: String?, isScreen: Bool, + name: String?, + isScreen: Bool, device: ViewImageConfig, testName: String = #function, - traits: UITraitCollection = .init()) -> String? { - var delay: TimeInterval = 0 - var precision: Float = 1 - var perceptualPrecision: Float = 1 - - let view = view - .onPreferenceChange(SnapshotDelayPreferenceKey.self) { delay = $0 } - .onPreferenceChange(SnapshotPrecisionPreferenceKey.self) { precision = $0 } - .onPreferenceChange(SnapshotPerceptualPrecisionPreferenceKey.self) { perceptualPrecision = $0 } - + traits: UITraitCollection = .init(), + preferences: SnapshotPreferences) -> String? { let matchingView = isScreen ? AnyView(view) : AnyView(view .frame(width: device.size?.width) .fixedSize(horizontal: false, vertical: true) @@ -105,15 +116,27 @@ class PreviewTests: XCTestCase { return withSnapshotTesting(record: recordMode) { verifySnapshot(of: matchingView, - as: .prefireImage(precision: { precision }, - perceptualPrecision: { perceptualPrecision }, - duration: { delay }, + as: .prefireImage(preferences: preferences, layout: isScreen ? .device(config: device) : .sizeThatFits, traits: traits), named: name, testName: testName) } } + + private func wait(for duration: TimeInterval) { + let expectation = XCTestExpectation(description: "Wait") + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + expectation.fulfill() + } + _ = XCTWaiter.wait(for: [expectation], timeout: duration + 1) + } +} + +private class SnapshotPreferences { + var delay: TimeInterval = 0 + var precision: Float = 1 + var perceptualPrecision: Float = 1 } // MARK: - SnapshotTesting + Extensions @@ -144,9 +167,7 @@ private extension PreviewDevice { private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { static func prefireImage(drawHierarchyInKeyWindow: Bool = false, - precision: @escaping () -> Float, - perceptualPrecision: @escaping () -> Float, - duration: @escaping () -> TimeInterval, + preferences: SnapshotPreferences, layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init()) -> Snapshotting { let config: ViewImageConfig @@ -165,7 +186,7 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { config = .init(safeArea: .one, size: size, traits: traits) } - return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale)) + return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale)) .asyncPullback { view in var config = config @@ -181,31 +202,19 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { controller = hostingController } - - return Async { callback in - let strategy = snapshotView(config: config, - drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, - traits: traits, - view: controller.view, - viewController: controller) - - let duration = duration() - if duration != .zero { - let expectation = XCTestExpectation(description: "Wait") - DispatchQueue.main.asyncAfter(deadline: .now() + duration) { - expectation.fulfill() - } - _ = XCTWaiter.wait(for: [expectation], timeout: duration + 1) - } - strategy.run(callback) - } + + return snapshotView(config: config, + drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, + traits: traits, + view: controller.view, + viewController: controller) } } } private extension Diffing where Value == UIImage { - static func prefireImage(precision: @escaping () -> Float, perceptualPrecision: @escaping () -> Float, scale: CGFloat?) -> Diffing { - lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: 0.98, scale: scale) + static func prefireImage(preferences: SnapshotPreferences, scale: CGFloat?) -> Diffing { + lazy var originalDiffing = Diffing.image(precision: preferences.precision, perceptualPrecision: preferences.perceptualPrecision, scale: scale) return Diffing(toData: { originalDiffing.toData($0) }, fromData: { originalDiffing.fromData($0) }, diff: { originalDiffing.diff($0, $1) }) diff --git a/README.md b/README.md index d9ad55c092..244860fffb 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,17 @@ # Element X iOS -ElementX iOS is a [Matrix](https://matrix.org/) iOS Client provided by [Element](https://element.io/). +Element X iOS is a [Matrix](https://matrix.org/) iOS Client provided by [Element](https://element.io/). -The application is a total rewrite of [Element-iOS](https://github.com/element-hq/element-ios) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targetting devices running iOS 16+. +The application is a total rewrite of [Element iOS](https://github.com/element-hq/element-ios) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running iOS 17+. ## Rust SDK -ElementX leverages the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) through an FFI layer exposed as a [swift package](https://github.com/matrix-org/matrix-rust-components-swift) that the final client can directly import and use. - -We're doing this as a way to share code between platforms and while we've seen promising results it's still in the experimental stage and bound to change. +Element X leverages the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) through an FFI layer exposed as a [swift package](https://github.com/matrix-org/matrix-rust-components-swift) that the final client can directly import and use. We're doing this as a way to share code between platforms, with [Element X Android](https://github.com/element-hq/element-x-android) using the same SDK. ## Status -This project is in work in progress. The app does not cover yet all functionalities we expect. +This project is in an early rollout & migration phase. ## Contributing @@ -37,9 +35,9 @@ Please refer to the [setting up a development environment](CONTRIBUTING.md#setti ## Support -When you are experiencing an issue on ElementX iOS, please first search in [GitHub issues](https://github.com/element-hq/element-x-ios/issues) +When you are experiencing an issue on Element X iOS, please first search in [GitHub issues](https://github.com/element-hq/element-x-ios/issues) and then in [#element-x-ios:matrix.org](https://matrix.to/#/#element-x-ios:matrix.org). -If after your research you still have a question, ask at [#element-x-ios:matrix.org](https://matrix.to/#/#element-x-ios:matrix.org). Otherwise feel free to create a GitHub issue if you encounter a bug or a crash, by explaining clearly in detail what happened. You can also perform bug reporting (Rageshake) from the Element application by shaking your phone or going to the application settings. This is especially recommended when you encounter a crash. +If after your research you still have a question, ask at [#element-x-ios:matrix.org](https://matrix.to/#/#element-x-ios:matrix.org). Otherwise feel free to create a GitHub issue if you encounter a bug or a crash, by explaining clearly in detail what happened. You can also perform bug reporting (Rageshake) from the Element application by going to the application settings. This is especially recommended when you encounter a crash. ## Forking diff --git a/TchapX/development/SupportingFiles/Info.plist b/TchapX/development/SupportingFiles/Info.plist index fb51c555a9..6c40a6cd0c 100644 --- a/TchapX/development/SupportingFiles/Info.plist +++ b/TchapX/development/SupportingFiles/Info.plist @@ -2,27 +2,8 @@ - BGTaskSchedulerPermittedIdentifiers - - io.element.elementx.background.refresh - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - $(APP_DISPLAY_NAME) - CFBundleDocumentTypes - - - CFBundleTypeName - Mention Pills - CFBundleTypeRole - Viewer - LSHandlerRank - Owner - LSItemContentTypes - $(PILLS_UT_TYPE_IDENTIFIER) - - CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -32,94 +13,10 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - APPL + BNDL CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - Element Call - CFBundleURLSchemes - - io.element.call - - - - CFBundleTypeRole - Editor - CFBundleURLName - Application - CFBundleURLSchemes - - io.element - - - + 1.0 CFBundleVersion - $(CURRENT_PROJECT_VERSION) - ITSAppUsesNonExemptEncryption - - LSSupportsOpeningDocumentsInPlace - - NSCameraUsageDescription - To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera. - NSFaceIDUsageDescription - Face ID is used to access your app. - NSLocationWhenInUseUsageDescription - Grant location access so that $(APP_DISPLAY_NAME) can share your location. - NSMicrophoneUsageDescription - To record and send messages with audio, $(APP_DISPLAY_NAME) needs to access the microphone. - NSPhotoLibraryAddUsageDescription - Allows saving photos and videos to your library. - NSUserActivityTypes - - INSendMessageIntent - INStartCallIntent - - UIBackgroundModes - - audio - fetch - processing - voip - - UILaunchScreen - - UIColorName - colors/background-color - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UTExportedTypeDeclarations - - - UTTypeConformsTo - - public.text - - UTTypeDescription - Mention Pills - UTTypeIdentifier - $(PILLS_UT_TYPE_IDENTIFIER) - - - appGroupIdentifier - $(APP_GROUP_IDENTIFIER) - baseBundleIdentifier - $(BASE_BUNDLE_IDENTIFIER) - keychainAccessGroupIdentifier - $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) - mapLibreAPIKey - $(MAPLIBRE_API_KEY) - productionAppName - $(PRODUCTION_APP_NAME) + 1 diff --git a/TchapX/development/SupportingFiles/NSE/target.yml b/TchapX/development/SupportingFiles/NSE/target.yml index 0f542f37ec..ad1ae35bce 100644 --- a/TchapX/development/SupportingFiles/NSE/target.yml +++ b/TchapX/development/SupportingFiles/NSE/target.yml @@ -25,7 +25,7 @@ schemes: test: config: Debug disableMainThreadChecker: false - + targets: NSE-Development: type: app-extension @@ -87,6 +87,7 @@ targets: - path: ../../../../ElementX/Sources/Other/Extensions/ImageCache.swift - path: ../../../../ElementX/Sources/Other/Extensions/LayoutDirection.swift - path: ../../../../ElementX/Sources/Other/Extensions/NSRegularExpresion.swift + - path: ../../../../ElementX/Sources/Other/Extensions/ProcessInfo.swift - path: ../../../../ElementX/Sources/Other/Extensions/String.swift - path: ../../../../ElementX/Sources/Other/Extensions/Task.swift - path: ../../../../ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -112,6 +113,7 @@ targets: - path: ../../../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../../../ElementX/Sources/Services/UserSession/RestorationToken.swift - path: ../../../../ElementX/Sources/Services/UserSession/SessionDirectories.swift + - path: ../../../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift - path: ../../../../ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift - path: ../../../../ElementX/Sources/Application/AppSettings.swift - path: ../../../../ElementX/Sources/AppHooks/AppHooks.swift diff --git a/TchapX/production/SupportingFiles/NSE/target.yml b/TchapX/production/SupportingFiles/NSE/target.yml index b5accca4e8..9c6ef5a107 100644 --- a/TchapX/production/SupportingFiles/NSE/target.yml +++ b/TchapX/production/SupportingFiles/NSE/target.yml @@ -25,7 +25,7 @@ schemes: test: config: Debug disableMainThreadChecker: false - + targets: NSE-Production: type: app-extension @@ -87,6 +87,7 @@ targets: - path: ../../../../ElementX/Sources/Other/Extensions/ImageCache.swift - path: ../../../../ElementX/Sources/Other/Extensions/LayoutDirection.swift - path: ../../../../ElementX/Sources/Other/Extensions/NSRegularExpresion.swift + - path: ../../../../ElementX/Sources/Other/Extensions/ProcessInfo.swift - path: ../../../../ElementX/Sources/Other/Extensions/String.swift - path: ../../../../ElementX/Sources/Other/Extensions/Task.swift - path: ../../../../ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -112,6 +113,7 @@ targets: - path: ../../../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../../../ElementX/Sources/Services/UserSession/RestorationToken.swift - path: ../../../../ElementX/Sources/Services/UserSession/SessionDirectories.swift + - path: ../../../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift - path: ../../../../ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift - path: ../../../../ElementX/Sources/Application/AppSettings.swift - path: ../../../../ElementX/Sources/AppHooks/AppHooks.swift diff --git a/TchapX/staging/SupportingFiles/NSE/target.yml b/TchapX/staging/SupportingFiles/NSE/target.yml index a37f608a39..55ce91cf58 100644 --- a/TchapX/staging/SupportingFiles/NSE/target.yml +++ b/TchapX/staging/SupportingFiles/NSE/target.yml @@ -25,7 +25,7 @@ schemes: test: config: Debug disableMainThreadChecker: false - + targets: NSE-Staging: type: app-extension @@ -87,6 +87,7 @@ targets: - path: ../../../../ElementX/Sources/Other/Extensions/ImageCache.swift - path: ../../../../ElementX/Sources/Other/Extensions/LayoutDirection.swift - path: ../../../../ElementX/Sources/Other/Extensions/NSRegularExpresion.swift + - path: ../../../../ElementX/Sources/Other/Extensions/ProcessInfo.swift - path: ../../../../ElementX/Sources/Other/Extensions/String.swift - path: ../../../../ElementX/Sources/Other/Extensions/Task.swift - path: ../../../../ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -112,6 +113,7 @@ targets: - path: ../../../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../../../ElementX/Sources/Services/UserSession/RestorationToken.swift - path: ../../../../ElementX/Sources/Services/UserSession/SessionDirectories.swift + - path: ../../../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift - path: ../../../../ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift - path: ../../../../ElementX/Sources/Application/AppSettings.swift - path: ../../../../ElementX/Sources/AppHooks/AppHooks.swift diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateScreen.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateScreen.swift index e5f72310a5..ce5e2d0bec 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateScreen.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateScreen.swift @@ -24,7 +24,7 @@ struct TemplateScreen: View { } .compoundList() .navigationTitle(context.viewState.title) - .onChange(of: context.composerText) { _ in + .onChange(of: context.composerText) { context.send(viewAction: .textChanged) } } diff --git a/Tools/Sources/BuildSDK.swift b/Tools/Sources/BuildSDK.swift index 7a6346f503..156cbfb227 100644 --- a/Tools/Sources/BuildSDK.swift +++ b/Tools/Sources/BuildSDK.swift @@ -46,7 +46,7 @@ struct BuildSDK: ParsableCommand { Rust is missing the necessary targets to build the SDK. Run the following command to install them: - rustup target add \(missingTargets.joined(separator: " ")) --toolchain nightly + rustup target add \(missingTargets.joined(separator: " ")) """ default: diff --git a/UITests/Sources/AppLockSetupUITests.swift b/UITests/Sources/AppLockSetupUITests.swift index 0911636358..5c2660af90 100644 --- a/UITests/Sources/AppLockSetupUITests.swift +++ b/UITests/Sources/AppLockSetupUITests.swift @@ -119,6 +119,8 @@ class AppLockSetupUITests: XCTestCase { func testCancel() async throws { app = Application.launch(.appLockSetupFlowUnlock) + app.showKeyboardIfNeeded() // The secure text field is focussed automatically + // Create PIN screen. try await app.assertScreenshot(.appLockSetupFlowUnlock) @@ -134,13 +136,13 @@ class AppLockSetupUITests: XCTestCase { let textField = app.secureTextFields[A11yIdentifiers.appLockSetupPINScreen.textField] XCTAssert(textField.waitForExistence(timeout: 10)) - textField.clearAndTypeText("2023") + textField.clearAndTypeText("2023", app: app) } private func enterDifferentPIN() { let textField = app.secureTextFields[A11yIdentifiers.appLockSetupPINScreen.textField] XCTAssert(textField.waitForExistence(timeout: 10)) - textField.clearAndTypeText("2233") + textField.clearAndTypeText("2233", app: app) } } diff --git a/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift b/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift index e65b8c7875..40ff6d82c9 100644 --- a/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift +++ b/UITests/Sources/AuthenticationFlowCoordinatorUITests.swift @@ -19,9 +19,13 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Server Confirmation: Tap continue button app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + // Login Screen: Enter valid credentials - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n") - app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678") + app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n", app: app) + app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678", app: app) try await app.assertScreenshot(.authenticationFlow) @@ -39,20 +43,43 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Server Confirmation: Tap continue button app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + // Login Screen: Enter invalid credentials - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice") - app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321") + app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice", app: app) + app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321", app: app) - // Login Screen: Tap next - let nextButton = app.buttons[A11yIdentifiers.loginScreen.continue] - XCTAssertTrue(nextButton.waitForExistence(timeout: 2.0)) - XCTAssertTrue(nextButton.isEnabled) - nextButton.tap() + // Login Screen: Tap continue + XCTAssertTrue(continueButton.isEnabled) + continueButton.tap() // Then login should fail. XCTAssertTrue(app.alerts.element.waitForExistence(timeout: 2.0), "An error alert should be shown when attempting login with invalid credentials.") } + func testLoginWithUnsupportedUserID() async throws { + // Given the authentication flow. + let app = Application.launch(.authenticationFlow) + + // Splash Screen: Tap get started button + app.buttons[A11yIdentifiers.authenticationStartScreen.signIn].tap() + + // Server Confirmation: Tap continue button + app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() + + // Login Screen: Wait for continue button to appear + let continueButton = app.buttons[A11yIdentifiers.loginScreen.continue] + XCTAssertTrue(continueButton.waitForExistence(timeout: 2.0)) + + // When entering a username on a homeserver with an unsupported flow. + app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n", app: app) + + // Then the screen should not allow login to continue. + try await app.assertScreenshot(.authenticationFlow, step: 1) + } + func testSelectingOIDCServer() { // Given the authentication flow. let app = Application.launch(.authenticationFlow) @@ -64,7 +91,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { app.buttons[A11yIdentifiers.serverConfirmationScreen.changeServer].tap() // Server Selection: Clear the default, enter OIDC server and continue. - app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("company.com\n") + app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("company.com\n", app: app) // Server Confirmation: Tap continue button app.buttons[A11yIdentifiers.serverConfirmationScreen.continue].tap() diff --git a/UITests/Sources/BugReportUITests.swift b/UITests/Sources/BugReportUITests.swift index 03b8ec214c..d87441c964 100644 --- a/UITests/Sources/BugReportUITests.swift +++ b/UITests/Sources/BugReportUITests.swift @@ -20,13 +20,13 @@ class BugReportUITests: XCTestCase { let app = Application.launch(.bugReport) // Type 4 characters and the send button should be disabled. - app.textViews[A11yIdentifiers.bugReportScreen.report].clearAndTypeText("Text") + app.textViews[A11yIdentifiers.bugReportScreen.report].clearAndTypeText("Text", app: app) XCTAssert(app.switches[A11yIdentifiers.bugReportScreen.sendLogs].isOn) XCTAssert(!app.switches[A11yIdentifiers.bugReportScreen.canContact].isOn) try await app.assertScreenshot(.bugReport, step: 2) // Type more than 4 characters and send the button should become enabled. - app.textViews[A11yIdentifiers.bugReportScreen.report].clearAndTypeText("Longer text") + app.textViews[A11yIdentifiers.bugReportScreen.report].clearAndTypeText("Longer text", app: app) XCTAssert(app.switches[A11yIdentifiers.bugReportScreen.sendLogs].isOn) XCTAssert(!app.switches[A11yIdentifiers.bugReportScreen.canContact].isOn) try await app.assertScreenshot(.bugReport, step: 3) diff --git a/UITests/Sources/EncryptionResetUITests.swift b/UITests/Sources/EncryptionResetUITests.swift new file mode 100644 index 0000000000..5bc5bea00d --- /dev/null +++ b/UITests/Sources/EncryptionResetUITests.swift @@ -0,0 +1,37 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@MainActor +class EncryptionResetUITests: XCTestCase { + var app: XCUIApplication! + + @MainActor enum Step { + static let resetScreen = 0 + static let passwordScreen = 1 + static let resetingEncryption = 2 + } + + func testPasswordFlow() async throws { + app = Application.launch(.encryptionReset) + + // Starting with the root screen. + try await app.assertScreenshot(.encryptionReset, step: Step.resetScreen) + + // Confirm the intent to reset. + app.buttons[A11yIdentifiers.encryptionResetScreen.continueReset].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionReset, step: Step.passwordScreen) + + // Enter the password and submit. + let passwordField = app.secureTextFields[A11yIdentifiers.encryptionResetPasswordScreen.passwordField] + passwordField.clearAndTypeText("supersecurepassword", app: app) + app.buttons[A11yIdentifiers.encryptionResetPasswordScreen.submit].tap() + try await app.assertScreenshot(.encryptionReset, step: Step.resetingEncryption) + } +} diff --git a/UITests/Sources/EncryptionSettingsUITests.swift b/UITests/Sources/EncryptionSettingsUITests.swift new file mode 100644 index 0000000000..9b22a6d437 --- /dev/null +++ b/UITests/Sources/EncryptionSettingsUITests.swift @@ -0,0 +1,80 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@MainActor +class EncryptionSettingsUITests: XCTestCase { + var app: XCUIApplication! + + @MainActor enum Step { + static let secureBackupScreenSetUp = 0 + static let keyBackupScreen = 1 + static let secureBackupScreenDisabled = 2 + static let setUpRecovery = 3 + static let changeRecovery = 4 + + static let secureBackupScreenOutOfSync = 5 + static let confirmRecovery = 6 + } + + func testFlow() async throws { + app = Application.launch(.encryptionSettings) + + // Starting with key storage and recovery enabled. + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + + // Toggle key storage off. + app.switches[A11yIdentifiers.secureBackupScreen.keyStorage].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.keyBackupScreen) + + // Confirm deletion of keys. + app.buttons[A11yIdentifiers.secureBackupKeyBackupScreen.deleteKeyStorage].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenDisabled) + + // Toggle key storage back on and set up recovery. + app.switches[A11yIdentifiers.secureBackupScreen.keyStorage].tap() + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.setUpRecovery) + + // Generate and copy a new recovery key. + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.done].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + + // Change the recovery key. + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.changeRecovery) + + // Generate and copy the updated recovery key. + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.generateRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.copyRecoveryKey].tap() + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.done].tap() + app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + } + + func testOutOfSyncFlow() async throws { + app = Application.launch(.encryptionSettingsOutOfSync) + + // Starting with key storage and recovery enabled. + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenOutOfSync) + + // Confirm the recovery key. + app.buttons[A11yIdentifiers.secureBackupScreen.recoveryKey].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.confirmRecovery) + + // Enter the recovery key and submit. + let recoveryKeyField = app.secureTextFields[A11yIdentifiers.secureBackupRecoveryKeyScreen.recoveryKeyField] + recoveryKeyField.clearAndTypeText("sUpe RSec rEtR Ecov ERYk Ey12", app: app) + app.buttons[A11yIdentifiers.secureBackupRecoveryKeyScreen.confirm].tap() + try await app.assertScreenshot(.encryptionSettings, step: Step.secureBackupScreenSetUp) + } +} diff --git a/UITests/Sources/LoginScreenUITests.swift b/UITests/Sources/LoginScreenUITests.swift deleted file mode 100644 index 2275e1648a..0000000000 --- a/UITests/Sources/LoginScreenUITests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import XCTest - -@MainActor -class LoginScreenUITests: XCTestCase { - func testMatrixDotOrg() async throws { - // Given the initial login screen which defaults to matrix.org. - let app = Application.launch(.login) - try await app.assertScreenshot(.login) - - // When typing in a username and password. - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:matrix.org") - app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678") - - // Then the form should be ready to submit. - try await app.assertScreenshot(.login, step: 0) - } - - func testUnsupported() async throws { - // Given the initial login screen. - let app = Application.launch(.login) - - // When entering a username on a homeserver with an unsupported flow. - app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("@test:server.net\n") - - // Then the screen should not allow login to continue. - try await app.assertScreenshot(.login, step: 1) - } -} diff --git a/UITests/Sources/PollFormScreenUITests.swift b/UITests/Sources/PollFormScreenUITests.swift index b2587b7483..fa02f72b9d 100644 --- a/UITests/Sources/PollFormScreenUITests.swift +++ b/UITests/Sources/PollFormScreenUITests.swift @@ -17,15 +17,15 @@ class PollFormScreenUITests: XCTestCase { func testFilledPoll() async throws { let app = Application.launch(.createPoll) let questionTextField = app.textViews[A11yIdentifiers.pollFormScreen.question] - questionTextField.forceTap() + questionTextField.tapCenter() questionTextField.typeText("Do you like polls?") let option1TextField = app.textViews[A11yIdentifiers.pollFormScreen.optionID(0)] - option1TextField.forceTap() + option1TextField.tapCenter() option1TextField.typeText("Yes") let option2TextField = app.textViews[A11yIdentifiers.pollFormScreen.optionID(1)] - option2TextField.forceTap() + option2TextField.tapCenter() option2TextField.typeText("No") // Dismiss the keyboard diff --git a/UITests/Sources/RoomMembersListScreenUITests.swift b/UITests/Sources/RoomMembersListScreenUITests.swift index 3bcecdfcfe..dc4c296498 100644 --- a/UITests/Sources/RoomMembersListScreenUITests.swift +++ b/UITests/Sources/RoomMembersListScreenUITests.swift @@ -19,7 +19,7 @@ class RoomMembersListScreenUITests: XCTestCase { let app = Application.launch(.roomMembersListScreenPendingInvites) let searchBar = app.searchFields.firstMatch - searchBar.clearAndTypeText("alice\n") + searchBar.clearAndTypeText("alice\n", app: app) try await app.assertScreenshot(.roomMembersListScreenPendingInvites, step: 1) } @@ -28,7 +28,7 @@ class RoomMembersListScreenUITests: XCTestCase { let app = Application.launch(.roomMembersListScreenPendingInvites) let searchBar = app.searchFields.firstMatch - searchBar.clearAndTypeText("bob\n") + searchBar.clearAndTypeText("bob\n", app: app) try await app.assertScreenshot(.roomMembersListScreenPendingInvites, step: 2) } diff --git a/UITests/Sources/ServerSelectionUITests.swift b/UITests/Sources/ServerSelectionUITests.swift index f5003421c3..02ac4b9a73 100644 --- a/UITests/Sources/ServerSelectionUITests.swift +++ b/UITests/Sources/ServerSelectionUITests.swift @@ -34,7 +34,7 @@ class ServerSelectionUITests: XCTestCase { let app = Application.launch(.serverSelection) // When typing in an invalid homeserver - app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("thisisbad\n") // The tests only accept an address from LoginHomeserver.mockXYZ + app.textFields[A11yIdentifiers.changeServerScreen.server].clearAndTypeText("thisisbad\n", app: app) // The tests only accept an address from LoginHomeserver.mockXYZ // Then an error should be shown and the confirmation button disabled. try await app.assertScreenshot(.serverSelection, step: 2) diff --git a/UITests/Sources/StartChatScreenUITests.swift b/UITests/Sources/StartChatScreenUITests.swift index 66a8e37c7f..63d09893e6 100644 --- a/UITests/Sources/StartChatScreenUITests.swift +++ b/UITests/Sources/StartChatScreenUITests.swift @@ -17,7 +17,7 @@ class StartChatScreenUITests: XCTestCase { func testSearchWithNoResults() async throws { let app = Application.launch(.startChat) let searchField = app.searchFields.firstMatch - searchField.clearAndTypeText("None\n") + searchField.clearAndTypeText("None\n", app: app) XCTAssert(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0)) try await app.assertScreenshot(.startChat, step: 1) } @@ -25,7 +25,7 @@ class StartChatScreenUITests: XCTestCase { func testSearchWithResults() async throws { let app = Application.launch(.startChatWithSearchResults) let searchField = app.searchFields.firstMatch - searchField.clearAndTypeText("Bob\n") + searchField.clearAndTypeText("Bob\n", app: app) XCTAssertFalse(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0)) XCTAssertEqual(app.collectionViews.firstMatch.cells.count, 2) try await app.assertScreenshot(.startChat, step: 2) diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index bb8d05177b..7b932236ca 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -13,6 +13,9 @@ class UserSessionScreenTests: XCTestCase { func testUserSessionFlows() async throws { let app = Application.launch(.userSessionScreen) + + app.swipeDown() // Make sure the header shows a large title + try await app.assertScreenshot(.userSessionScreen, step: 1) app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() @@ -20,7 +23,7 @@ class UserSessionScreenTests: XCTestCase { try await Task.sleep(for: .seconds(1)) try await app.assertScreenshot(.userSessionScreen, step: 2) - app.buttons[A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions].forceTap() + app.buttons[A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions].tapCenter() try await app.assertScreenshot(.userSessionScreen, step: 3) } @@ -47,7 +50,7 @@ class UserSessionScreenTests: XCTestCase { let textField = app.textFields["Display name"] XCTAssert(textField.waitForExistence(timeout: 10)) - let joinButton = app.buttons["Join call now"] + let joinButton = app.buttons["Continue"] XCTAssert(joinButton.waitForExistence(timeout: 10)) } } diff --git a/UnitTests/Resources/Media/test_animated_image.gif b/UnitTests/Resources/Media/test_animated_image.gif new file mode 100644 index 0000000000..c31ede46c1 --- /dev/null +++ b/UnitTests/Resources/Media/test_animated_image.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba429285ac3d7e29a5167bd7fab9a5b5721a9817afe824f61436520f6e6651ec +size 127823 diff --git a/UnitTests/Resources/Media/test_apple_image.heic b/UnitTests/Resources/Media/test_apple_image.heic new file mode 100644 index 0000000000..2b53713818 --- /dev/null +++ b/UnitTests/Resources/Media/test_apple_image.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f110a5cd97e0e9b38258833274944e816f36379631eeb6883ddf6babb9bef37 +size 1827200 diff --git a/UnitTests/Resources/Media/test_rotated_image.jpg b/UnitTests/Resources/Media/test_rotated_image.jpg new file mode 100644 index 0000000000..6169c3717d --- /dev/null +++ b/UnitTests/Resources/Media/test_rotated_image.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:094ceac734f4461ae9f1c47eae58c13270da6f407718b12e3142cac3dc0b38a7 +size 1735894 diff --git a/UnitTests/Sources/AppRouteURLParserTests.swift b/UnitTests/Sources/AppRouteURLParserTests.swift index 952e249f80..c24c4c8c21 100644 --- a/UnitTests/Sources/AppRouteURLParserTests.swift +++ b/UnitTests/Sources/AppRouteURLParserTests.swift @@ -73,33 +73,6 @@ class AppRouteURLParserTests: XCTestCase { XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), nil) } - func testOIDCCallbackRoute() { - // Given an OIDC callback for this app. - let callbackURL = appSettings.oidcRedirectURL.appending(queryItems: [URLQueryItem(name: "state", value: "12345"), - URLQueryItem(name: "code", value: "67890")]) - - // When parsing that route. - let route = appRouteURLParser.route(from: callbackURL) - - // Then it should be considered a valid OIDC callback. - XCTAssertEqual(route, AppRoute.oidcCallback(url: callbackURL)) - } - - func testOIDCCallbackAppVariantRoute() { - // Given an OIDC callback for a different app variant. - let callbackURL = appSettings.oidcRedirectURL - .deletingLastPathComponent() - .appending(component: "elementz") - .appending(queryItems: [URLQueryItem(name: "state", value: "12345"), - URLQueryItem(name: "code", value: "67890")]) - - // When parsing that route in this app. - let route = appRouteURLParser.route(from: callbackURL) - - // Then the route shouldn't be considered valid and should be ignored. - XCTAssertEqual(route, nil) - } - func testMatrixUserURL() { let userID = "@test:matrix.org" guard let url = URL(string: "https://matrix.to/#/\(userID)") else { diff --git a/UnitTests/Sources/AudioPlayerStateTests.swift b/UnitTests/Sources/AudioPlayerStateTests.swift index 7761de3a2c..469dca9bf8 100644 --- a/UnitTests/Sources/AudioPlayerStateTests.swift +++ b/UnitTests/Sources/AudioPlayerStateTests.swift @@ -38,7 +38,7 @@ class AudioPlayerStateTests: XCTestCase { override func setUp() async throws { audioPlayerActionsSubject = .init() audioPlayerSeekCallsSubject = .init() - audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: Self.audioDuration) + audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: Self.audioDuration) audioPlayerMock = buildAudioPlayerMock() audioPlayerMock.seekToClosure = { [weak self] progress in self?.audioPlayerMock.currentTime = Self.audioDuration * progress @@ -162,7 +162,7 @@ class AudioPlayerStateTests: XCTestCase { func testHandlingAudioPlayerActionDidFinishLoading() async throws { audioPlayerMock.duration = 10.0 - audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0) + audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0) audioPlayerState.attachAudioPlayer(audioPlayerMock) let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in diff --git a/UnitTests/Sources/AuthenticationServiceTests.swift b/UnitTests/Sources/AuthenticationServiceTests.swift new file mode 100644 index 0000000000..e512db7318 --- /dev/null +++ b/UnitTests/Sources/AuthenticationServiceTests.swift @@ -0,0 +1,96 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +class AuthenticationServiceTests: XCTestCase { + var client: ClientSDKMock! + var userSessionStore: UserSessionStoreMock! + var encryptionKeyProvider: MockEncryptionKeyProvider! + + var service: AuthenticationService! + + func testLogin() async { + setupMocks() + + switch await service.configure(for: "matrix.org", flow: .login) { + case .success: + break + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + + XCTAssertEqual(service.flow, .login) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + + switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) { + case .success: + XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1) + XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1) + XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase, + encryptionKeyProvider.generateKey().base64EncodedString()) + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + } + + func testConfigureRegister() async { + setupMocks() + + switch await service.configure(for: "matrix.org", flow: .register) { + case .success: + break + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + + XCTAssertEqual(service.flow, .register) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + } + + func testConfigureRegisterNoSupport() async { + let homeserverAddress = "example.com" + setupMocks(serverAddress: homeserverAddress) + + switch await service.configure(for: homeserverAddress, flow: .register) { + case .success: + XCTFail("Configuration should have failed") + case .failure(let error): + XCTAssertEqual(error, .registrationNotSupported) + } + + XCTAssertEqual(service.flow, .login) + XCTAssertEqual(service.homeserver.value, .init(address: "matrix.org", loginMode: .unknown)) + } + + // MARK: - Helpers + + private func setupMocks(serverAddress: String = "matrix.org") { + let configuration: AuthenticationClientBuilderMock.Configuration = .init() + let clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration)) + + client = configuration.homeserverClients[serverAddress] + userSessionStore = UserSessionStoreMock(configuration: .init()) + encryptionKeyProvider = MockEncryptionKeyProvider() + + service = AuthenticationService(userSessionStore: userSessionStore, + encryptionKeyProvider: encryptionKeyProvider, + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + } +} + +struct MockEncryptionKeyProvider: EncryptionKeyProviderProtocol { + private let key = "12345678" + + func generateKey() -> Data { + Data(key.utf8) + } +} diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift index c79e725f32..0852dede1e 100644 --- a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -17,7 +17,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase { let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } @@ -32,7 +32,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase { let viewModel = BlockedUsersScreenViewModel(hideProfiles: false, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains(where: { $0.displayName != nil }) } diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 162ac0d97f..9779ae8bf0 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -28,7 +28,7 @@ class ComposerToolbarViewModelTests: XCTestCase { draftServiceMock = ComposerDraftServiceMock() viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: completionSuggestionServiceMock, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: draftServiceMock) @@ -41,14 +41,14 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerFocus() { - viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock")))) XCTAssertTrue(viewModel.state.bindings.composerFocused) viewModel.process(timelineAction: .removeFocus) XCTAssertFalse(viewModel.state.bindings.composerFocused) } func testComposerMode() { - let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) viewModel.process(timelineAction: .setMode(mode: mode)) XCTAssertEqual(viewModel.state.composerMode, mode) viewModel.process(timelineAction: .clear) @@ -56,7 +56,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerModeIsPublished() { - let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) let expectation = expectation(description: "Composer mode is published") let cancellable = viewModel .context @@ -105,7 +105,7 @@ class ComposerToolbarViewModelTests: XCTestCase { let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)) viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: mockCompletionSuggestionService, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mentionDisplayHelper: ComposerMentionDisplayHelper.mock, analyticsService: ServiceLocator.shared.analytics, composerDraftService: draftServiceMock) @@ -113,11 +113,11 @@ class ComposerToolbarViewModelTests: XCTestCase { XCTAssertEqual(viewModel.state.suggestions, suggestions) } - func testSuggestionTrigger() async { + func testSuggestionTrigger() async throws { + let deferred = deferFulfillment(wysiwygViewModel.$attributedContent) { $0.plainText == "#not_implemented_yay" } wysiwygViewModel.setMarkdownContent("@test") wysiwygViewModel.setMarkdownContent("#not_implemented_yay") - - await Task.yield() + try await deferred.fulfill() // The first one is nil because when initialised the view model is empty XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil, .init(type: .user, text: "test", range: .init(location: 0, length: 5)), nil]) @@ -236,7 +236,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .init(timelineID: "", eventID: "testID")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID")))) viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.saveDraft() @@ -257,12 +257,11 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(timelineID: "", - eventID: "testID"), - replyDetails: .loaded(sender: .init(id: ""), - eventID: "testID", - eventContent: .message(.text(.init(body: "reply text")))), - isThread: false))) + viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID", + replyDetails: .loaded(sender: .init(id: ""), + eventID: "testID", + eventContent: .message(.text(.init(body: "reply text")))), + isThread: false))) viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.saveDraft() @@ -283,12 +282,11 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(timelineID: "", - eventID: "testID"), - replyDetails: .loaded(sender: .init(id: ""), - eventID: "testID", - eventContent: .message(.text(.init(body: "reply text")))), - isThread: false))) + viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID", + replyDetails: .loaded(sender: .init(id: ""), + eventID: "testID", + eventContent: .message(.text(.init(body: "reply text")))), + isThread: false))) viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) @@ -397,7 +395,7 @@ class ComposerToolbarViewModelTests: XCTestCase { await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) - XCTAssertEqual(viewModel.state.composerMode, .edit(originalItemId: .init(timelineID: "", eventID: "testID"))) + XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"))) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!")) } @@ -431,13 +429,13 @@ class ComposerToolbarViewModelTests: XCTestCase { await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) // Testing the loading state first - XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(timelineID: "", eventID: testEventID), + XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID, replyDetails: .loading(eventID: testEventID), isThread: false)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text)) await fulfillment(of: [loadReplyExpectation], timeout: 10) - XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(timelineID: "", eventID: testEventID), + XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID, replyDetails: loadedReply, isThread: true)) } @@ -445,8 +443,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testRestoreReplyAndCancelReplyMode() async { let testEventID = "testID" let text = "Hello world!" - let loadedReply = TimelineItemReplyDetails.loaded(sender: .init(id: "userID", - displayName: "Username"), + let loadedReply = TimelineItemReplyDetails.loaded(sender: .init(id: "userID", displayName: "Username"), eventID: testEventID, eventContent: .message(.text(.init(body: "Reply text")))) @@ -472,7 +469,7 @@ class ComposerToolbarViewModelTests: XCTestCase { await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) // Testing the loading state first - XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(timelineID: "", eventID: testEventID), + XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID, replyDetails: .loading(eventID: testEventID), isThread: false)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text)) @@ -486,7 +483,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testSaveVolatileDraftWhenEditing() { viewModel.context.composerFormattingEnabled = false viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .random))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString)))) let draft = draftServiceMock.saveVolatileDraftReceivedDraft XCTAssertNotNil(draft) diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 2dc1940e20..b145dfbfd9 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -29,11 +29,13 @@ class CreateRoomScreenViewModelTests: XCTestCase { userSession = UserSessionMock(.init(clientProxy: clientProxy)) let parameters = CreateRoomFlowParameters() usersSubject.send([.mockAlice, .mockBob, .mockCharlie]) + ServiceLocator.shared.settings.knockingEnabled = true let viewModel = CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), selectedUsers: usersSubject.asCurrentValuePublisher(), analytics: ServiceLocator.shared.analytics, - userIndicatorController: UserIndicatorControllerMock()) + userIndicatorController: UserIndicatorControllerMock(), + appSettings: ServiceLocator.shared.settings) self.viewModel = viewModel viewModel.actions.sink { [weak self] action in @@ -69,4 +71,38 @@ class CreateRoomScreenViewModelTests: XCTestCase { context.roomName = "A" XCTAssertTrue(context.viewState.canCreateRoom) } + + func testCreateKnockingRoom() async { + context.roomName = "A" + context.roomTopic = "B" + context.isRoomPrivate = false + context.isKnockingOnly = true + XCTAssertTrue(context.viewState.canCreateRoom) + + let expectation = expectation(description: "Wait for the room to be created") + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in + XCTAssertTrue(isKnockingOnly) + XCTAssertFalse(isPrivate) + defer { expectation.fulfill() } + return .success("") + } + context.send(viewAction: .createRoom) + await fulfillment(of: [expectation]) + } + + func testCreatePrivateRoomCantHaveKnockRule() async { + context.roomName = "A" + context.roomTopic = "B" + context.isRoomPrivate = true + context.isKnockingOnly = true + context.send(viewAction: .createRoom) + let expectation = expectation(description: "Wait for the room to be created") + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in + XCTAssertFalse(isKnockingOnly) + XCTAssertTrue(isPrivate) + expectation.fulfill() + return .success("") + } + await fulfillment(of: [expectation]) + } } diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift index 4733122b8c..799a67ee2a 100644 --- a/UnitTests/Sources/EmojiProviderTests.swift +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -18,7 +18,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) let categories = await emojiProvider.categories() XCTAssertEqual(emojiLoaderMock.categories, categories) @@ -31,7 +31,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) let categories = await emojiProvider.categories(searchString: "") XCTAssertEqual(emojiLoaderMock.categories, categories) @@ -48,7 +48,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categoriesForFirstLoad - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) _ = await emojiProvider.categories() emojiLoaderMock.categories = categoriesForSecondLoad @@ -78,7 +78,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categories - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) _ = await emojiProvider.categories() let result = await emojiProvider.categories(searchString: searchString) diff --git a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift b/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift index 40c72f959d..7c6d4bcd28 100644 --- a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift +++ b/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift @@ -19,7 +19,7 @@ class GlobalSearchScreenViewModelTests: XCTestCase { override func setUpWithError() throws { cancellables.removeAll() viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) context = viewModel.context } diff --git a/UnitTests/Sources/HomeScreenRoomTests.swift b/UnitTests/Sources/HomeScreenRoomTests.swift index bb47ffc0ce..1cbe828d23 100644 --- a/UnitTests/Sources/HomeScreenRoomTests.swift +++ b/UnitTests/Sources/HomeScreenRoomTests.swift @@ -23,8 +23,7 @@ class HomeScreenRoomTests: XCTestCase { hasOngoingCall: Bool) { roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()), id: "Test room", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: "Test room", isDirect: false, avatarURL: nil, diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index 5e3c463681..fb79524cac 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -92,7 +92,7 @@ class InviteUsersScreenViewModelTests: XCTestCase { let viewModel = InviteUsersScreenViewModel(clientProxy: ClientProxyMock(.init()), selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock()) viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie]) diff --git a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift index ea2ef31a48..2f8b6e0521 100644 --- a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift @@ -16,6 +16,11 @@ class JoinRoomScreenViewModelTests: XCTestCase { var context: JoinRoomScreenViewModelType.Context { viewModel.context } + + override func tearDown() { + viewModel = nil + AppSettings.resetAllSettings() + } func testInteraction() async throws { setupViewModel() @@ -43,7 +48,32 @@ class JoinRoomScreenViewModelTests: XCTestCase { XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInvite) } - private func setupViewModel(throwing: Bool = false) { + func testKnockedState() async throws { + setupViewModel(knocked: true) + + try await deferFulfillment(viewModel.context.$viewState) { state in + state.mode == .knocked + }.fulfill() + } + + func testCancelKnock() async throws { + setupViewModel(knocked: true) + + try await deferFulfillment(viewModel.context.$viewState) { state in + state.mode == .knocked + }.fulfill() + + context.send(viewAction: .cancelKnock) + XCTAssertEqual(viewModel.context.alertInfo?.id, .cancelKnock) + + let deferred = deferFulfillment(viewModel.actionsPublisher) { action in + action == .dismiss + } + context.alertInfo?.secondaryButton?.action?() + try await deferred.fulfill() + } + + private func setupViewModel(throwing: Bool = false, knocked: Bool = false) { let clientProxy = ClientProxyMock(.init()) clientProxy.joinRoomViaReturnValue = throwing ? .failure(.sdkError(ClientProxyMockError.generic)) : .success(()) @@ -60,10 +90,22 @@ class JoinRoomScreenViewModelTests: XCTestCase { isPublic: false, canKnock: false)) + if knocked { + clientProxy.roomForIdentifierClosure = { _ in + let roomProxy = KnockedRoomProxyMock(.init()) + // to test the cancel knock function + roomProxy.cancelKnockUnderlyingReturnValue = .success(()) + return .knocked(roomProxy) + } + } + + ServiceLocator.shared.settings.knockingEnabled = true + viewModel = JoinRoomScreenViewModel(roomID: "1", via: [], + appSettings: ServiceLocator.shared.settings, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 56d34ed35c..3ab0e77fdd 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -22,7 +22,7 @@ class LoggingTests: XCTestCase { let target = "tests" XCTAssertTrue(RustTracing.logFiles.isEmpty) - MXLog.configure(target: target, logLevel: .info) + MXLog.configure(currentTarget: target, filePrefix: target, logLevel: .info) // There is something weird with Rust logging where the file writing handle doesn't // notice that the file it is writing to was deleted, so we can't run these checks @@ -80,8 +80,7 @@ class LoggingTests: XCTestCase { let heroName = "Pseudonym" let roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()), id: "myroomid", - isInvite: false, - inviter: nil, + joinRequestType: nil, name: roomName, isDirect: true, avatarURL: nil, @@ -116,7 +115,7 @@ class LoggingTests: XCTestCase { func validateTimelineContentIsRedacted() throws { // Given timeline items that contain text let textAttributedString = "TextAttributed" - let textMessage = TextRoomTimelineItem(id: .random, + let textMessage = TextRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, @@ -125,7 +124,7 @@ class LoggingTests: XCTestCase { sender: .init(id: "sender"), content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString))) let noticeAttributedString = "NoticeAttributed" - let noticeMessage = NoticeRoomTimelineItem(id: .random, + let noticeMessage = NoticeRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, @@ -134,7 +133,7 @@ class LoggingTests: XCTestCase { sender: .init(id: "sender"), content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString))) let emoteAttributedString = "EmoteAttributed" - let emoteMessage = EmoteRoomTimelineItem(id: .random, + let emoteMessage = EmoteRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, @@ -142,33 +141,44 @@ class LoggingTests: XCTestCase { isThreaded: false, sender: .init(id: "sender"), content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString))) - let imageMessage = ImageRoomTimelineItem(id: .init(timelineID: "myimagemessage"), + let imageMessage = ImageRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "sender"), - content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil)) - let videoMessage = VideoRoomTimelineItem(id: .random, + content: .init(filename: "ImageString", + caption: "ImageString", + source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), + thumbnailSource: nil)) + let videoMessage = VideoRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "sender"), - content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil)) - let fileMessage = FileRoomTimelineItem(id: .random, + content: .init(filename: "VideoString", + caption: "VideoString", + duration: 0, + source: nil, + thumbnailSource: nil)) + let fileMessage = FileRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, isEditable: false, canBeRepliedTo: true, isThreaded: false, sender: .init(id: "sender"), - content: .init(body: "FileString", source: nil, thumbnailSource: nil, contentType: nil)) + content: .init(filename: "FileString", + caption: "FileString", + source: nil, + thumbnailSource: nil, + contentType: nil)) // When logging that value - MXLog.configure(logLevel: .info) + MXLog.configure(currentTarget: "tests", filePrefix: nil, logLevel: .info) MXLog.info(textMessage) MXLog.info(noticeMessage) @@ -184,25 +194,25 @@ class LoggingTests: XCTestCase { } let content = try String(contentsOf: logFile) - XCTAssertTrue(content.contains(textMessage.id.timelineID)) + XCTAssertTrue(content.contains(textMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(textMessage.body)) XCTAssertFalse(content.contains(textAttributedString)) - XCTAssertTrue(content.contains(noticeMessage.id.timelineID)) + XCTAssertTrue(content.contains(noticeMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(noticeMessage.body)) XCTAssertFalse(content.contains(noticeAttributedString)) - XCTAssertTrue(content.contains(emoteMessage.id.timelineID)) + XCTAssertTrue(content.contains(emoteMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(emoteMessage.body)) XCTAssertFalse(content.contains(emoteAttributedString)) - XCTAssertTrue(content.contains(imageMessage.id.timelineID)) + XCTAssertTrue(content.contains(imageMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(imageMessage.body)) - XCTAssertTrue(content.contains(videoMessage.id.timelineID)) + XCTAssertTrue(content.contains(videoMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(videoMessage.body)) - XCTAssertTrue(content.contains(fileMessage.id.timelineID)) + XCTAssertTrue(content.contains(fileMessage.id.uniqueID.id)) XCTAssertFalse(content.contains(fileMessage.body)) } @@ -218,9 +228,32 @@ class LoggingTests: XCTestCase { let rustEmoteMessage = EmoteMessageContent(body: emoteString, formatted: FormattedBody(format: .html, body: "\(emoteString)")) - let rustImageMessage = ImageMessageContent(body: "ImageString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustVideoMessage = VideoMessageContent(body: "VideoString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustFileMessage = FileMessageContent(body: "FileString", formatted: nil, filename: "FileName", source: MediaSource(noPointer: .init()), info: nil) + let rustImageMessage = ImageMessageContent(body: "ImageString", + formatted: nil, + rawFilename: "ImageString", + filename: "ImageString", + caption: "ImageString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) + + let rustVideoMessage = VideoMessageContent(body: "VideoString", + formatted: nil, + rawFilename: "VideoString", + filename: "VideoString", + caption: "VideoString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) + + let rustFileMessage = FileMessageContent(body: "FileString", + formatted: nil, + rawFilename: "FileString", + filename: "FileString", + caption: "FileString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) // When logging that value MXLog.info(rustTextMessage) diff --git a/UnitTests/Sources/LoginScreenViewModelTests.swift b/UnitTests/Sources/LoginScreenViewModelTests.swift new file mode 100644 index 0000000000..859b0ab064 --- /dev/null +++ b/UnitTests/Sources/LoginScreenViewModelTests.swift @@ -0,0 +1,173 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +@MainActor +class LoginScreenViewModelTests: XCTestCase { + var viewModel: LoginScreenViewModelProtocol! + var context: LoginScreenViewModelType.Context { viewModel.context } + + var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! + var service: AuthenticationServiceProtocol! + + private func setupViewModel(homeserverAddress: String = "matrix.org") async { + clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init()) + service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + + guard case .success = await service.configure(for: homeserverAddress, flow: .login) else { + XCTFail("A valid server should be configured for the test.") + return + } + + viewModel = LoginScreenViewModel(authenticationService: service, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) + } + + func testMatrixDotOrg() async { + // Given the initial view model configured for matrix.org. + await setupViewModel() + + // Then the view state should contain a homeserver that matches matrix.org and show the login form. + XCTAssertEqual(context.viewState.homeserver, .mockMatrixDotOrg, "The homeserver data should match the default homeserver.") + XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") + } + + func testBasicServer() async { + // Given the view model configured for a basic server example.com that only supports password authentication. + await setupViewModel(homeserverAddress: "example.com") + + // Then the view state should be updated with the homeserver and show the login form. + XCTAssertEqual(context.viewState.homeserver, .mockBasicServer, "The homeserver data should should match the new homeserver.") + XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") + } + + func testUsernameWithEmptyPassword() async { + // Given a form with an empty username and password. + await setupViewModel() + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a username without a password. + context.username = "bob" + context.password = "" + + // Then the credentials should be considered invalid. + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + } + + func testEmptyUsernameWithPassword() async { + // Given a form with an empty username and password. + await setupViewModel() + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a password without a username. + context.username = "" + context.password = "12345678" + + // Then the credentials should be considered invalid. + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + } + + func testValidCredentials() async { + // Given a form with an empty username and password. + await setupViewModel() + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") + + // When entering a username and an 8-character password. + context.username = "bob" + context.password = "12345678" + + // Then the credentials should be considered valid. + XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + } + + func testLoadingServerWithoutPassword() async throws { + // Given a form with valid credentials. + await setupViewModel() + context.username = "@bob:example.com" + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be not be valid without a password.") + XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") + XCTAssertFalse(context.viewState.canSubmit, "The form should not be submittable.") + + // When updating the view model whilst loading a homeserver. + let deferred = deferFulfillment(context.$viewState, keyPath: \.isLoading, transitionValues: [true, false]) + context.send(viewAction: .parseUsername) + + // Then the view state should represent the loading but never allow submitting to occur. + try await deferred.fulfill() + XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") + XCTAssertFalse(context.viewState.canSubmit, "The form should still not be submittable.") + } + + func testLoadingServerWithPasswordEntered() async throws { + // Given a form with valid credentials. + await setupViewModel() + context.username = "@bob:example.com" + context.password = "12345678" + XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.") + XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + + // When updating the view model whilst loading a homeserver. + let deferred = deferFulfillment(context.$viewState, keyPath: \.canSubmit, transitionValues: [false, true]) + context.send(viewAction: .parseUsername) + + // Then the view should be blocked from submitting while loading and then become unblocked again. + try await deferred.fulfill() + XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") + XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") + } + + func testOIDCServer() async throws { + // Given the screen configured for matrix.org + await setupViewModel() + + // When entering a username for a user on a homeserver with OIDC. + let deferred = deferFulfillment(viewModel.actions) { $0.isConfiguredForOIDC } + context.username = "@bob:company.com" + context.send(viewAction: .parseUsername) + try await deferred.fulfill() + + // Then the view state should be updated with the homeserver and show the OIDC button. + XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.") + } + + func testUnsupportedServer() async throws { + // Given the screen configured for matrix.org + await setupViewModel() + XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.") + + // When entering a username for an unsupported homeserver. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.username = "@bob:server.net" + context.send(viewAction: .parseUsername) + try await deferred.fulfill() + + // Then the view state should be updated to show an alert. + XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.") + } +} diff --git a/UnitTests/Sources/LoginViewModelTests.swift b/UnitTests/Sources/LoginViewModelTests.swift deleted file mode 100644 index ba02c88a92..0000000000 --- a/UnitTests/Sources/LoginViewModelTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import XCTest - -@testable import ElementX - -@MainActor -class LoginViewModelTests: XCTestCase { - let defaultHomeserver = LoginHomeserver.mockMatrixDotOrg - var viewModel: LoginScreenViewModelProtocol! - var context: LoginScreenViewModelType.Context! - - @MainActor override func setUp() async throws { - viewModel = LoginScreenViewModel(homeserver: defaultHomeserver, slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL) - context = viewModel.context - } - - func testMatrixDotOrg() { - // Given the initial view model configured for matrix.org. - let homeserver = defaultHomeserver - - // Then the view state should contain a homeserver that matches matrix.org and show the login form. - XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should match the original.") - XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") - } - - func testBasicServer() { - // Given a basic server example.com that only supports password registration. - let homeserver = LoginHomeserver.mockBasicServer - - // When updating the view model with the server. - viewModel.update(homeserver: homeserver) - - // Then the view state should be updated with the homeserver and show the login form. - XCTAssertEqual(context.viewState.homeserver, homeserver, "The homeserver data should should match the new homeserver.") - XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.") - } - - func testUsernameWithEmptyPassword() { - // Given a form with an empty username and password. - XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") - XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - - // When entering a username without a password. - context.username = "bob" - context.password = "" - - // Then the credentials should be considered invalid. - XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - } - - func testEmptyUsernameWithPassword() { - // Given a form with an empty username and password. - XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") - XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - - // When entering a password without a username. - context.username = "" - context.password = "12345678" - - // Then the credentials should be considered invalid. - XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - } - - func testValidCredentials() { - // Given a form with an empty username and password. - XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") - XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") - XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - - // When entering a username and an 8-character password. - context.username = "bob" - context.password = "12345678" - - // Then the credentials should be considered valid. - XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") - XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") - } - - func testLoadingServer() { - // Given a form with valid credentials. - context.username = "bob" - context.password = "12345678" - XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.") - XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.") - XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") - - // When updating the view model whilst loading a homeserver. - viewModel.update(isLoading: true) - - // Then the view state should reflect that the homeserver is loading. - XCTAssertTrue(context.viewState.isLoading, "The view should now be in a loading state.") - XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.") - - // When updating the view model after loading a homeserver. - viewModel.update(isLoading: false) - - // Then the view state should reflect that the homeserver is now loaded. - XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.") - XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.") - } - - func testOIDCServer() { - // Given a basic server example.com that supports OIDC registration. - let homeserver = LoginHomeserver.mockOIDC - - // When updating the view model with the server. - viewModel.update(homeserver: homeserver) - - // Then the view state should be updated with the homeserver and show the OIDC button. - XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.") - } - - func testLogsForPassword() { - // Given the coordinator and view model results that contain passwords. - let password = "supersecretpassword" - let viewModelAction: LoginScreenViewModelAction = .login(username: "Alice", password: password) - - // When creating a string representation of those results (e.g. for logging). - let viewModelActionString = "\(viewModelAction)" - - // Then the password should not be included in that string. - XCTAssertFalse("\(viewModelActionString)".contains(password), "The password must not be included in any strings.") - } -} diff --git a/UnitTests/Sources/MediaPlayerProviderTests.swift b/UnitTests/Sources/MediaPlayerProviderTests.swift index a9fcca9a6a..92ef5997d5 100644 --- a/UnitTests/Sources/MediaPlayerProviderTests.swift +++ b/UnitTests/Sources/MediaPlayerProviderTests.swift @@ -60,7 +60,7 @@ class MediaPlayerProviderTests: XCTestCase { } func testPlayerStates() async throws { - let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.random) + let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent) // By default, there should be no player state XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId)) @@ -76,7 +76,7 @@ class MediaPlayerProviderTests: XCTestCase { let audioPlayer = AudioPlayerMock() audioPlayer.actions = PassthroughSubject().eraseToAnyPublisher() - let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0), count: 10) + let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0), count: 10) for audioPlayerState in audioPlayerStates { mediaPlayerProvider.register(audioPlayerState: audioPlayerState) audioPlayerState.attachAudioPlayer(audioPlayer) @@ -95,7 +95,7 @@ class MediaPlayerProviderTests: XCTestCase { let audioPlayer = AudioPlayerMock() audioPlayer.actions = PassthroughSubject().eraseToAnyPublisher() - let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0), count: 10) + let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0), count: 10) for audioPlayerState in audioPlayerStates { mediaPlayerProvider.register(audioPlayerState: audioPlayerState) audioPlayerState.attachAudioPlayer(audioPlayer) diff --git a/UnitTests/Sources/MediaProvider/MockImageCache.swift b/UnitTests/Sources/MediaProvider/MockImageCache.swift index 4e49317e65..64c41bde5e 100644 --- a/UnitTests/Sources/MediaProvider/MockImageCache.swift +++ b/UnitTests/Sources/MediaProvider/MockImageCache.swift @@ -5,7 +5,7 @@ // Please see LICENSE in the repository root for full details. // @testable import ElementX -import Kingfisher +@testable import Kingfisher import UIKit class MockImageCache: ImageCache { @@ -35,5 +35,6 @@ class MockImageCache: ImageCache { callbackQueue: CallbackQueue = .untouch, completionHandler: ((CacheStoreResult) -> Void)? = nil) { storedImages[key] = image + completionHandler?(.init(memoryCacheResult: .success(()), diskCacheResult: .success(()))) } } diff --git a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift index e33bc64b7a..93ae732697 100644 --- a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift +++ b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift @@ -5,12 +5,26 @@ // Please see LICENSE in the repository root for full details. // +import UniformTypeIdentifiers import XCTest @testable import ElementX final class MediaUploadingPreprocessorTests: XCTestCase { - let mediaUploadingPreprocessor = MediaUploadingPreprocessor() + var appSettings: AppSettings! + var mediaUploadingPreprocessor: MediaUploadingPreprocessor! + + override func setUp() { + AppSettings.resetAllSettings() + appSettings = AppSettings() + appSettings.optimizeMediaUploads = false + ServiceLocator.shared.register(appSettings: appSettings) + mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings) + } + + override func tearDown() { + AppSettings.resetAllSettings() + } func testAudioFileProcessing() async { guard let url = Bundle(for: Self.self).url(forResource: "test_audio.mp3", withExtension: nil) else { @@ -33,6 +47,9 @@ final class MediaUploadingPreprocessorTests: XCTestCase { } func testLandscapeMovVideoProcessing() async { + // Allow an increased execution time as we encode the video twice now. + executionTimeAllowance = 180 + guard let url = Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil) else { XCTFail("Failed retrieving test asset") return @@ -46,6 +63,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase { // Check that the file name is preserved XCTAssertEqual(videoURL.lastPathComponent, "landscape_test_video.mp4") + XCTAssertEqual(videoURL.pathExtension, "mp4", "The file extension should match the container we use.") // Check that the thumbnail is generated correctly guard let thumbnailData = try? Data(contentsOf: thumbnailURL), @@ -67,12 +85,34 @@ final class MediaUploadingPreprocessorTests: XCTestCase { XCTAssertNotNil(videoInfo.thumbnailInfo) XCTAssertEqual(videoInfo.thumbnailInfo?.mimetype, "image/jpeg") - XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 34206, accuracy: 100) + XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 33611, accuracy: 100) XCTAssertEqual(videoInfo.thumbnailInfo?.width, 800) XCTAssertEqual(videoInfo.thumbnailInfo?.height, 450) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + XCTAssertEqual(optimizedVideoURL.pathExtension, "mp4", "The file extension should match the container we use.") + + // Check optimised video info + XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4") + XCTAssertEqual(optimizedVideoInfo.blurhash, "K22PJZx^DgadWAbbMuRio$") + XCTAssertEqual(optimizedVideoInfo.size ?? 0, 1_431_959, accuracy: 100) // Note: This is slightly stupid because it is larger now 🤦‍♂️ + XCTAssertEqual(optimizedVideoInfo.width, 1280) + XCTAssertEqual(optimizedVideoInfo.height, 720) + XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100) } func testPortraitMp4VideoProcessing() async { + // Allow an increased execution time as we encode the video twice now. + executionTimeAllowance = 180 + guard let url = Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil) else { XCTFail("Failed retrieving test asset") return @@ -86,6 +126,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase { // Check that the file name is preserved XCTAssertEqual(videoURL.lastPathComponent, "portrait_test_video.mp4") + XCTAssertEqual(videoURL.pathExtension, "mp4", "The file extension should match the container we use.") // Check that the thumbnail is generated correctly guard let thumbnailData = try? Data(contentsOf: thumbnailURL), @@ -107,9 +148,28 @@ final class MediaUploadingPreprocessorTests: XCTestCase { XCTAssertNotNil(videoInfo.thumbnailInfo) XCTAssertEqual(videoInfo.thumbnailInfo?.mimetype, "image/jpeg") - XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 83220, accuracy: 100) + XCTAssertEqual(videoInfo.thumbnailInfo?.size ?? 0, 81515, accuracy: 100) XCTAssertEqual(videoInfo.thumbnailInfo?.width, 337) XCTAssertEqual(videoInfo.thumbnailInfo?.height, 600) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + XCTAssertEqual(optimizedVideoURL.pathExtension, "mp4", "The file extension should match the container we use.") + + // Check optimised video info + XCTAssertEqual(optimizedVideoInfo.mimetype, "video/mp4") + XCTAssertEqual(optimizedVideoInfo.blurhash, "K7BDNJD*0L%#sl_2~C9ZE1") + XCTAssertEqual(optimizedVideoInfo.size ?? 0, 21_936_767, accuracy: 100) + XCTAssertEqual(optimizedVideoInfo.width, 720) + XCTAssertEqual(optimizedVideoInfo.height, 1280) + XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100) } func testLandscapeImageProcessing() async { @@ -135,9 +195,27 @@ final class MediaUploadingPreprocessorTests: XCTestCase { XCTAssertNotNil(imageInfo.thumbnailInfo) XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg") - XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 89553, accuracy: 100) + XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 87733, accuracy: 100) XCTAssertEqual(imageInfo.thumbnailInfo?.width, 800) XCTAssertEqual(imageInfo.thumbnailInfo?.height, 344) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL) + + // Check optimised image info + XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg") + XCTAssertEqual(optimizedImageInfo.blurhash, "K%I#.NofkC_4ayaxxujsWB") + XCTAssertEqual(optimizedImageInfo.size ?? 0, 524_226, accuracy: 100) + XCTAssertEqual(optimizedImageInfo.width, 2048) + XCTAssertEqual(optimizedImageInfo.height, 879) } func testPortraitImageProcessing() async { @@ -156,57 +234,261 @@ final class MediaUploadingPreprocessorTests: XCTestCase { // Check resulting image info XCTAssertEqual(imageInfo.mimetype, "image/jpeg") - XCTAssertEqual(imageInfo.blurhash, "KdE:ets+RP^-n*RP%OWAV@") + XCTAssertEqual(imageInfo.blurhash, "KdE|0Ls+RP^-n*RP%OWAV@") XCTAssertEqual(imageInfo.size ?? 0, 4_414_666, accuracy: 100) XCTAssertEqual(imageInfo.width, 3024) XCTAssertEqual(imageInfo.height, 4032) XCTAssertNotNil(imageInfo.thumbnailInfo) XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg") - XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 264_500, accuracy: 100) + XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 258_914, accuracy: 100) XCTAssertEqual(imageInfo.thumbnailInfo?.width, 600) XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL) + + // Check optimised image info + XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg") + XCTAssertEqual(optimizedImageInfo.blurhash, "KdE|0Ls+RP^-n*RP%OWAV@") + XCTAssertEqual(optimizedImageInfo.size ?? 0, 1_462_937, accuracy: 100) + XCTAssertEqual(optimizedImageInfo.width, 1536) + XCTAssertEqual(optimizedImageInfo.height, 2048) } - // MARK: - Private + func testPNGImageProcessing() async { + guard let url = Bundle(for: Self.self).url(forResource: "test_image.png", withExtension: nil) else { + XCTFail("Failed retrieving test asset") + return + } + + guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(convertedImageURL, _, imageInfo) = result else { + XCTFail("Failed processing asset") + return + } + + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: convertedImageURL), "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.") + XCTAssertEqual(convertedImageURL.pathExtension, "png", "The file extension should match the MIME type.") + + // Check resulting image info + XCTAssertEqual(imageInfo.mimetype, "image/png") + XCTAssertEqual(imageInfo.blurhash, "K0TSUA~qfQ~qj[fQfQfQfQ") + XCTAssertEqual(imageInfo.size ?? 0, 4868, accuracy: 100) + XCTAssertEqual(imageInfo.width, 240) + XCTAssertEqual(imageInfo.height, 240) + + XCTAssertNotNil(imageInfo.thumbnailInfo) + XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg") + XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 1725, accuracy: 100) + XCTAssertEqual(imageInfo.thumbnailInfo?.width, 240) + XCTAssertEqual(imageInfo.thumbnailInfo?.height, 240) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: optimizedImageURL), "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.") + XCTAssertEqual(optimizedImageURL.pathExtension, "png", "The file extension should match the MIME type.") + + // Check optimised image info + XCTAssertEqual(optimizedImageInfo.mimetype, "image/png") + XCTAssertEqual(optimizedImageInfo.blurhash, "K0TSUA~qfQ~qj[fQfQfQfQ") + XCTAssertEqual(optimizedImageInfo.size ?? 0, 8199, accuracy: 100) + // Assert that resizing didn't upscale to the maxPixelSize. + XCTAssertEqual(optimizedImageInfo.width, 240) + XCTAssertEqual(optimizedImageInfo.height, 240) + } - private func compare(originalImageAt originalImageURL: URL, toConvertedImageAt convertedImageURL: URL, withThumbnailAt thumbnailURL: URL) { - guard let originalImageData = try? Data(contentsOf: originalImageURL), - let originalImage = UIImage(data: originalImageData), - let convertedImageData = try? Data(contentsOf: convertedImageURL), - let convertedImage = UIImage(data: convertedImageData) else { - fatalError() + func testHEICImageProcessing() async { + guard let url = Bundle(for: Self.self).url(forResource: "test_apple_image.heic", withExtension: nil) else { + XCTFail("Failed retrieving test asset") + return } - // Check that the file name is preserved - XCTAssertEqual(originalImageURL.lastPathComponent, convertedImageURL.lastPathComponent) + guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else { + XCTFail("Failed processing asset") + return + } - // Check that new image is the same size as the original one - XCTAssertEqual(originalImage.size, convertedImage.size) + compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL) - // Check that the GPS data has been stripped - guard let imageSource = CGImageSourceCreateWithData(originalImageData as NSData, nil) else { - XCTFail("Invalid test asset") + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: convertedImageURL), "image/heic", "Unoptimised HEICs should always be sent as is.") + XCTAssertEqual(convertedImageURL.pathExtension, "heic", "The file extension should match the MIME type.") + + // Check resulting image info + XCTAssertEqual(imageInfo.mimetype, "image/heic") + XCTAssertEqual(imageInfo.blurhash, "KGD]3ns:T00$kWxFXmt6xv") + XCTAssertEqual(imageInfo.size ?? 0, 1_857_833, accuracy: 100) + XCTAssertEqual(imageInfo.width, 3024) + XCTAssertEqual(imageInfo.height, 4032) + + XCTAssertNotNil(imageInfo.thumbnailInfo) + XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg") + XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 218_108, accuracy: 100) + XCTAssertEqual(imageInfo.thumbnailInfo?.width, 600) + XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") return } - guard let originalMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else { - XCTFail("Test asset is expected to contain metadata") + compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL) + + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: optimizedImageURL), "image/jpeg", "Optimised HEICs should always be converted to JPEG for compatibility.") + XCTAssertEqual(optimizedImageURL.pathExtension, "jpeg", "The file extension should match the MIME type.") + + // Check optimised image info + XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg") + XCTAssertEqual(optimizedImageInfo.blurhash, "KGD]3ns:T00#kWxFb^s:xv") + XCTAssertEqual(optimizedImageInfo.size ?? 0, 1_049_393, accuracy: 100) + XCTAssertEqual(optimizedImageInfo.width, 1536) + XCTAssertEqual(optimizedImageInfo.height, 2048) + } + + func testGIFImageProcessing() async { + guard let url = Bundle(for: Self.self).url(forResource: "test_animated_image.gif", withExtension: nil) else { + XCTFail("Failed retrieving test asset") + return + } + guard let originalSize = try? FileManager.default.sizeForItem(at: url), originalSize > 0 else { + XCTFail("Failed fetching test asset's original size") return } - XCTAssertNotNil(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)")) + guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(convertedImageURL, _, imageInfo) = result else { + XCTFail("Failed processing asset") + return + } + + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: convertedImageURL), "image/gif", "GIFs should always be sent as GIF to preserve the animation.") + XCTAssertEqual(convertedImageURL.pathExtension, "gif", "The file extension should match the MIME type.") + + // Check resulting image info + XCTAssertEqual(imageInfo.mimetype, "image/gif") + XCTAssertEqual(imageInfo.blurhash, "K7SY{qs;%NxuRjof~qozIU") + XCTAssertEqual(imageInfo.size ?? 0, UInt64(originalSize), accuracy: 100) + XCTAssertEqual(imageInfo.width, 490) + XCTAssertEqual(imageInfo.height, 498) - guard let convertedImageSource = CGImageSourceCreateWithData(convertedImageData as NSData, nil) else { - XCTFail("Invalid converted asset") + XCTAssertNotNil(imageInfo.thumbnailInfo) + XCTAssertEqual(imageInfo.thumbnailInfo?.mimetype, "image/jpeg") + XCTAssertEqual(imageInfo.thumbnailInfo?.size ?? 0, 29511, accuracy: 100) + XCTAssertEqual(imageInfo.thumbnailInfo?.width, 490) + XCTAssertEqual(imageInfo.thumbnailInfo?.height, 498) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") return } - guard let convertedMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(convertedImageSource, 0, nil) else { - XCTFail("Test asset is expected to contain metadata") + // Make sure the output file matches the image info. + XCTAssertEqual(mimeType(from: optimizedImageURL), "image/gif", "GIFs should always be sent as GIF to preserve the animation.") + XCTAssertEqual(optimizedImageURL.pathExtension, "gif", "The file extension should match the MIME type.") + + // Ensure optimised image is still the same as the original image. + XCTAssertEqual(optimizedImageInfo.mimetype, "image/gif") + XCTAssertEqual(optimizedImageInfo.blurhash, "K7SY{qs;%NxuRjof~qozIU") + XCTAssertEqual(optimizedImageInfo.size ?? 0, UInt64(originalSize), accuracy: 100) + XCTAssertEqual(optimizedImageInfo.width, 490) + XCTAssertEqual(optimizedImageInfo.height, 498) + } + + func testRotatedImageProcessing() async { + guard let url = Bundle(for: Self.self).url(forResource: "test_rotated_image.jpg", withExtension: nil) else { + XCTFail("Failed retrieving test asset") return } + guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else { + XCTFail("Failed processing asset") + return + } + + compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL) + + // Check resulting image info + XCTAssertEqual(imageInfo.mimetype, "image/jpeg") + XCTAssertEqual(imageInfo.width, 2848) + XCTAssertEqual(imageInfo.height, 4272) + + XCTAssertNotNil(imageInfo.thumbnailInfo) + XCTAssertEqual(imageInfo.thumbnailInfo?.width, 533) + XCTAssertEqual(imageInfo.thumbnailInfo?.height, 800) + + // Repeat with optimised media setting + appSettings.optimizeMediaUploads = true + + guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url), + case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else { + XCTFail("Failed processing asset") + return + } + + compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL) + + // Check optimised image info + XCTAssertEqual(optimizedImageInfo.mimetype, "image/jpeg") + XCTAssertEqual(optimizedImageInfo.width, 1365) + XCTAssertEqual(optimizedImageInfo.height, 2048) + } + + // MARK: - Private + + private func compare(originalImageAt originalImageURL: URL, toConvertedImageAt convertedImageURL: URL, withThumbnailAt thumbnailURL: URL) { + guard let originalImageData = try? Data(contentsOf: originalImageURL), + let originalImage = UIImage(data: originalImageData), + let convertedImageData = try? Data(contentsOf: convertedImageURL), + let convertedImage = UIImage(data: convertedImageData) else { + fatalError() + } + + if appSettings.optimizeMediaUploads { + // Check that new image has been scaled within the requirements for an optimised image + XCTAssert(convertedImage.size.width <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize) + XCTAssert(convertedImage.size.height <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize) + } else { + // Check that the file name is preserved + XCTAssertEqual(originalImageURL.lastPathComponent, convertedImageURL.lastPathComponent) + // Check that new image is the same size as the original one + XCTAssertEqual(originalImage.size, convertedImage.size) + } + + // Check that the GPS data has been stripped + let originalMetadata = metadata(from: originalImageData) + XCTAssertNotNil(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)")) + + let convertedMetadata = metadata(from: convertedImageData) XCTAssertNil(convertedMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)")) // Check that the thumbnail is generated correctly @@ -223,5 +505,33 @@ final class MediaUploadingPreprocessorTests: XCTestCase { XCTAssert(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height) XCTAssert(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width) } + + let thumbnailMetadata = metadata(from: thumbnailData) + XCTAssertNil(thumbnailMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)")) + } + + private func metadata(from imageData: Data) -> NSDictionary { + guard let imageSource = CGImageSourceCreateWithData(imageData as NSData, nil) else { + XCTFail("Invalid asset") + return [:] + } + + guard let convertedMetadata: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else { + XCTFail("Test asset is expected to contain metadata") + return [:] + } + + return convertedMetadata + } + + private func mimeType(from url: URL) -> String? { + guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil), + let typeIdentifier = CGImageSourceGetType(imageSource), + let type = UTType(typeIdentifier as String), + let mimeType = type.preferredMIMEType else { + XCTFail("Failed to get mimetype from URL.") + return nil + } + return mimeType } } diff --git a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift index 292d85d1f1..3076a40aa7 100644 --- a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift +++ b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift @@ -12,7 +12,7 @@ import XCTest @MainActor class MessageForwardingScreenViewModelTests: XCTestCase { - let forwardingItem = MessageForwardingItem(id: .init(timelineID: "t1", eventID: "t1"), + let forwardingItem = MessageForwardingItem(id: .event(uniqueID: .init(id: "t1"), eventOrTransactionID: .eventId(eventId: "t1")), roomID: "1", content: .init(noPointer: .init())) var viewModel: MessageForwardingScreenViewModelProtocol! @@ -29,7 +29,7 @@ class MessageForwardingScreenViewModelTests: XCTestCase { clientProxy: clientProxy, roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), userIndicatorController: UserIndicatorControllerMock(), - mediaProvider: MockMediaProvider()) + mediaProvider: MediaProviderMock(configuration: .init())) context = viewModel.context } diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 12b926b481..7f7f72e556 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -19,13 +19,14 @@ class PillContextTests: XCTestCase { proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertFalse(context.viewState.isOwnMention) @@ -47,13 +48,14 @@ class PillContextTests: XCTestCase { proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) @@ -68,13 +70,14 @@ class PillContextTests: XCTestCase { mockController.roomProxy = proxyMock let mock = TimelineViewModel(roomProxy: proxyMock, timelineController: mockController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index 91fc8e4279..f690d00367 100644 --- a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -61,7 +61,7 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase { private func makeViewModel(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, - itemID: .random, + itemID: .randomEvent, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) } diff --git a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift index 427ff39e90..b78bb60c88 100644 --- a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift @@ -196,7 +196,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { roomProxy = JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)) viewModel = RoomChangeRolesScreenViewModel(mode: mode, roomProxy: roomProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: UserIndicatorControllerMock(), analytics: ServiceLocator.shared.analytics) } diff --git a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift index ad79d87fa2..2088df6e3e 100644 --- a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift @@ -111,7 +111,8 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase { private func setupViewModel(roomProxyConfiguration: JoinedRoomProxyMockConfiguration) { userIndicatorController = UserIndicatorControllerMock.default viewModel = .init(roomProxy: JoinedRoomProxyMock(roomProxyConfiguration), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), userIndicatorController: userIndicatorController) } } diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index bd8e09abe1..0908be9190 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -26,7 +26,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, @@ -42,7 +42,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -65,7 +65,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: false, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -89,7 +89,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: false, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -143,7 +143,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -167,7 +167,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -202,7 +202,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -236,7 +236,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -271,7 +271,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -307,7 +307,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { canUserInvite: false)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -325,7 +325,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: true, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -362,7 +362,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -386,7 +386,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -410,7 +410,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { } viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -431,7 +431,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -452,7 +452,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isPublic: false, members: mockedMembers)) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()), @@ -471,7 +471,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(msg: "error") viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), analyticsService: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController, notificationSettingsProxy: notificationSettingsProxyMock, diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index a10fa0f591..18e20f4524 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -298,7 +298,7 @@ class RoomFlowCoordinatorTests: XCTestCase { isChildFlow: asChildFlow, roomTimelineControllerFactory: timelineControllerFactory, navigationStackCoordinator: navigationStackCoordinator, - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index aa79f28b8a..25c8f61014 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -29,7 +29,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -46,7 +46,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -84,7 +84,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -121,7 +121,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -157,7 +157,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -193,7 +193,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -210,7 +210,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID, roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) diff --git a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift index ba4ccfb24e..8dd9f0cc9e 100644 --- a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift @@ -279,7 +279,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { private func setup(with members: [RoomMemberProxyMock]) { roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members)) viewModel = .init(roomProxy: roomProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index a7998b4dce..773c9af2c7 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -6,6 +6,7 @@ // @testable import ElementX +import MatrixRustSDK import Combine import XCTest @@ -23,23 +24,25 @@ class RoomScreenViewModelTests: XCTestCase { } func testPinnedEventsBanner() async throws { - ServiceLocator.shared.settings.pinningEnabled = true + var configuration = JoinedRoomProxyMockConfiguration() let timelineSubject = PassthroughSubject() - let updateSubject = PassthroughSubject() - let roomProxyMock = JoinedRoomProxyMock(.init()) + let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let roomProxyMock = JoinedRoomProxyMock(configuration) // setup a way to inject the mock of the pinned events timeline roomProxyMock.pinnedEventsTimelineClosure = { await timelineSubject.values.first() } // setup the room proxy actions publisher - roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel // check if in the default state is not showing but is indeed loading @@ -54,8 +57,8 @@ class RoomScreenViewModelTests: XCTestCase { deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.pinnedEventsBannerState.count == 2 } - roomProxyMock.underlyingPinnedEventIDs = ["test1", "test2"] - updateSubject.send(.roomInfoUpdate) + configuration.pinnedEventIDs = ["test1", "test2"] + infoSubject.send(.init(roomInfo: RoomInfo(configuration))) try await deferred.fulfill() XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) @@ -67,8 +70,8 @@ class RoomScreenViewModelTests: XCTestCase { let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>() pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher() pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock - pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")), - .event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2"))] + pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2")))] // check if the banner is now in a loaded state and is showing the counter deferred = deferFulfillment(viewModel.context.$viewState) { viewState in @@ -84,9 +87,9 @@ class RoomScreenViewModelTests: XCTestCase { deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.pinnedEventsBannerState.count == 3 } - providerUpdateSubject.send(([.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")), - .event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2")), - .event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test3")), id: "3"))], .initial)) + providerUpdateSubject.send(([.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init(id: "3")))], .initial)) try await deferred.fulfill() XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) @@ -101,24 +104,25 @@ class RoomScreenViewModelTests: XCTestCase { } func testPinnedEventsBannerSelection() async throws { - ServiceLocator.shared.settings.pinningEnabled = true let roomProxyMock = JoinedRoomProxyMock(.init()) // setup a way to inject the mock of the pinned events timeline let pinnedTimelineMock = TimelineProxyMock() let pinnedTimelineProviderMock = RoomTimelineProviderMock() pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() - pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test1")), id: "1")), - .event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test2")), id: "2")), - .event(.init(item: EventTimelineItemSDKMock(configuration: .init(eventID: "test3")), id: "3"))] + pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2"))), + .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init(id: "3")))] roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: "test1", - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel // check if the banner is now in a loaded state and is showing the counter @@ -154,18 +158,21 @@ class RoomScreenViewModelTests: XCTestCase { } func testRoomInfoUpdate() async throws { - let updateSubject = PassthroughSubject() - let roomProxyMock = JoinedRoomProxyMock(.init(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)) + var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false) + let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let roomProxyMock = JoinedRoomProxyMock(configuration) // setup the room proxy actions publisher roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false) - roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in @@ -176,9 +183,9 @@ class RoomScreenViewModelTests: XCTestCase { } try await deferred.fulfill() - roomProxyMock.name = "NewName" - roomProxyMock.avatar = .room(id: "TestID", name: "NewName", avatarURL: .documentsDirectory) - roomProxyMock.hasOngoingCall = true + configuration.name = "NewName" + configuration.avatarURL = .documentsDirectory + configuration.hasOngoingCall = true roomProxyMock.canUserJoinCallUserIDReturnValue = .success(true) deferred = deferFulfillment(viewModel.context.$viewState) { viewState in @@ -188,7 +195,7 @@ class RoomScreenViewModelTests: XCTestCase { viewState.hasOngoingCall } - updateSubject.send(.roomInfoUpdate) + infoSubject.send(.init(roomInfo: RoomInfo(configuration))) try await deferred.fulfill() } @@ -196,13 +203,15 @@ class RoomScreenViewModelTests: XCTestCase { // Given a room screen with no ongoing call. let ongoingCallRoomIDSubject = CurrentValueSubject(nil) let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID")) - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel XCTAssertTrue(viewModel.state.shouldShowCallButton) diff --git a/UnitTests/Sources/RoomSummaryTests.swift b/UnitTests/Sources/RoomSummaryTests.swift index 55cb93948d..2626351eb4 100644 --- a/UnitTests/Sources/RoomSummaryTests.swift +++ b/UnitTests/Sources/RoomSummaryTests.swift @@ -56,8 +56,7 @@ class RoomSummaryTests: XCTestCase { func makeSummary(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummary { RoomSummary(roomListItem: .init(noPointer: .init()), id: roomDetails.id, - isInvite: false, - inviter: nil, + joinRequestType: nil, name: roomDetails.name, isDirect: isDirect, avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil, diff --git a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift index b859e17414..d6a0428dae 100644 --- a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift +++ b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift @@ -26,18 +26,11 @@ class ServerConfirmationScreenViewStateTests: XCTestCase { func testRegisterMessageString() { let matrixDotOrgRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address, - authenticationFlow: .register, - homeserverSupportsRegistration: true) + authenticationFlow: .register) XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") let oidcRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockOIDC.address, - authenticationFlow: .register, - homeserverSupportsRegistration: true) + authenticationFlow: .register) XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.") - - let otherRegister = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockBasicServer.address, - authenticationFlow: .register, - homeserverSupportsRegistration: false) - XCTAssertEqual(otherRegister.message, L10n.errorAccountCreationNotPossible, "The registration message should always be the same.") } } diff --git a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift index 0d89417d09..4a551cdb64 100644 --- a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift @@ -11,5 +11,141 @@ import XCTest @MainActor class ServerConfirmationScreenViewModelTests: XCTestCase { - // Nothing to test, the view model has no mutable state. + var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! + var service: AuthenticationServiceProtocol! + + var viewModel: ServerConfirmationScreenViewModel! + var context: ServerConfirmationScreenViewModel.Context { viewModel.context } + + func testConfirmLoginWithoutConfiguration() async throws { + // Given a view model for login using a service that hasn't been configured. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then a call to configure service should be made. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + } + + func testConfirmLoginAfterConfiguration() async throws { + // Given a view model for login using a service that has already been configured (via the server selection screen). + setupViewModel(authenticationFlow: .login) + guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .login) else { + XCTFail("The configuration should succeed.") + return + } + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + } + + func testConfirmRegisterWithoutConfiguration() async throws { + // Given a view model for registration using a service that hasn't been configured. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then a call to configure service should be made. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + } + + func testConfirmRegisterAfterConfiguration() async throws { + // Given a view model for registration using a service that has already been configured (via the server selection screen). + setupViewModel(authenticationFlow: .register) + guard case .success = await service.configure(for: viewModel.state.homeserverAddress, flow: .register) else { + XCTFail("The configuration should succeed.") + return + } + XCTAssertNotEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(viewModel.actions) { $0 == .confirm } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + } + + func testRegistrationNotSupportedAlert() async throws { + // Given a view model for registration using a service that hasn't been configured and the default server doesn't support registration. + setupViewModel(authenticationFlow: .register, supportsRegistrationHelper: false) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configured homeserver should be used and no additional call should be made to the service. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .registration) + } + + func testLoginNotSupportedAlert() async throws { + // Given a view model for login using a service that hasn't been configured and the default server doesn't support login. + setupViewModel(authenticationFlow: .login, supportsRegistrationHelper: false, supportsPasswordLogin: false) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When continuing from the confirmation screen. + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the configuration should fail with an alert about not supporting login. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .login) + } + + // MARK: - Helpers + + private func setupViewModel(authenticationFlow: AuthenticationFlow, supportsRegistrationHelper: Bool = true, supportsPasswordLogin: Bool = true) { + // Manually create a configuration as the default homeserver address setting is immutable. + let clientConfiguration: ClientSDKMock.Configuration = if supportsRegistrationHelper { + .init(supportsPasswordLogin: supportsPasswordLogin) + } else { + .init(supportsPasswordLogin: supportsPasswordLogin, elementWellKnown: "") + } + let client = ClientSDKMock(configuration: clientConfiguration) + let configuration = AuthenticationClientBuilderMock.Configuration(homeserverClients: ["matrix.org": client], + qrCodeClient: client) + + clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init(builderConfiguration: configuration)) + service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + + viewModel = ServerConfirmationScreenViewModel(authenticationService: service, + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } } diff --git a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift new file mode 100644 index 0000000000..a4fd808691 --- /dev/null +++ b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift @@ -0,0 +1,141 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +@MainActor +class ServerSelectionScreenViewModelTests: XCTestCase { + var clientBuilderFactory: AuthenticationClientBuilderFactoryMock! + var service: AuthenticationServiceProtocol! + + var viewModel: ServerSelectionScreenViewModelProtocol! + var context: ServerSelectionScreenViewModelType.Context { viewModel.context } + + func testSelectForLogin() async throws { + // Given a view model for login. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When selecting matrix.org. + context.homeserverAddress = "matrix.org" + let deferred = deferFulfillment(viewModel.actions) { $0 == .updated } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should succeed. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + } + + func testLoginNotSupportedAlert() async throws { + // Given a view model for login. + setupViewModel(authenticationFlow: .login) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When selecting a server that doesn't support login. + context.homeserverAddress = "server.net" + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should fail with an alert about not supporting registration. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .loginAlert) + } + + func testSelectForRegistration() async throws { + // Given a view model for registration. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + + // When selecting matrix.org. + context.homeserverAddress = "matrix.org" + let deferred = deferFulfillment(viewModel.actions) { $0 == .updated } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should succeed. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg) + } + + func testRegistrationNotSupportedAlert() async throws { + // Given a view model for registration. + setupViewModel(authenticationFlow: .register) + XCTAssertEqual(service.homeserver.value.loginMode, .unknown) + XCTAssertFalse(service.homeserver.value.supportsRegistration) + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0) + XCTAssertNil(context.alertInfo) + + // When selecting a server that doesn't support registration. + context.homeserverAddress = "example.com" + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then selection should fail with an alert about not supporting registration. + XCTAssertEqual(clientBuilderFactory.makeBuilderSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1) + XCTAssertEqual(context.alertInfo?.id, .registrationAlert) + } + + func testInvalidServer() async throws { + // Given a new instance of the view model. + setupViewModel(authenticationFlow: .login) + XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error message for a new view model.") + XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.") + XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), + "The standard footer message should be shown.") + + // When attempting to discover an invalid server + var deferred = deferFulfillment(context.$viewState) { $0.isShowingFooterError } + context.homeserverAddress = "idontexist" + context.send(viewAction: .confirm) + try await deferred.fulfill() + + // Then the footer should now be showing an error. + XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.") + XCTAssertNotNil(context.viewState.footerErrorMessage, "The error message should be stored.") + XCTAssertNotEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), + "The error message should be shown.") + + // And when clearing the error. + deferred = deferFulfillment(context.$viewState) { !$0.isShowingFooterError } + context.homeserverAddress = "" + context.send(viewAction: .clearFooterError) + try await deferred.fulfill() + + // Then the error message should now be removed. + XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") + XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), + "The standard footer message should be shown again.") + } + + // MARK: - Helpers + + private func setupViewModel(authenticationFlow: AuthenticationFlow) { + clientBuilderFactory = AuthenticationClientBuilderFactoryMock(configuration: .init()) + service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()), + encryptionKeyProvider: EncryptionKeyProvider(), + clientBuilderFactory: clientBuilderFactory, + appSettings: ServiceLocator.shared.settings, + appHooks: AppHooks()) + + viewModel = ServerSelectionScreenViewModel(authenticationService: service, + authenticationFlow: authenticationFlow, + slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, + userIndicatorController: UserIndicatorControllerMock()) + } +} diff --git a/UnitTests/Sources/ServerSelectionViewModelTests.swift b/UnitTests/Sources/ServerSelectionViewModelTests.swift deleted file mode 100644 index b6aafa1a6d..0000000000 --- a/UnitTests/Sources/ServerSelectionViewModelTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright 2022-2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import XCTest - -@testable import ElementX - -@MainActor -class ServerSelectionViewModelTests: XCTestCase { - var viewModel: ServerSelectionScreenViewModelProtocol! - var context: ServerSelectionScreenViewModelType.Context! - - @MainActor override func setUp() { - viewModel = ServerSelectionScreenViewModel(homeserverAddress: "", - slidingSyncLearnMoreURL: ServiceLocator.shared.settings.slidingSyncLearnMoreURL, - isModallyPresented: true) - context = viewModel.context - } - - func testErrorMessage() async throws { - // Given a new instance of the view model. - XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.") - XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), - "The standard footer message should be shown.") - - // When an error occurs. - let message = "Unable to contact server." - viewModel.displayError(.footerMessage(message)) - - // Then the footer should now be showing an error. - XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.") - XCTAssertEqual(String(context.viewState.footerMessage.characters), message, "The error message should be shown.") - - // And when clearing the error. - context.send(viewAction: .clearFooterError) - - // Wait for the action to spawn a Task. - await Task.yield() - - // Then the error message should now be removed. - XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") - XCTAssertEqual(String(context.viewState.footerMessage.characters), L10n.screenChangeServerFormNotice(L10n.actionLearnMore), - "The standard footer message should be shown again.") - } -} diff --git a/UnitTests/Sources/SessionVerificationStateMachineTests.swift b/UnitTests/Sources/SessionVerificationStateMachineTests.swift index 472c86549c..73fe848201 100644 --- a/UnitTests/Sources/SessionVerificationStateMachineTests.swift +++ b/UnitTests/Sources/SessionVerificationStateMachineTests.swift @@ -15,7 +15,7 @@ class SessionVerificationStateMachineTests: XCTestCase { @MainActor override func setUpWithError() throws { - stateMachine = SessionVerificationScreenStateMachine() + stateMachine = SessionVerificationScreenStateMachine(state: .initial) } func testAcceptChallenge() { diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift index c19c2df466..ec284f712a 100644 --- a/UnitTests/Sources/SessionVerificationViewModelTests.swift +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -18,7 +18,7 @@ class SessionVerificationViewModelTests: XCTestCase { override func setUpWithError() throws { sessionVerificationController = SessionVerificationControllerProxyMock.configureMock() - viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController) + viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController, flow: .initiator) context = viewModel.context } @@ -66,7 +66,7 @@ class SessionVerificationViewModelTests: XCTestCase { let waitForAcceptance = XCTestExpectation(description: "Wait for acceptance") - let cancellable = sessionVerificationController.callbacks + let cancellable = sessionVerificationController.actions .delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first. .sink { callback in switch callback { @@ -94,7 +94,7 @@ class SessionVerificationViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Wait for cancellation") - let cancellable = sessionVerificationController.callbacks + let cancellable = sessionVerificationController.actions .delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first. .sink { callback in switch callback { @@ -124,7 +124,7 @@ class SessionVerificationViewModelTests: XCTestCase { let sasVerificationStartExpectation = XCTestExpectation(description: "Wait for SaS verification start") let verificationDataReceivalExpectation = XCTestExpectation(description: "Wait for Emoji data") - let cancellable = sessionVerificationController.callbacks + let cancellable = sessionVerificationController.actions .delay(for: .seconds(0.1), scheduler: DispatchQueue.main) // Allow the view model to process the callback first. .sink { callback in switch callback { diff --git a/UnitTests/Sources/TextBasedRoomTimelineTests.swift b/UnitTests/Sources/TextBasedRoomTimelineTests.swift index 775621fcbc..66a6d0e337 100644 --- a/UnitTests/Sources/TextBasedRoomTimelineTests.swift +++ b/UnitTests/Sources/TextBasedRoomTimelineTests.swift @@ -11,7 +11,7 @@ import XCTest final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEnd() { let timestamp = "Now" - let timelineItem = TextRoomTimelineItem(id: .random, + let timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: true, isEditable: true, @@ -24,7 +24,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndLonger() { let timestamp = "10:00 AM" - let timelineItem = TextRoomTimelineItem(id: .random, + let timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: true, isEditable: true, @@ -37,7 +37,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndWithEdit() { let timestamp = "Now" - var timelineItem = TextRoomTimelineItem(id: .random, + var timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: true, isEditable: true, @@ -52,7 +52,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() { let timestamp = "Now" - var timelineItem = TextRoomTimelineItem(id: .random, + var timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, isOutgoing: true, isEditable: true, diff --git a/UnitTests/Sources/TimelineItemFactoryTests.swift b/UnitTests/Sources/TimelineItemFactoryTests.swift index ec352957e1..9033adda4e 100644 --- a/UnitTests/Sources/TimelineItemFactoryTests.swift +++ b/UnitTests/Sources/TimelineItemFactoryTests.swift @@ -6,6 +6,8 @@ // @testable import ElementX +import MatrixRustSDK + import XCTest @MainActor @@ -18,19 +20,9 @@ class TimelineItemFactoryTests: XCTestCase { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: ownUserID)) - let eventTimelineItem = EventTimelineItemSDKMock() - eventTimelineItem.isOwnReturnValue = true - eventTimelineItem.timestampReturnValue = 0 - eventTimelineItem.isEditableReturnValue = false - eventTimelineItem.canBeRepliedToReturnValue = false - eventTimelineItem.senderReturnValue = senderUserID - eventTimelineItem.senderProfileReturnValue = .pending - - let timelineItemContent = TimelineItemContentSDKMock() - timelineItemContent.kindReturnValue = .callInvite - eventTimelineItem.contentReturnValue = timelineItemContent + let eventTimelineItem = EventTimelineItem.mockCallInvite(sender: senderUserID) - let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, id: "0") + let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init(id: "0")) let item = factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 9876a7cad1..fb89dc6f3a 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -8,6 +8,7 @@ @testable import ElementX import Combine +import MatrixRustSDK import XCTest @MainActor @@ -256,44 +257,11 @@ class TimelineViewModelTests: XCTestCase { XCTAssertEqual(arguments?.type, .read) } - func testSendMoreReadReceipts() async throws { - // Given a room with only text items in the timeline that are all read. - let items = [TextRoomTimelineItem(eventID: "t1"), - TextRoomTimelineItem(eventID: "t2"), - TextRoomTimelineItem(eventID: "t3")] - let (viewModel, _, timelineProxy, timelineController) = readReceiptsConfiguration(with: items) - viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) - try await Task.sleep(for: .milliseconds(100)) - XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1) - var arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments - XCTAssertEqual(arguments?.eventID, "t3") - XCTAssertEqual(arguments?.type, .read) - - // When sending a receipt for the first item in the timeline. - viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id)) - try await Task.sleep(for: .milliseconds(100)) - - // When a new message is received and marked as read. - let newMessage = TextRoomTimelineItem(eventID: "t4") - timelineController.timelineItems.append(newMessage) - timelineController.callbacks.send(.updatedTimelineItems(timelineItems: timelineController.timelineItems, isSwitchingTimelines: false)) - try await Task.sleep(for: .milliseconds(100)) - - viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(newMessage.id)) - try await Task.sleep(for: .milliseconds(100)) - - // Then the request should be made. - XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 3) - arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments - XCTAssertEqual(arguments?.eventID, "t4") - XCTAssertEqual(arguments?.type, .read) - } - func testSendReadReceiptWithoutEvents() async throws { // Given a room with only virtual items. - let items = [SeparatorRoomTimelineItem(timelineID: "v1"), - SeparatorRoomTimelineItem(timelineID: "v2"), - SeparatorRoomTimelineItem(timelineID: "v3")] + let items = [SeparatorRoomTimelineItem(uniqueID: .init(id: "v1")), + SeparatorRoomTimelineItem(uniqueID: .init(id: "v2")), + SeparatorRoomTimelineItem(uniqueID: .init(id: "v3"))] let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. @@ -308,7 +276,7 @@ class TimelineViewModelTests: XCTestCase { // Given a room where the last event is a virtual item. let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), - SeparatorRoomTimelineItem(timelineID: "v3")] + SeparatorRoomTimelineItem(uniqueID: .init(id: "v3"))] let (viewModel, _, _, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. @@ -336,13 +304,14 @@ class TimelineViewModelTests: XCTestCase { let viewModel = TimelineViewModel(roomProxy: roomProxy, timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) return (viewModel, roomProxy, timelineProxy, timelineController) } @@ -360,13 +329,14 @@ class TimelineViewModelTests: XCTestCase { timelineController.timelineItems = [message] let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts @@ -379,54 +349,48 @@ class TimelineViewModelTests: XCTestCase { // MARK: - Pins func testPinnedEvents() async throws { - ServiceLocator.shared.settings.pinningEnabled = true - - // Note: We need to start the test with a non-default value so we know the view model has finished the Task. - let roomProxyMock = JoinedRoomProxyMock(.init(name: "", - pinnedEventIDs: .init(["test1"]))) - let actionsSubject = PassthroughSubject() - roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() + var configuration = JoinedRoomProxyMockConfiguration(name: "", + pinnedEventIDs: .init(["test1"])) + let roomProxyMock = JoinedRoomProxyMock(configuration) + let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) - - var deferred = deferFulfillment(viewModel.context.$viewState) { value in - value.pinnedEventIDs == ["test1"] - } - try await deferred.fulfill() + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) + XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs) - roomProxyMock.underlyingPinnedEventIDs = ["test1", "test2"] - deferred = deferFulfillment(viewModel.context.$viewState) { value in + configuration.pinnedEventIDs = ["test1", "test2"] + let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.pinnedEventIDs == ["test1", "test2"] } - actionsSubject.send(.roomInfoUpdate) + infoSubject.send(.init(roomInfo: RoomInfo(configuration))) try await deferred.fulfill() } func testCanUserPinEvents() async throws { - ServiceLocator.shared.settings.pinningEnabled = true - - // Note: We need to start the test with the non-default value so we know the view model has finished the Task. - let roomProxyMock = JoinedRoomProxyMock(.init(name: "", canUserPin: true)) - let actionsSubject = PassthroughSubject() - roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() + let configuration = JoinedRoomProxyMockConfiguration(name: "", canUserPin: true) + let roomProxyMock = JoinedRoomProxyMock(configuration) + let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) var deferred = deferFulfillment(viewModel.context.$viewState) { value in value.canCurrentUserPin @@ -437,7 +401,7 @@ class TimelineViewModelTests: XCTestCase { deferred = deferFulfillment(viewModel.context.$viewState) { value in !value.canCurrentUserPin } - actionsSubject.send(.roomInfoUpdate) + infoSubject.send(.init(roomInfo: RoomInfo(configuration))) try await deferred.fulfill() } @@ -449,20 +413,21 @@ class TimelineViewModelTests: XCTestCase { TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")), focussedEventID: focussedEventID, timelineController: timelineController, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) } } private extension TextRoomTimelineItem { init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) { let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(id: sender, timestamp: Date())])] : [] - self.init(id: .random, + self.init(id: .randomEvent, timestamp: "10:47 am", isOutgoing: sender == "bob", isEditable: sender == "bob", @@ -475,14 +440,14 @@ private extension TextRoomTimelineItem { } private extension SeparatorRoomTimelineItem { - init(timelineID: String) { - self.init(id: .init(timelineID: timelineID), text: "") + init(uniqueID: TimelineUniqueId) { + self.init(id: .virtual(uniqueID: uniqueID), text: "") } } private extension TextRoomTimelineItem { init(eventID: String) { - self.init(id: .init(timelineID: UUID().uuidString, eventID: eventID), + self.init(id: .event(uniqueID: .init(id: UUID().uuidString), eventOrTransactionID: .eventId(eventId: eventID)), timestamp: "", isOutgoing: false, isEditable: false, diff --git a/UnitTests/Sources/TracingConfigurationTests.swift b/UnitTests/Sources/TracingConfigurationTests.swift index 864c8898fe..2150be85be 100644 --- a/UnitTests/Sources/TracingConfigurationTests.swift +++ b/UnitTests/Sources/TracingConfigurationTests.swift @@ -10,14 +10,35 @@ import XCTest @testable import ElementX class TracingConfigurationTests: XCTestCase { - func testConfiguration() { - let configuration = TracingConfiguration(logLevel: .trace, target: nil) - - let filterComponents = configuration.filter.components(separatedBy: ",") - XCTAssertEqual(filterComponents.first, "info") - XCTAssertTrue(filterComponents.contains("matrix_sdk_base::sliding_sync=trace")) - XCTAssertTrue(filterComponents.contains("matrix_sdk::http_client=debug")) - XCTAssertTrue(filterComponents.contains("matrix_sdk_crypto=debug")) - XCTAssertTrue(filterComponents.contains("hyper=warn")) + func testConfiguration() { // swiftlint:disable line_length + var filter = TracingConfiguration(logLevel: .error, currentTarget: "tests", filePrefix: nil).filter + + XCTAssertEqual(filter, "hyper=warn,matrix_sdk_ffi=info,matrix_sdk::client=trace,matrix_sdk_crypto=debug,matrix_sdk_crypto::olm::account=trace,matrix_sdk::oidc=trace,matrix_sdk::http_client=debug,matrix_sdk::sliding_sync=info,matrix_sdk_base::sliding_sync=info,matrix_sdk_ui::timeline=info,tests=error") + + filter = TracingConfiguration(logLevel: .warn, currentTarget: "tests", filePrefix: nil).filter + + XCTAssertEqual(filter, "hyper=warn,matrix_sdk_ffi=info,matrix_sdk::client=trace,matrix_sdk_crypto=debug,matrix_sdk_crypto::olm::account=trace,matrix_sdk::oidc=trace,matrix_sdk::http_client=debug,matrix_sdk::sliding_sync=info,matrix_sdk_base::sliding_sync=info,matrix_sdk_ui::timeline=info,tests=warn") + + filter = TracingConfiguration(logLevel: .info, currentTarget: "tests", filePrefix: nil).filter + + XCTAssertEqual(filter, "hyper=warn,matrix_sdk_ffi=info,matrix_sdk::client=trace,matrix_sdk_crypto=debug,matrix_sdk_crypto::olm::account=trace,matrix_sdk::oidc=trace,matrix_sdk::http_client=debug,matrix_sdk::sliding_sync=info,matrix_sdk_base::sliding_sync=info,matrix_sdk_ui::timeline=info,tests=info") + + filter = TracingConfiguration(logLevel: .debug, currentTarget: "tests", filePrefix: nil).filter + + XCTAssertEqual(filter, "hyper=warn,matrix_sdk_ffi=debug,matrix_sdk::client=trace,matrix_sdk_crypto=debug,matrix_sdk_crypto::olm::account=trace,matrix_sdk::oidc=trace,matrix_sdk::http_client=debug,matrix_sdk::sliding_sync=debug,matrix_sdk_base::sliding_sync=debug,matrix_sdk_ui::timeline=debug,tests=debug") + + filter = TracingConfiguration(logLevel: .trace, currentTarget: "tests", filePrefix: nil).filter + + XCTAssertEqual(filter, "hyper=warn,matrix_sdk_ffi=trace,matrix_sdk::client=trace,matrix_sdk_crypto=trace,matrix_sdk_crypto::olm::account=trace,matrix_sdk::oidc=trace,matrix_sdk::http_client=trace,matrix_sdk::sliding_sync=trace,matrix_sdk_base::sliding_sync=trace,matrix_sdk_ui::timeline=trace,tests=trace") + } // swiftlint:enable line_length + + func testLevelOrdering() { + var logLevels: [TracingConfiguration.LogLevel] = [.info, .error, .trace, .debug, .warn] + + XCTAssertEqual(logLevels.sorted(), [.error, .warn, .info, .debug, .trace]) + + logLevels = [.warn, .error, .debug, .trace, .info, .error] + + XCTAssertEqual(logLevels.sorted(), [.error, .error, .warn, .info, .debug, .trace]) } } diff --git a/UnitTests/Sources/UserProfileScreenViewModelTests.swift b/UnitTests/Sources/UserProfileScreenViewModelTests.swift index ecd4d2efda..f040de8c28 100644 --- a/UnitTests/Sources/UserProfileScreenViewModelTests.swift +++ b/UnitTests/Sources/UserProfileScreenViewModelTests.swift @@ -22,7 +22,7 @@ class UserProfileScreenViewModelTests: XCTestCase { viewModel = UserProfileScreenViewModel(userID: profile.userID, isPresentedModally: false, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) @@ -42,7 +42,7 @@ class UserProfileScreenViewModelTests: XCTestCase { viewModel = UserProfileScreenViewModel(userID: profile.userID, isPresentedModally: false, clientProxy: clientProxy, - mediaProvider: MockMediaProvider(), + mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 24e482b76e..83f79dfa92 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -246,7 +246,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws { // Sometimes the state machine's state changes before the coordinators have updated the stack. - let delayedPublisher = userSessionFlowCoordinator.statePublisher.delay(for: .milliseconds(10), scheduler: DispatchQueue.main) + let delayedPublisher = userSessionFlowCoordinator.statePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main) let deferred = deferFulfillment(delayedPublisher) { $0 == expectedState } userSessionFlowCoordinator.handleAppRoute(route, animated: true) diff --git a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift index 644bdd62ea..7a85f15dc4 100644 --- a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift +++ b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift @@ -14,14 +14,14 @@ import XCTest class VoiceMessageMediaManagerTests: XCTestCase { private var voiceMessageMediaManager: VoiceMessageMediaManager! private var voiceMessageCache: VoiceMessageCacheMock! - private var mediaProvider: MockMediaProvider! + private var mediaProvider: MediaProviderMock! private let someURL = URL("/some/url") private let audioOGGMimeType = "audio/ogg" override func setUp() async throws { voiceMessageCache = VoiceMessageCacheMock() - mediaProvider = MockMediaProvider() + mediaProvider = MediaProviderMock(configuration: .init()) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache) } @@ -50,7 +50,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { voiceMessageCache.fileURLForReturnValue = nil let mediaSource = MediaSourceProxy(url: someURL, mimeType: "audio/ogg; codecs=opus") - mediaProvider.loadFileFromSourceReturnValue = MediaFileHandleProxy.unmanaged(url: loadedFile) + mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, @@ -103,7 +103,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { // Check if the file is not already present in cache voiceMessageCache.fileURLForReturnValue = nil let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) - mediaProvider.loadFileFromSourceReturnValue = MediaFileHandleProxy.unmanaged(url: loadedFile) + mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) let audioConverter = AudioConverterMock() voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, @@ -139,7 +139,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { } let audioConverter = AudioConverterMock() - mediaProvider.loadFileFromSourceReturnValue = MediaFileHandleProxy.unmanaged(url: loadedFile) + mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache, diff --git a/ci_scripts/free_space.sh b/ci_scripts/free_space.sh deleted file mode 100755 index 18fdcc3351..0000000000 --- a/ci_scripts/free_space.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -# -# Taken from -# https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 -# - -set -ux - -df -h -sudo rm -rf /usr/share/dotnet -sudo rm -rf /opt/ghc -sudo rm -rf "/usr/local/share/boost" -sudo rm -rf "$AGENT_TOOLSDIRECTORY" -df -h diff --git a/codecov.yml b/codecov.yml index 7af5676dec..a13d2ab90a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,6 +14,7 @@ ignore: - "ElementX/Sources/Vendor" - "ElementX/Sources/UITests" - "ElementX/Sources/UnitTests" + - "ElementX/Sources/Settings/DeveloperOptionsScreen" - "Tools" - "**/Mock*.swift" - "**/*Mock.swift" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d6b573b818..9e336b42d7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -80,21 +80,25 @@ lane :alpha do end lane :unit_tests do |options| + reset_simulator = ENV.key?('CI') + run_tests( scheme: "UnitTests", - device: 'iPhone 16', + device: "iPhone 16 (18.0)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, + reset_simulator: reset_simulator ) if !options[:skip_previews] run_tests( scheme: "PreviewTests", - device: 'iPhone SE (3rd generation)', + device: "iPhone SE (3rd generation) (18.0)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, + reset_simulator: reset_simulator ) end @@ -102,18 +106,14 @@ lane :unit_tests do |options| end lane :ui_tests do |options| - # Use a fresh simulator state to ensure hardware keyboard isn't attached. - # Not necessary when running on GitHub. - # reset_simulator_contents() - create_simulator_if_necessary( - name: "iPhone 16", + name: "iPhone 16 (18.0)", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" ) create_simulator_if_necessary( - name: "iPad (10th generation)", + name: "iPad (10th generation) (18.0)", type: "com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" ) @@ -124,14 +124,17 @@ lane :ui_tests do |options| test_to_run = nil end + reset_simulator = ENV.key?('CI') + run_tests( scheme: "UITests", - devices: ["iPhone 16", "iPad (10th generation)"], + devices: ["iPhone 16 (18.0)", "iPad (10th generation) (18.0)"], ensure_devices_found: true, prelaunch_simulator: true, result_bundle: true, only_testing: test_to_run, number_of_retries: 3, + reset_simulator: reset_simulator ) end @@ -140,17 +143,19 @@ lane :integration_tests do clear_derived_data() create_simulator_if_necessary( - name: "iPhone 16 Pro", + name: "iPhone 16 Pro (18.0)", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" ) + reset_simulator = ENV.key?('CI') + run_tests( scheme: "IntegrationTests", - devices: ["iPhone 16 Pro"], + device: "iPhone 16 Pro (18.0)", ensure_devices_found: true, result_bundle: true, - reset_simulator: true + reset_simulator: reset_simulator ) end diff --git a/project-tchap-x.yml b/project-tchap-x.yml index 0ff3970534..4fb15fac5a 100644 --- a/project-tchap-x.yml +++ b/project-tchap-x.yml @@ -24,7 +24,7 @@ options: settings: DEVELOPMENT_TEAM: NVMQD635C6 - MARKETING_VERSION: 0.1.1 + MARKETING_VERSION: 0.2.0 CURRENT_PROJECT_VERSION: 1 include: diff --git a/project.yml b/project.yml index 04567db8db..4c0b181f7b 100644 --- a/project.yml +++ b/project.yml @@ -10,8 +10,8 @@ options: groupSortPosition: bottom createIntermediateGroups: true deploymentTarget: - iOS: '16.4' - macOS: '13.3' + iOS: '17.6' + macOS: '14.6' groupOrdering: - order: - ElementX @@ -41,7 +41,7 @@ settings: APP_GROUP_IDENTIFIER: group.$(BASE_APP_GROUP_IDENTIFIER) APP_NAME: ElementX KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER)" - MARKETING_VERSION: 1.8.4 + MARKETING_VERSION: 1.9.4 CURRENT_PROJECT_VERSION: 1 SUPPORTS_MACCATALYST: false @@ -60,26 +60,27 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.52 + exactVersion: 1.0.65 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios - revision: 92110afc158ac6ee7c68d5e975144bafa6c58396 + revision: e3f9665621872f60d3652579c3f0dc7bf806e72c # path: ../compound-ios AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events - minorVersion: 0.25.0 + minorVersion: 0.28.0 # path: ../matrix-analytics-events Emojibase: url: https://github.com/matrix-org/emojibase-bindings - minorVersion: 1.0.0 + minorVersion: 1.3.3 + # path: ../emojibase-bindings SwiftOGG: url: https://github.com/element-hq/swift-ogg minorVersion: 0.0.3 # path: ../swift-ogg WysiwygComposer: - url: https://github.com/matrix-org/matrix-rich-text-editor-swift - exactVersion: 2.37.7 + url: https://github.com/element-hq/matrix-rich-text-editor-swift + exactVersion: 2.37.12 # path: ../matrix-rich-text-editor/platforms/ios/lib/WysiwygComposer # External dependencies @@ -91,7 +92,7 @@ packages: minorVersion: 1.0.0 DeviceKit: url: https://github.com/devicekit/DeviceKit - minorVersion: 5.2.2 + minorVersion: 5.5.0 DSWaveformImage: url: https://github.com/dmrschmidt/DSWaveformImage exactVersion: 14.1.1 @@ -100,16 +101,16 @@ packages: exactVersion: 1.6.26 GZIP: url: https://github.com/nicklockwood/GZIP - minorVersion: 1.3.0 + minorVersion: 1.3.2 KeychainAccess: url: https://github.com/kishikawakatsumi/KeychainAccess minorVersion: 4.2.0 Kingfisher: url: https://github.com/onevcat/Kingfisher - minorVersion: 7.6.0 + minorVersion: 8.0.3 KZFileWatchers: url: https://github.com/krzysztofzablocki/KZFileWatchers - branch: master + minorVersion: 1.2.0 LoremSwiftum: url: https://github.com/lukaskubanek/LoremSwiftum minorVersion: 2.2.1 @@ -133,7 +134,7 @@ packages: minorVersion: 6.0.0 Version: url: https://github.com/mxcl/Version - minorVersion: 2.0.0 + minorVersion: 2.1.0 aggregateTargets: Periphery: