diff --git a/.gitattributes b/.gitattributes index a1202033e2..ca32692480 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ UITests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text -UnitTests/Resources/** filter=lfs diff=lfs merge=lfs -text +DevelopmentAssets/Media/** filter=lfs diff=lfs merge=lfs -text UnitTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text PreviewTests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index dabb5e4080..f4e9f148c2 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -7,7 +7,7 @@ on: jobs: build: name: Danger - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index b23b449aa6..eecda979ac 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -11,7 +11,7 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build') name: Release - runs-on: macos-14 + runs-on: macos-15 concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. diff --git a/.github/workflows/translations-pr.yml b/.github/workflows/translations-pr.yml index 2c4ca11abb..695344cebc 100644 --- a/.github/workflows/translations-pr.yml +++ b/.github/workflows/translations-pr.yml @@ -1,13 +1,13 @@ name: Open Translations PR on: schedule: - # At 00:00 on every Monday UTC - - cron: '0 0 * * 1' + # At 03:00 on every Monday UTC + - cron: '0 3 * * 1' workflow_dispatch: jobs: open-translations-pr: - runs-on: macos-14 + runs-on: macos-15 # Skip in forks if: github.repository == 'element-hq/element-x-ios' steps: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 63fa5e54ed..81be7193e4 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -11,7 +11,7 @@ on: jobs: tests: name: Tests - runs-on: macos-14 + runs-on: macos-15 concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. diff --git a/.github/workflows/unit_tests_enterprise.yml b/.github/workflows/unit_tests_enterprise.yml index 5bc8baac36..02545643dd 100644 --- a/.github/workflows/unit_tests_enterprise.yml +++ b/.github/workflows/unit_tests_enterprise.yml @@ -11,7 +11,7 @@ on: jobs: tests: name: Tests (Enterprise) - runs-on: macos-14 + runs-on: macos-15 # Skip in forks if: github.repository == 'element-hq/element-x-ios' diff --git a/CHANGES.md b/CHANGES.md index 2987f6851c..3fe9a833a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,174 @@ +## Changes in 1.9.7 (2024-11-28) + +### What's Changed + +✨ Features +* Support adding a caption to media uploads. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3531 +* Show both defaults and frequent emojis in the timeline item menu, make the list scrollable by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3534 +* Enable inline replies for push notifications. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3542 + +🙌 Improvements +* using `roomPreview` API for invited rooms by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3530 +* Add support for sharing URLs and text. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3546 +* Add support for adding/editing/removing media captions. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3547 +* Update how file captions are rendered by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3554 + +🐛 Bugfixes +* Handle NSItemProvider public.image data types. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3541 +* Fix the media upload preview screen on macOS. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3543 +* Delay handling inline notification replies until the user session is established by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3544 +* Put the share extension Info.plist updates in the xcodegen yaml 🤦‍♂️ by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3549 +* Fix the presentation of QuickLook when viewing logs on macOS. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3556 +* Stop delaying ElementCall until the next sync loop and only notify other participants when presumed to already be up to date. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3559 +* Add back missing send button when media captions are disabled. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3561 +* Fix a bug on iOS 17 where you couldn't long press on a message. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3567 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3552 + +🧱 Build +* Link the MatrixRustSDK dynamically and only embed it in the main target by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3539 + +🚧 In development 🚧 +* Knock Requests List Screen by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3533 +* Knock Requests banner display logic by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3550 +* Knock Requests navigation flows by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3555 + +Others +* Fix UI tests, update compound to roll back snapshot testing and avoid the requirement for Swift Testing. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3540 +* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3545 +* Move timeline item tap gestures to the items themselves instead of the bubbled styler by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3553 +* Bump the RustSDK to v1.0.75 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3557 +* Move the media caption composer (and Add Caption action) behind a feature flag for now. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3560 +* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3565 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.6...1.9.7 + +## Changes in 1.9.6 (2024-11-19) + +### What's Changed + +✨ Features +* Share extension by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3506 +* Enable local echoes for media uploads on development builds. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3514 + +🙌 Improvements +* Stacked Avatars View by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3504 + +🐛 Bugfixes +* Regenerate thumbnails to see if it helps with phantom avatar switching. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3503 +* Fix #1947 - Check expected files are still present before restoring a session. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3507 +* Fix Rooms that user has knocked not displaying the request sent screen by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3513 +* Fix share extension app group so it works for nightlies too by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3520 +* Fix toolbar icons disappearing on the iPad after backgrounding the app by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3521 +* Fix a bug where the security banner has the wrong state when out of sync. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3511 +* Fix image animations / remove fading when switching between local and remote echoes. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3525 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3523 + +🧱 Build +* Update the project to use Xcode 16.1 by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3505 + +🚧 In development 🚧 +* Knock requests banner by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3510 +* Knocking Request Cell by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3516 + +Others +* Update the strings for unsupported calls. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3502 +* Bump the RustSDK to v1.0.67 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3512 +* UI test fixes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3515 +* Group image and video metadata in specialised structs by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3518 +* Update compound by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3519 +* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3524 +* Update compound iOS by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3532 +* Expose the public search feature flag in the developer settings and disable it by default. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3528 +* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3535 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.5...1.9.6 + +## Changes in 1.9.5 (2024-11-11) + +### What's Changed + +🐛 Bugfixes +* Stop setting up CallKit sessions when joining calls by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3496 +* Fix for creating a knocking room by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3499 +* Make stopSync more aware of background usage. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3501 + +🗣 Translations +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3500 + +🚧 In development 🚧 +* Add alias to public room creation by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3450 + +Others +* Fix incorrect analytics mapping for UTDs. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3497 +* Update compound. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3498 +* Tweak the session verification icons and copy. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3495 + + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.4...1.9.5 + +## Changes in 1.9.4 (2024-11-07) + +### What's Changed + +✨ Features +* Hook reaction pickers into the system's recently used keyboard emojis by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3453 +* Incoming session verification support by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3428 +* Enable the Optimised Media Uploads feature. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3467 + +🙌 Improvements +* Enable identity pinning violation notifications unconditionally by @andybalaam in https://github.com/element-hq/element-x-ios/pull/3457 +* Tweak the flow for changing a recovery key. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3452 +* Replace individual RoomProxy properties with a stored RoomInfo. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3445 +* Use an https callback for OIDC once again. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3461 +* Tweak the flow for setting up a recovery key. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3463 +* Tweak the flow for disabling key storage. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3464 +* Directly show Recovery Key and Encryption Reset screens from the home screen banner. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3482 + +🐛 Bugfixes +* Fix the order of the frequently used emojis when showing them in the full reaction picker by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3455 +* Stop the sync loop after each background app refresh. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3481 +* Fix the Setup Recovery flow from the home screen banner. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3483 +* Fix race condition when setting up session verification controller subscriptions by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3486 +* Fix a couple of race conditions when observing room info updates for calls. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3487 +* Syncing fixes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3488 +* Start syncing when receiving a background VoIP call for the cases in which the app was suspended but not terminated by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3491 +* Update SDK 1.0.65 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3494 + +🗣 Translations +* Update translations and some snapshots. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3459 +* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/3477 + +🧱 Build +* remove iOS 16 support by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3443 +* min macos support by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3451 +* Revert "min macos support" by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3458 + +📄 Documentation +* Update the README. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3480 + +🚧 In development 🚧 +* Knocked Preview implementation by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3426 +* Switch optimised video uploads to use 720p by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3462 + +Others +* Update verify identity button title. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3466 +* Update the strings for out of sync Key Storage. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3468 +* Update SDK 1.0.63 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3479 +* Encryption Flow Coordinators. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/3471 +* Update SDK 1.0.64 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/3490 +* Fastlane fails resetting the right simulator, use `device` instead of `destination`. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/3493 + +### New Contributors +* @andybalaam made their first contribution in https://github.com/element-hq/element-x-ios/pull/3457 + +**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/1.9.3...1.9.4 + ## Changes in 1.9.3 (2024-10-24) ### What's Changed diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 715b05cf70..8bd039e09f 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; + 00C3023B6DF55024D8876B76 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 01373C1AC4839604C4FDA404 /* test_apple_image.heic in Resources */ = {isa = PBXBuildFile; fileRef = BB576F4118C35E6B5124FA22 /* test_apple_image.heic */; }; 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */; }; @@ -29,8 +31,10 @@ 024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; }; 02A92F8F4538CECDFB4F2607 /* RoomDirectorySearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */; }; 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; + 0307469D99B5FE6C7043AE39 /* KnockRequestsListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A8EC299E12490588B07C /* KnockRequestsListScreenCoordinator.swift */; }; 037006FB6DF1374F94E4058D /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */; }; 038AB2E86960FD240231D4C2 /* GeneratedPreviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */; }; + 03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 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 */; }; @@ -39,7 +43,10 @@ 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; 05BAB510CBC2ED35C154ADD0 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */; }; 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; + 05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; + 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; + 069358C2C825A19DE6CB127E /* TracingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */; }; 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; @@ -96,12 +103,14 @@ 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; }; 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; 12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; }; + 12E6D052D055531A6783E21B /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */; }; 1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638790D3F915F0909315C47A /* PollView.swift */; }; 1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 13CBC470FB619A6393A21908 /* RoomNotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */; }; 14343C2F9AD2BFEA92CA28FF /* MapTilerStyleBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */; }; 1471A080552631358D152C18 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */; }; + 147597951DB07123A87AA1D1 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; }; 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */; }; 151D2477F75782C8702F2873 /* PollInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */; }; @@ -109,8 +118,10 @@ 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; }; 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; }; + 1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; 1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */; }; 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; + 16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */; }; 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; }; 16E4F1B8B9BFE1367F96DDA7 /* CompletionSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989FC684408B31A677F5538B /* CompletionSuggestionView.swift */; }; 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; @@ -118,13 +129,16 @@ 1795EA6A6C4942CAE0459DF0 /* SecureBackupKeyBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */; }; 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; + 18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; }; 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; }; + 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; }; 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; 19DF5600A7F547B22DD7872A /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; + 1A3783005E6945F8583AF997 /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; 1A3B073568D1DC8F76F1F3A0 /* UserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */; }; @@ -132,7 +146,7 @@ 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */; }; 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */; }; - 1B67DE519285647C98812723 /* ScaledOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */; }; + 1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; }; 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; @@ -154,7 +168,6 @@ 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; 21813AF91CFC6F3E3896DB53 /* AppLockSetupBiometricsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F130DF775CE6BC51A4E392 /* AppLockSetupBiometricsScreenModels.swift */; }; 21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */; }; - 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = C733D11B421CFE3A657EF230 /* test_image.png */; }; 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 */; }; @@ -176,12 +189,15 @@ 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; 260FFC1475EE94F641C3F3F9 /* PollFormScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */; }; 261261778DEFAEFC042B875E /* JoinedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */; }; + 26252AA9AED64010788F4C26 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; }; 2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */; }; 273AB64B9A26B61C51858867 /* AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A07BAEDD74C48795A996A /* AsyncSequence.swift */; }; + 2748E5574A1031DD05E54FDA /* KnockRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B71F6D9062E8EB8929BB97 /* KnockRequestCell.swift */; }; 274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97CE98208321C4D66E363612 /* ShimmerModifier.swift */; }; 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; 27F015B0D5436633B5B3C8C3 /* SecureBackupRecoveryKeyScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */; }; + 27FEF0F40750465195C9D6D6 /* RoomSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */; }; 2814E7075BF3A5C0CCBC9F90 /* RoomDirectorySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF32E4136FD6F159D86C2C /* RoomDirectorySearchView.swift */; }; 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */; }; @@ -194,6 +210,7 @@ 29EE1791E0AFA1ABB7F23D2F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; 2A864BB12A8501B47805D828 /* AuthenticationFlowCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */; }; 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */; }; + 2AB9D4146C8748CF1D007B67 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; }; 2ABF11717C64054CEF2819A3 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */; }; 2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */; }; 2B97BCE72D86645F1485C976 /* RoomDirectorySearchMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */; }; @@ -214,6 +231,7 @@ 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 */; }; + 2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; }; 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; @@ -227,7 +245,6 @@ 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 */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; @@ -236,13 +253,13 @@ 34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 34433A509DFEC93579B3B35B /* AdvancedSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18CC37B97E77838609CFFE7 /* AdvancedSettingsScreen.swift */; }; 3467FEE8210D301FF1B77001 /* UserIndicatorControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */; }; - 3471204F2CC05D4821C35F23 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 7A5D2323D7B6BF4913EB7EED /* landscape_test_image.jpg */; }; 34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */; }; 34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; }; 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; + 36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; }; 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; }; 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */; }; @@ -264,6 +281,7 @@ 3982C505960006B341CFD0C6 /* UserDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */; }; 3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */; }; 39A987B3E41B976D1DF944C6 /* CallScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */; }; + 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */; }; 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; }; 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */; }; 3A64A93A651A3CB8774ADE8E /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; @@ -281,7 +299,6 @@ 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 */; }; @@ -301,6 +318,7 @@ 4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */; }; 422E8D182CA688D4565CD1E1 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */; }; + 42995EA68E194B19DAD6AEEF /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; }; 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */; }; 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */; }; 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */; }; @@ -360,6 +378,7 @@ 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */; }; 4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; + 4E22086585CB3B35FEEFBBB9 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 4E36A66E0EDA74BF3A036FD0 /* RoomChangeRolesScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */; }; 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; @@ -386,11 +405,11 @@ 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; 53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; - 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D455BC2423D911A62ACFB2 /* NSELogger.swift */; }; 54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 5518DA4A6C9B4FC4B497EA9A /* LogViewerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B795AAAB7B8747FE2FF311 /* LogViewerScreenModels.swift */; }; 558E2673B04FDD06A1A12DD3 /* LogViewerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */; }; + 558F37B1A8F2C4CC9B1ACEDA /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 3262F08E1C3483C22A7A319F /* Compound */; }; 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */; }; 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */; }; @@ -409,18 +428,22 @@ 5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6569593FA36B22259E806A67 /* AudioRecorderState.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; + 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */; }; + 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; }; 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 */; }; + 5C61810ED7B7CB48346B1B9D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */; }; 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 */; }; 5D56CE09743C6B90C21B04C2 /* RoomMembersListScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; + 5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; 5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.swift */; }; @@ -436,6 +459,7 @@ 61941DEE5F3834765770BE01 /* InviteUsersScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F32E0B4B83D2A11EE8D011 /* InviteUsersScreenSelectedItem.swift */; }; 61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; }; 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; + 62684AECDFC5C7DC989CBD9E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 7B6BC3219ADD8AA0311D2B86 /* SnapshotTesting */; }; 627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */; }; 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* LoremSwiftum in Frameworks */ = {isa = PBXBuildFile; productRef = 1A6B622CCFDEFB92D9CF1CA5 /* LoremSwiftum */; }; @@ -444,7 +468,6 @@ 63780F9DA06573E38A471ECA /* GenericCallLinkWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */; }; 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; }; 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; }; - 6409CE10CFF4DCB68C4C3872 /* ScaledPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */; }; 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; }; 6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */; }; 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; }; @@ -463,6 +486,8 @@ 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 */; }; + 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */; }; + 66E9202BED03B5BB00E812A1 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.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 */; }; @@ -486,8 +511,8 @@ 6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; }; 6AEB650311F694A5702255C9 /* UserProfileScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */; }; 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */; }; + 6B61F5B27412ED4BC2F9769C /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */; }; 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */; }; - 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */; }; 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */; }; 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */; }; 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; }; @@ -528,6 +553,7 @@ 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 756EA0D663261889EF64E6D4 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */; }; 7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; + 75ED4B73983228BB6922CE3C /* KnockRequestsListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */; }; 762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */; }; 762DB0973865293F0C3D3D7B /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */; }; 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; }; @@ -561,6 +587,7 @@ 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; }; + 7AED78DC086695E93F0647D2 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */; }; 7B3A59786DB2F741A1743ED0 /* PinnedEventsTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510E89B989477E5EE8E503C0 /* PinnedEventsTimelineScreenViewModelProtocol.swift */; }; 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; @@ -569,14 +596,16 @@ 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */; }; 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */; }; 7C0E29E0279866C62EC67A28 /* JoinRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */; }; + 7C164A642E8932B5F9004550 /* test_voice_message.m4a in Resources */ = {isa = PBXBuildFile; fileRef = DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; 7D249465ED00988EEEC14E05 /* JoinedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */; }; 7D261B5119E78CC8E771CA15 /* GlobalSearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */; }; 7D58B4F46CAA9A7C3E4C6A30 /* UserDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88410BD213FDF9B28E8B671F /* UserDetailsEditScreen.swift */; }; - 7D6DC832DE7A3DE874E2E9BC /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 7B6BC3219ADD8AA0311D2B86 /* SnapshotTesting */; }; + 7D6DC832DE7A3DE874E2E9BC /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = BB111AE9D390233CDD2C7FD5 /* MatrixRustSDK */; }; 7E2BB42805C59DB57E95610F /* PillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7773CBFDBD458E0B7E270507 /* PillView.swift */; }; + 7E43FBB918AAC136034F2758 /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = 810133CF215075C285FC6F3A /* test_image.png */; }; 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; }; 7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; @@ -584,6 +613,7 @@ 7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */; }; 7F941B063C94E1718DFC2CF3 /* RoomChangeRolesScreenRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E6EB7960BC9D0F7396B3BD /* RoomChangeRolesScreenRow.swift */; }; 7FED77802940EA7DF4D0D3A2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */; }; + 7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C07EA60CAB296D7726210F5B /* MatrixRustSDK */; }; 7FF6E1FBE6E9517FD29A1D8E /* RoomChangeRolesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */; }; 8015842CB4DE1BE414D2CDED /* AppLockSetupBiometricsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */; }; 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; }; @@ -605,7 +635,6 @@ 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; 8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; }; 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; }; - 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */; }; 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */; }; 859E2CA2EDF343BD24DE52EB /* RoomDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */; }; 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; }; @@ -623,13 +652,13 @@ 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 */; }; - 87CEDB8A0696F0D5AE2ABB28 /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = D5E26C54362206BBDD096D83 /* test_audio.mp3 */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 88356DE7F2AD243AB10C7B7A /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; }; 887AC93C523AEFB640EA5EC8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */; }; 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */; }; 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; + 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 8944548A684F1C837CEC47F4 /* RoomMembersListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */; }; 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */; }; 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; @@ -656,12 +685,14 @@ 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; 8DC176CC5ABA24138EB443DD /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55679AF67545EF8087E47BE /* RoomMemberDetails.swift */; }; + 8DCA1F05C3BA6ED826F1599D /* RoomSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */; }; 8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */; }; 8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; }; 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; 8E7A902CA16E24928F83646C /* ElementCallServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */; }; 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; }; 8F2FAA98457750D9D664136F /* Mapbox in Frameworks */ = {isa = PBXBuildFile; productRef = C1BF15833233CD3BDB7E2B1D /* Mapbox */; }; + 8F3AD08F2E706AA60F1A1D4D /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */; }; 904F06C9C1AEF884C2077542 /* RoomDirectorySearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */; }; 90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; @@ -694,7 +725,6 @@ 9603EEF6DE980BB1D15D4707 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; }; 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; - 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; }; 96B3606E30F824095B1DD022 /* NetworkMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; @@ -732,7 +762,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 */; }; + 9EE71509E6E7519A2B2388B3 /* KnockRequestsListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */; }; 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 */; }; @@ -742,10 +772,12 @@ A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; }; A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; }; A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; }; + A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */; }; A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */; }; A0D7E5BD0298A97DCBDCE40B /* Emojibase in Frameworks */ = {isa = PBXBuildFile; productRef = C05729B1684C331F5FFE9232 /* Emojibase */; }; A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; }; A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; }; + A1672EF491FE6F3BBF7878BE /* test_apple_image.heic in Resources */ = {isa = PBXBuildFile; fileRef = BB576F4118C35E6B5124FA22 /* test_apple_image.heic */; }; A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; }; A1BA8D6BABAFA9BAAEAA3FFD /* NotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */; }; A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */; }; @@ -793,6 +825,7 @@ AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; }; AA5924D3B67F7ACD98BBEFDC /* OrientationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */; }; AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; }; + AAA551AD8768309024D4907B /* KnockRequestsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1511B1DCECC0DC75EB267328 /* KnockRequestsListScreen.swift */; }; AADE7C2497A7B55D8BED7BD6 /* IdentityConfirmedScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; ABD29E06DD1224812E750AF8 /* ReadReceiptCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */; }; @@ -820,6 +853,7 @@ B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; }; B188D0907A4D38AAAF6FEFA8 /* AppLockSetupFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */; }; B1B255CE0E4306DD6E09D936 /* EncryptionResetPasswordScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54A5E6F398C269AD52C9AE21 /* EncryptionResetPasswordScreenModels.swift */; }; + B20484642B41C2D76238BAAA /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; }; B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; }; B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; }; B272E5D1DE8BDA87A6B7A696 /* RoomTimelineProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */; }; @@ -846,8 +880,10 @@ B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; }; B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; }; B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; }; + B855AF29D7D8FC8DAAA73D4A /* test_voice_message.m4a in Resources */ = {isa = PBXBuildFile; fileRef = DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */; }; B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; }; B89990DD875B0B603D4D4332 /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; + B8EC8A544162B0A41B9AB339 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */; }; B93FA0DA1504B301CAEE141B /* NotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; @@ -856,6 +892,7 @@ B9CB30FED3E29D2036EA3FCC /* DeveloperOptionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */; }; BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; + BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */; }; BA4C9049BC96DED3A2F3B82E /* RoomNotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */; }; BAC845780F17CCFBC5A9CA37 /* AppLockUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */; }; BB04B1D8E7401C90506D401E /* QRCodeLoginServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536C0E2178949B290776EA4E /* QRCodeLoginServiceProtocol.swift */; }; @@ -870,9 +907,11 @@ BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */; }; BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */; }; BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; }; + BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; }; BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; }; C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; + C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */; }; @@ -907,6 +946,7 @@ 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 */; }; + C7F20DBF873CC72FB482E326 /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; }; 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 */; }; @@ -919,6 +959,7 @@ 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 */; }; + C9C562D85999E436C7265AF1 /* MatrixRustSDK in Embed Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; @@ -928,7 +969,6 @@ CB6BCBF28E4B76EA08C2926D /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16048D30F0438731C41F775 /* StateRoomTimelineItem.swift */; }; CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; CBA9EDF305036039166E76FF /* StartChatScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */; }; - CBB4F39A1309F7281AE7AA8E /* test_voice_message.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */; }; CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */; }; CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */; }; CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */; }; @@ -950,13 +990,11 @@ D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; D02DEB36D32A72A1B365E452 /* SessionVerificationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */; }; D050D7756E92CA061ED0ABF0 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */; }; - D0550B8E0AE2C0CDBE52C88F /* MediaPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */; }; D0A965852D6C04138FA55181 /* SecureBackupLogoutConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */; }; D10BA4F041DC58580A440A32 /* RoomRolesAndPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */; }; D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4103AB4340F2974D690A12A /* CallScreen.swift */; }; - D1EEF0CB0F5D9C15E224E670 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9A2AC7BE17C05CF7D2A22338 /* landscape_test_video.mov */; }; D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */; }; D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */; }; D26093BB80B69092B0E9AC7C /* PinnedItemsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66763BD54A3A1D9C6E6F2F1 /* PinnedItemsIndicatorView.swift */; }; @@ -976,6 +1014,7 @@ D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; + D5E8EE8A288EFCCF646860EA /* KnockRequestsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */; }; D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; }; D63974A88CF2BC721F109C77 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = DCA3C4A997AD28E6918D4CE5 /* Compound */; }; @@ -983,11 +1022,13 @@ D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; D8459AAD6969B1431ECBE990 /* UnsupportedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */; }; - D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */; }; + D8F1462EA00AFC939FF9ACCA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 203D1ACC20287F8986C959D3 /* target.yml */; }; + D97C782FE0005995C36FA04A /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; }; + DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; }; DB65401349C143DFF883E2B0 /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EC6EA7EDFCE46710DA306 /* AnalyticsPromptScreenViewModel.swift */; }; DBC8D1DBFE9F9CA7662BC8AA /* RoomPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */; }; @@ -1002,6 +1043,7 @@ DEDBD3E9CFCC9F20CAC79881 /* JoinRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */; }; DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */; }; DF05F9C9D3D977EB77E13692 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 385D4C28F9DC5CF53BD9ECDB /* DeviceKit */; }; + DF40CDBEFE1D02B5F9C4ACB1 /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = 810133CF215075C285FC6F3A /* test_image.png */; }; DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; }; DF8F1211F2B0B56F0FCCA5C2 /* CertificateValidatorHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */; }; DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4959CECEC984B3995616F427 /* DataProtectionManager.swift */; }; @@ -1012,6 +1054,7 @@ E0FB26262689F04D66A949D7 /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; }; E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */; }; E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D53754227CEBD06358956D7 /* PinnedEventsTimelineScreenCoordinator.swift */; }; + E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; E21FE4C5B614F311C0955859 /* UserProfileProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */; }; E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */; }; @@ -1039,7 +1082,6 @@ E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */; }; E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; }; - E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; }; E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */; }; E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; @@ -1048,6 +1090,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 */; }; + E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.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 */; }; @@ -1095,22 +1138,27 @@ F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; }; F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; }; + F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; }; + F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; }; F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */; }; F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */; }; F3F9D61C53C348043D3D6F51 /* EncryptionResetScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */; }; F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */; }; - F4971845B5C4F270F6BC5745 /* ScaledFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */; }; + F4582042AA4225CC1E4B8A1E /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */; }; F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */; }; F4C005F006FC3657B9F0A31D /* BugReportHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25586C0ADB814FEE9897DCAA /* BugReportHook.swift */; }; + F4D5A2A8304ED61621BF02D4 /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */; }; F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; }; F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; }; F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; F669B55BC237CDA5EC9332FE /* MentionSuggestionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */; }; + F66BBBE51B258BBB0B918C68 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */; }; F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */; }; F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; @@ -1147,7 +1195,9 @@ FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; + FDC67E8C0EDCB00ABC66C859 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */; }; FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */; }; + FDE47D4686BA0F86BB584633 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CAA3B9DF998B397C9EE64E8B /* Collections */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FEC03105D1BDE0F49BD7F243 /* PinnedEventsTimelineScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6572E6EF5D5F4B0C338A40 /* PinnedEventsTimelineScreenModels.swift */; }; FEFD5290B31FCBA6999912C8 /* RoomChangePermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2721D7B051F0159AA919DA05 /* RoomChangePermissionsScreenViewModelProtocol.swift */; }; @@ -1172,6 +1222,13 @@ remoteGlobalIDString = C0FAEB81CFD9776CD78CE489; remoteInfo = ElementX; }; + 52A426E590105174D83B9532 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AC22997D58D612146053154D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 19F0C845D67E9BEA4BE7133E; + remoteInfo = ShareExtension; + }; 6848AF4480814C5F810FB7EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AC22997D58D612146053154D /* Project object */; @@ -1203,10 +1260,22 @@ dstSubfolderSpec = 13; files = ( EB88DBD77221E2CFE463018C /* NSE.appex in Embed Foundation Extensions */, + 00C3023B6DF55024D8876B76 /* ShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + F5356A7EB31909A578B8B4FB /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C9C562D85999E436C7265AF1 /* MatrixRustSDK in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -1226,7 +1295,6 @@ 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; - 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; @@ -1241,6 +1309,7 @@ 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 = ""; }; + 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; 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 = ""; }; 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; @@ -1305,6 +1374,7 @@ 13BE9781699FB510E9263192 /* AppSettingsHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsHook.swift; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = ""; }; + 1511B1DCECC0DC75EB267328 /* KnockRequestsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreen.swift; sourceTree = ""; }; 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenCoordinator.swift; sourceTree = ""; }; 15748C254911E3654C93B0ED /* MentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionBuilder.swift; sourceTree = ""; }; 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorProtocol.swift; sourceTree = ""; }; @@ -1314,6 +1384,7 @@ 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 = ""; }; + 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyMock.swift; sourceTree = ""; }; 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; @@ -1333,6 +1404,7 @@ 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; + 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenModels.swift; sourceTree = ""; }; 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 = ""; }; @@ -1347,6 +1419,7 @@ 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 = ""; }; + 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModelProtocol.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 = ""; }; @@ -1357,6 +1430,7 @@ 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenCoordinator.swift; sourceTree = ""; }; 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = ""; }; 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = ""; }; + 203D1ACC20287F8986C959D3 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMapProtocol.swift; sourceTree = ""; }; 20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModelProtocol.swift; sourceTree = ""; }; 2141693488CE5446BB391964 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; @@ -1379,6 +1453,7 @@ 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyMock.swift; sourceTree = ""; }; 24B8177BD2AF45A286F5DA31 /* GlobalSearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreen.swift; sourceTree = ""; }; 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModel.swift; sourceTree = ""; }; + 24E637CF570711FB5FD63DEA /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = ""; }; 24EC819497BB5F8C4998D760 /* RoomListFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterView.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1483,15 +1558,18 @@ 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.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 = ""; }; + 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionView.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 = ""; }; 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenModels.swift; sourceTree = ""; }; 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextField.swift; sourceTree = ""; }; + 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionViewController.swift; sourceTree = ""; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = ""; }; 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptCell.swift; sourceTree = ""; }; + 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 3D9B45D584D232CB9E5C7734 /* RoomChangeRolesScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenSelectedItem.swift; sourceTree = ""; }; 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenCoordinator.swift; sourceTree = ""; }; 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenModels.swift; sourceTree = ""; }; @@ -1501,7 +1579,6 @@ 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelTests.swift; sourceTree = ""; }; 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = ""; }; 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; 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 = ""; }; @@ -1511,6 +1588,7 @@ 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 = ""; }; + 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionLogger.swift; sourceTree = ""; }; 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenModels.swift; sourceTree = ""; }; 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModelTests.swift; sourceTree = ""; }; 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; @@ -1518,11 +1596,13 @@ 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutLabelStyle.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 = ""; }; + 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; 43C2067FF58B4996323EB40C /* SessionDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectories.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; + 44B71F6D9062E8EB8929BB97 /* KnockRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestCell.swift; sourceTree = ""; }; 44C314C00533E2C297796B60 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; @@ -1547,8 +1627,10 @@ 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 = ""; }; + 47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsBannerView.swift; sourceTree = ""; }; 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenModels.swift; sourceTree = ""; }; + 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModelTests.swift; sourceTree = ""; }; 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimer.swift; sourceTree = ""; }; 49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; @@ -1579,6 +1661,7 @@ 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 = ""; }; + 505ADA084C0B38A0C4AD2574 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = ""; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = ""; }; @@ -1596,6 +1679,7 @@ 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineItem.swift; sourceTree = ""; }; 536C0E2178949B290776EA4E /* QRCodeLoginServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginServiceProtocol.swift; sourceTree = ""; }; 536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test_animated_image.gif; sourceTree = ""; }; 542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = ""; }; 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = ""; }; @@ -1633,7 +1717,6 @@ 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogProtocol.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D53754227CEBD06358956D7 /* PinnedEventsTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreenCoordinator.swift; sourceTree = ""; }; - 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledFrameModifier.swift; sourceTree = ""; }; 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetection.swift; sourceTree = ""; }; 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = ""; }; 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = ""; }; @@ -1652,6 +1735,7 @@ 61B33F23681660E940BA57F4 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/SAS.strings; sourceTree = ""; }; 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreMapView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListEmptyStateView.swift; sourceTree = ""; }; 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderTests.swift; sourceTree = ""; }; 62B07B296D7A9D2F09120853 /* OrderedSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSet.swift; sourceTree = ""; }; 638790D3F915F0909315C47A /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; @@ -1671,6 +1755,7 @@ 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; + 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; 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 = ""; }; @@ -1681,6 +1766,7 @@ 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = ""; }; 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; + 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenViewModelProtocol.swift; sourceTree = ""; }; 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestsAppCoordinator.swift; sourceTree = ""; }; 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenter.swift; sourceTree = ""; }; 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; @@ -1714,7 +1800,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 = ""; }; + 723B055A57857BFF0F18D9CB /* 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 = ""; }; @@ -1737,10 +1823,10 @@ 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = ""; }; 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = ""; }; + 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape_test_video.mov; sourceTree = ""; }; 796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = ""; }; 79FAC366FF299BCC555D756E /* ElementWellKnown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementWellKnown.swift; sourceTree = ""; }; 7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = ""; }; - 7A5D2323D7B6BF4913EB7EED /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = ""; }; 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelProtocol.swift; sourceTree = ""; }; 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilderHook.swift; sourceTree = ""; }; 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModel.swift; sourceTree = ""; }; @@ -1770,6 +1856,7 @@ 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = ""; }; 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = ""; }; 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; + 810133CF215075C285FC6F3A /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = ""; }; 8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; 811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreen.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -1786,6 +1873,7 @@ 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModelProtocol.swift; sourceTree = ""; }; 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModel.swift; sourceTree = ""; }; 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; + 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModel.swift; sourceTree = ""; }; 84311D707B09854D67F78BBF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersViewModelTests.swift; sourceTree = ""; }; 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelTests.swift; sourceTree = ""; }; @@ -1826,6 +1914,7 @@ 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 = ""; }; + 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenModels.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 = ""; }; @@ -1871,6 +1960,7 @@ 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 969694F67E844FCA51F7E051 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; + 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = ""; }; 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreen.swift; sourceTree = ""; }; 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissions.swift; sourceTree = ""; }; 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1886,7 +1976,6 @@ 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = ""; }; 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilder.swift; sourceTree = ""; }; 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; - 9A2AC7BE17C05CF7D2A22338 /* landscape_test_video.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape_test_video.mov; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = ""; }; 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = ""; }; @@ -1894,7 +1983,6 @@ 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 = ""; }; @@ -1945,11 +2033,13 @@ A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = ""; }; A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; + A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.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 = ""; }; + A9E88667D393612FD5D84718 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/SAS.strings; sourceTree = ""; }; A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = ""; }; AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1982,7 +2072,6 @@ AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFNumberedListView.swift; sourceTree = ""; }; AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; - AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; @@ -2031,6 +2120,7 @@ B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = ""; }; B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = ""; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; + B88CE0A058727BC68EEEC6B6 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; @@ -2039,15 +2129,18 @@ BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = ""; }; + BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = ""; }; 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 = ""; }; + BE98688578F8B0541D853695 /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = ""; }; BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; @@ -2065,6 +2158,7 @@ C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = ""; }; + C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = ""; }; @@ -2096,7 +2190,6 @@ 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 = ""; }; C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; C7D851A10FDA55579960DC61 /* WebRegistrationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreenCoordinator.swift; sourceTree = ""; }; @@ -2106,7 +2199,9 @@ C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; + C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenViewModelTests.swift; sourceTree = ""; }; C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; + C9E8A8EC299E12490588B07C /* KnockRequestsListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenCoordinator.swift; sourceTree = ""; }; C9F893F4A111CB7BA5C96949 /* AppLockSetupBiometricsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModel.swift; sourceTree = ""; }; CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinator.swift; sourceTree = ""; }; CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelTests.swift; sourceTree = ""; }; @@ -2134,6 +2229,7 @@ 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 = ""; }; + D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenViewModel.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = ""; }; D1896F6288D80E1F3EFB3DF8 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ka; path = ka.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -2147,7 +2243,6 @@ D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = ""; }; D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = ""; }; - D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; D3F219838588C62198E726E3 /* LABiometryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LABiometryType.swift; sourceTree = ""; }; D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreen.swift; sourceTree = ""; }; D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorderProtocol.swift; sourceTree = ""; }; @@ -2159,7 +2254,6 @@ D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelProtocol.swift; sourceTree = ""; }; D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenCoordinator.swift; sourceTree = ""; }; - D5E26C54362206BBDD096D83 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceTests.swift; sourceTree = ""; }; D622EC7898469BB1D0881CDD /* PollFormScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreen.swift; sourceTree = ""; }; D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; @@ -2170,6 +2264,7 @@ 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 = ""; }; + D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreen.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 = ""; }; @@ -2184,6 +2279,8 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; 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 = ""; }; @@ -2202,8 +2299,6 @@ E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineItem.swift; sourceTree = ""; }; - E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; - E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledPaddingModifier.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = ""; }; E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = ""; }; @@ -2221,6 +2316,7 @@ 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 = ""; }; + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E60757AFE04391B43EA568B8 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2254,7 +2350,6 @@ EB76A9AFC6CCAD4998D9B045 /* IdentityConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModel.swift; sourceTree = ""; }; 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 = ""; }; @@ -2281,11 +2376,11 @@ F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelTests.swift; sourceTree = ""; }; F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyle.swift; sourceTree = ""; }; F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModel.swift; sourceTree = ""; }; + F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenCoordinator.swift; sourceTree = ""; }; F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelProtocol.swift; sourceTree = ""; }; F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreen.swift; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; @@ -2303,7 +2398,6 @@ 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 = ""; }; @@ -2330,7 +2424,6 @@ FDEDD4D2DE0646DA724985D5 /* QRCodeLoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenModels.swift; sourceTree = ""; }; FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenCoordinator.swift; sourceTree = ""; }; FE1E6FAA3719E9B7A2D5510B /* FormattingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingToolbar.swift; sourceTree = ""; }; - FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProtocol.swift; sourceTree = ""; }; FF720BA68256297680980481 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; FFECCE59967018204876D0A5 /* LocationMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMarkerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2352,6 +2445,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7E8EB7CD881C54161D4474E5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F66BBBE51B258BBB0B918C68 /* MatrixRustSDK in Frameworks */, + FDE47D4686BA0F86BB584633 /* Collections in Frameworks */, + 558F37B1A8F2C4CC9B1ACEDA /* Compound in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A7A4BAD642A61DCC41621311 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF59B36A7B2DB184B62826F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2372,7 +2483,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7D6DC832DE7A3DE874E2E9BC /* SnapshotTesting in Frameworks */, + 7D6DC832DE7A3DE874E2E9BC /* MatrixRustSDK in Frameworks */, + 62684AECDFC5C7DC989CBD9E /* SnapshotTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2492,6 +2604,7 @@ 06501F0E978B2D5C92771DC7 /* Logging */ = { isa = PBXGroup; children = ( + 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */, 111B698739E3410E2CDB7144 /* MXLog.swift */, 542D4F49FABA056DEEEB3400 /* RustTracing.swift */, ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */, @@ -2863,6 +2976,18 @@ path = View; sourceTree = ""; }; + 2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */ = { + isa = PBXGroup; + children = ( + F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */, + 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */, + 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */, + 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */, + FF654D7FD6693839E3185FAD /* View */, + ); + path = RoomSelectionScreen; + sourceTree = ""; + }; 2ECFF6B05DAA37EB10DBF7E8 /* UITests */ = { isa = PBXGroup; children = ( @@ -2949,6 +3074,7 @@ F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */, 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */, 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */, + 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */, @@ -2988,6 +3114,7 @@ 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */, 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */, + A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */, E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */, AD529C89924EE32CE307F36F /* VisualListItem.swift */, ); @@ -3215,12 +3342,24 @@ C0FAC17D4DD7D3A502822550 /* UITests */, 8A9C09B6A392465E03B8D1B1 /* IntegrationTests */, 823ED0EC3F1B6CF47D284011 /* Tools */, + A8002CB4F20B6282850A614C /* DevelopmentAssets */, B04B538A859CD012755DC19C /* NSE */, 1803CD2B96BF06009334BB61 /* PreviewTests */, + D0111119CDF3E28E6D7768E8 /* ShareExtension */, 681566846AF307E9BA4C72C6 /* Products */, ); sourceTree = ""; }; + 40D9A816C45E0278C29DF883 /* SupportingFiles */ = { + isa = PBXGroup; + children = ( + 505ADA084C0B38A0C4AD2574 /* Info.plist */, + B88CE0A058727BC68EEEC6B6 /* ShareExtension.entitlements */, + 203D1ACC20287F8986C959D3 /* target.yml */, + ); + path = SupportingFiles; + sourceTree = ""; + }; 40E6246F03D1FE377BC5D963 /* Room */ = { isa = PBXGroup; children = ( @@ -3514,11 +3653,18 @@ path = BugReportScreen; sourceTree = ""; }; + 557C534BD2052BFFD810CE3D /* ShareExtension */ = { + isa = PBXGroup; + children = ( + DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */, + ); + path = ShareExtension; + sourceTree = ""; + }; 566F2B84465726112B830CF6 /* Other */ = { isa = PBXGroup; children = ( 4959CECEC984B3995616F427 /* DataProtectionManager.swift */, - D3D455BC2423D911A62ACFB2 /* NSELogger.swift */, EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */, 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */, ); @@ -3649,7 +3795,6 @@ 6709362D60732DED2069AE0F /* MediaPlayer */ = { isa = PBXGroup; children = ( - FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */, E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */, F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */, ); @@ -3694,6 +3839,7 @@ 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */, 0D8F620C8B314840D8602E3F /* NSE.appex */, D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */, + 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */, F506C6ADB1E1DA6638078E11 /* UITests.xctest */, AAC9344689121887B74877AF /* UnitTests.xctest */, ); @@ -3867,6 +4013,7 @@ 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */, DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */, FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */, + C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */, 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, @@ -4060,6 +4207,7 @@ 79023E5904B155E8E2B8B502 /* View */ = { isa = PBXGroup; children = ( + 47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, @@ -4140,6 +4288,14 @@ path = Replies; sourceTree = ""; }; + 7FF02C3DED8CD9890375D9FF /* View */ = { + isa = PBXGroup; + children = ( + 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */, + ); + path = View; + sourceTree = ""; + }; 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( @@ -4650,7 +4806,6 @@ A4852B57D55D71EEBFCD931D /* UnitTests */ = { isa = PBXGroup; children = ( - E600AACDF87CDBCE32683236 /* Resources */, 73CD9796729EB702B4DFA88C /* Sources */, 24FD174C31912A5FACFEAFB5 /* SupportingFiles */, ); @@ -4703,6 +4858,14 @@ path = View; sourceTree = ""; }; + A8002CB4F20B6282850A614C /* DevelopmentAssets */ = { + isa = PBXGroup; + children = ( + DDAF2AD15C368A2809857B6A /* Media */, + ); + path = DevelopmentAssets; + sourceTree = ""; + }; AAFDD509929A0CCF8BCE51EB /* Authentication */ = { isa = PBXGroup; children = ( @@ -4732,24 +4895,6 @@ path = View; sourceTree = ""; }; - ACF39CFC617601C942702CDC /* Media */ = { - isa = PBXGroup; - children = ( - 7A5D2323D7B6BF4913EB7EED /* landscape_test_image.jpg */, - 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; - sourceTree = ""; - }; AD5FCF9340D670C526AD17E4 /* UI */ = { isa = PBXGroup; children = ( @@ -4899,12 +5044,24 @@ path = View; sourceTree = ""; }; + BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */ = { + isa = PBXGroup; + children = ( + C9E8A8EC299E12490588B07C /* KnockRequestsListScreenCoordinator.swift */, + 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */, + D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */, + 6A5C217DD0749EC709EED028 /* KnockRequestsListScreenViewModelProtocol.swift */, + F2BBD71E8BF13D2DD2A19064 /* View */, + ); + path = KnockRequestsListScreen; + sourceTree = ""; + }; C0937E3B06A8F0E2DB7C8241 /* Other */ = { isa = PBXGroup; children = ( 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */, 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */, - E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */, + C142248014E08E885E323E56 /* Avatars.swift */, E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */, AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */, @@ -5092,15 +5249,21 @@ 565F1B2B300597C616B37888 /* FullscreenDialog.swift */, 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */, 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */, - 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */, - EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */, - E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */, 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */, EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */, ); path = Layout; sourceTree = ""; }; + D0111119CDF3E28E6D7768E8 /* ShareExtension */ = { + isa = PBXGroup; + children = ( + F08E29610C82E4201463C4A5 /* Sources */, + 40D9A816C45E0278C29DF883 /* SupportingFiles */, + ); + path = ShareExtension; + sourceTree = ""; + }; D382E465AF067C1BF888BF8E /* View */ = { isa = PBXGroup; children = ( @@ -5204,6 +5367,24 @@ path = View; sourceTree = ""; }; + DDAF2AD15C368A2809857B6A /* Media */ = { + isa = PBXGroup; + children = ( + 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */, + 78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */, + BC51BF90469412ABDE658CDD /* portrait_test_image.jpg */, + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */, + 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */, + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */, + 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */, + 810133CF215075C285FC6F3A /* test_image.png */, + BE98688578F8B0541D853695 /* test_pdf.pdf */, + 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */, + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */, + ); + path = Media; + sourceTree = ""; + }; DDC32FD8B94AA19C4FC062AD /* View */ = { isa = PBXGroup; children = ( @@ -5269,6 +5450,7 @@ B53CA9BECD3F97805E1432D0 /* HomeScreen */, F12966DF3DA87FEF21348D60 /* InviteUsersScreen */, FFD7C58CA6A7D6BBC2F584B5 /* JoinRoomScreen */, + BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */, 948DD12A5533BE1BC260E437 /* LocationSharing */, 73E032ADD008D63812791D97 /* LogViewerScreen */, 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, @@ -5291,6 +5473,7 @@ D57B3BC211BB74420C9138D7 /* RoomPollsHistoryScreen */, 7B890CCD20B037760BFDF957 /* RoomRolesAndPermissionsScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, + 2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */, 2565414373E6F68005966B8E /* SecureBackup */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, @@ -5310,14 +5493,6 @@ path = SDK; sourceTree = ""; }; - E600AACDF87CDBCE32683236 /* Resources */ = { - isa = PBXGroup; - children = ( - ACF39CFC617601C942702CDC /* Media */, - ); - path = Resources; - sourceTree = ""; - }; E68740F873AB18A5C26844EA /* Sources */ = { isa = PBXGroup; children = ( @@ -5330,6 +5505,7 @@ 22F9F1514B91803BB4B88894 /* AppHooks */, 337015ADFBA3AB96660DB3A6 /* Generated */, 31CE4DA53232AA534057F912 /* Mocks */, + 557C534BD2052BFFD810CE3D /* ShareExtension */, 4C826614718790C58C17117F /* UnitTests */, ); path = Sources; @@ -5422,6 +5598,15 @@ path = BlockedUsersScreen; sourceTree = ""; }; + F08E29610C82E4201463C4A5 /* Sources */ = { + isa = PBXGroup; + children = ( + 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */, + 7FF02C3DED8CD9890375D9FF /* View */, + ); + path = Sources; + sourceTree = ""; + }; F12966DF3DA87FEF21348D60 /* InviteUsersScreen */ = { isa = PBXGroup; children = ( @@ -5434,6 +5619,16 @@ path = InviteUsersScreen; sourceTree = ""; }; + F2BBD71E8BF13D2DD2A19064 /* View */ = { + isa = PBXGroup; + children = ( + 44B71F6D9062E8EB8929BB97 /* KnockRequestCell.swift */, + 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */, + 1511B1DCECC0DC75EB267328 /* KnockRequestsListScreen.swift */, + ); + path = View; + sourceTree = ""; + }; F3F90EBF3341F1FB47579B77 /* View */ = { isa = PBXGroup; children = ( @@ -5563,6 +5758,14 @@ path = View; sourceTree = ""; }; + FF654D7FD6693839E3185FAD /* View */ = { + isa = PBXGroup; + children = ( + D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */, + ); + path = View; + sourceTree = ""; + }; FFD7C58CA6A7D6BBC2F584B5 /* JoinRoomScreen */ = { isa = PBXGroup; children = ( @@ -5612,12 +5815,35 @@ productReference = F506C6ADB1E1DA6638078E11 /* UITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 19F0C845D67E9BEA4BE7133E /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A60414DDC2A95B206C91D4A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 8431C24C3AE0AA27308F4185 /* Sources */, + 087F14F27D0A6FDFB80392A1 /* Resources */, + 7E8EB7CD881C54161D4474E5 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + packageProductDependencies = ( + C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */, + CAA3B9DF998B397C9EE64E8B /* Collections */, + 3262F08E1C3483C22A7A319F /* Compound */, + ); + productName = ShareExtension; + productReference = 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 32C23C8D224D46EFE62AFAD0 /* UnitTests */ = { isa = PBXNativeTarget; buildConfigurationList = 79663128986C62EFAC289176 /* Build configuration list for PBXNativeTarget "UnitTests" */; buildPhases = ( 11F93544B4FC60F78F47D89C /* Sources */, 9B3512762CF4A1D45A79C340 /* Resources */, + A7A4BAD642A61DCC41621311 /* Frameworks */, ); buildRules = ( ); @@ -5625,6 +5851,9 @@ 0EEC1557A40FBA6DF49D83A2 /* PBXTargetDependency */, ); name = UnitTests; + packageProductDependencies = ( + C07EA60CAB296D7726210F5B /* MatrixRustSDK */, + ); productName = UnitTests; productReference = AAC9344689121887B74877AF /* UnitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -5644,6 +5873,7 @@ ); name = PreviewTests; packageProductDependencies = ( + BB111AE9D390233CDD2C7FD5 /* MatrixRustSDK */, 7B6BC3219ADD8AA0311D2B86 /* SnapshotTesting */, ); productName = PreviewTests; @@ -5661,6 +5891,7 @@ 215E1D91B98672C856F559D0 /* Resources */, EE878EAA342710DB973E0A87 /* Frameworks */, 8E3CD0D0BB6697512E867C1D /* Embed Foundation Extensions */, + F5356A7EB31909A578B8B4FB /* Embed Frameworks */, 98CA896D84BFD53B2554E891 /* ⚠️ SwiftLint */, B35AB66424BB30087EEE408C /* 🧹 SwiftFormat */, ); @@ -5668,6 +5899,7 @@ ); dependencies = ( 2C29670603B37E38705D5FF1 /* PBXTargetDependency */, + 58C473A5DEA945AACFEA8E9F /* PBXTargetDependency */, ); name = ElementX; packageProductDependencies = ( @@ -5770,6 +6002,9 @@ DevelopmentTeam = 7J4U792NQT; TestTargetID = C0FAEB81CFD9776CD78CE489; }; + 19F0C845D67E9BEA4BE7133E = { + DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; + }; 32C23C8D224D46EFE62AFAD0 = { DevelopmentTeam = 7J4U792NQT; }; @@ -5806,6 +6041,7 @@ es, et, fa, + fi, fr, hu, id, @@ -5858,6 +6094,7 @@ FEB53A5BC378C913769656D8 /* NSE */, F8E276FD6DC43EADB85241BC /* Periphery */, 7A17BE29BAC81ADBAC6349D9 /* PreviewTests */, + 19F0C845D67E9BEA4BE7133E /* ShareExtension */, 0E28CD62691FDBC63147D5E3 /* UITests */, 32C23C8D224D46EFE62AFAD0 /* UnitTests */, ); @@ -5865,6 +6102,14 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 087F14F27D0A6FDFB80392A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D8F1462EA00AFC939FF9ACCA /* target.yml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 215E1D91B98672C856F559D0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -5880,8 +6125,19 @@ 5FCD8AFA364206EE32B909A3 /* Settings.bundle in Resources */, CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */, 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */, + 147597951DB07123A87AA1D1 /* landscape_test_image.jpg in Resources */, + FDC67E8C0EDCB00ABC66C859 /* landscape_test_video.mov in Resources */, E67418DACEDBC29E988E6ACD /* message.caf in Resources */, + 12E6D052D055531A6783E21B /* portrait_test_image.jpg in Resources */, + 5C61810ED7B7CB48346B1B9D /* portrait_test_video.mp4 in Resources */, DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */, + 18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */, + 01373C1AC4839604C4FDA404 /* test_apple_image.heic in Resources */, + 6B61F5B27412ED4BC2F9769C /* test_audio.mp3 in Resources */, + DF40CDBEFE1D02B5F9C4ACB1 /* test_image.png in Resources */, + 2AB9D4146C8748CF1D007B67 /* test_pdf.pdf in Resources */, + C7F20DBF873CC72FB482E326 /* test_rotated_image.jpg in Resources */, + 7C164A642E8932B5F9004550 /* test_voice_message.m4a in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5914,18 +6170,18 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3471204F2CC05D4821C35F23 /* landscape_test_image.jpg in Resources */, - D1EEF0CB0F5D9C15E224E670 /* landscape_test_video.mov in Resources */, - 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */, - 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */, + F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */, + F4582042AA4225CC1E4B8A1E /* landscape_test_video.mov in Resources */, + 8F3AD08F2E706AA60F1A1D4D /* portrait_test_image.jpg in Resources */, + D97C782FE0005995C36FA04A /* 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 */, + B20484642B41C2D76238BAAA /* test_animated_image.gif in Resources */, + A1672EF491FE6F3BBF7878BE /* test_apple_image.heic in Resources */, + F4D5A2A8304ED61621BF02D4 /* test_audio.mp3 in Resources */, + 7E43FBB918AAC136034F2758 /* test_image.png in Resources */, + 1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */, + 42995EA68E194B19DAD6AEEF /* test_rotated_image.jpg in Resources */, + B855AF29D7D8FC8DAAA73D4A /* test_voice_message.m4a in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6067,7 +6323,7 @@ 484202C5D50983442D24D061 /* AttributedString.swift in Sources */, CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */, BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */, - 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */, + F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */, 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, 238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */, 0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */, @@ -6077,6 +6333,7 @@ 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, 9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */, CFEC53440C572CEEABC4A6A0 /* ElementXAttributeScope.swift in Sources */, + 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */, A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */, @@ -6091,7 +6348,6 @@ 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */, 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */, E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */, - 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */, 30CC4F796B27BE8B1DFDBF5A /* NSEUserSession.swift in Sources */, 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */, 94F0B78928E952689ACDB271 /* NetworkMonitor.swift in Sources */, @@ -6170,6 +6426,7 @@ A216C83ADCF32BA5EF8A6FBC /* InviteUsersViewModelTests.swift in Sources */, 7C0E29E0279866C62EC67A28 /* JoinRoomScreenViewModelTests.swift in Sources */, EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, + BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */, CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */, 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, @@ -6263,6 +6520,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8431C24C3AE0AA27308F4185 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B8EC8A544162B0A41B9AB339 /* AppSettings.swift in Sources */, + 2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */, + F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */, + C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */, + 05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */, + 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */, + 1A3783005E6945F8583AF997 /* NSItemProvider.swift in Sources */, + BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */, + 7AED78DC086695E93F0647D2 /* RustTracing.swift in Sources */, + DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */, + 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */, + 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */, + 069358C2C825A19DE6CB127E /* TracingConfiguration.swift in Sources */, + 03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */, + 26252AA9AED64010788F4C26 /* UIView.swift in Sources */, + 66E9202BED03B5BB00E812A1 /* URL.swift in Sources */, + 4E22086585CB3B35FEEFBBB9 /* UserPreference.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9797D588420FCBBC228A63C9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -6367,7 +6648,7 @@ 874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */, 6146996D5C4DDD5DA816FC87 /* AuthenticationTextFieldStyle.swift in Sources */, 4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */, - D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, + 1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */, 7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */, A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */, FC0EEFF630F34899953BB950 /* BigIcon.swift in Sources */, @@ -6505,6 +6786,7 @@ 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, + 36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */, @@ -6574,6 +6856,14 @@ 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */, FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */, CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */, + 2748E5574A1031DD05E54FDA /* KnockRequestCell.swift in Sources */, + D5E8EE8A288EFCCF646860EA /* KnockRequestsBannerView.swift in Sources */, + E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */, + AAA551AD8768309024D4907B /* KnockRequestsListScreen.swift in Sources */, + 0307469D99B5FE6C7043AE39 /* KnockRequestsListScreenCoordinator.swift in Sources */, + 9EE71509E6E7519A2B2388B3 /* KnockRequestsListScreenModels.swift in Sources */, + A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */, + 75ED4B73983228BB6922CE3C /* KnockRequestsListScreenViewModelProtocol.swift in Sources */, C969A62F3D9F14318481A33B /* KnockedRoomProxy.swift in Sources */, 6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */, 454F8DDC4442C0DE54094902 /* LABiometryType.swift in Sources */, @@ -6623,7 +6913,6 @@ 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */, A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */, 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */, - D0550B8E0AE2C0CDBE52C88F /* MediaPlayerProtocol.swift in Sources */, F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */, 762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */, B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */, @@ -6856,6 +7145,11 @@ 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, + 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */, + 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */, + 27FEF0F40750465195C9D6D6 /* RoomSelectionScreenModels.swift in Sources */, + 8DCA1F05C3BA6ED826F1599D /* RoomSelectionScreenViewModel.swift in Sources */, + 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */, 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */, 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */, 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */, @@ -6880,9 +7174,6 @@ 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */, D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */, 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */, - F4971845B5C4F270F6BC5745 /* ScaledFrameModifier.swift in Sources */, - 1B67DE519285647C98812723 /* ScaledOffsetModifier.swift in Sources */, - 6409CE10CFF4DCB68C4C3872 /* ScaledPaddingModifier.swift in Sources */, FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */, 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */, 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */, @@ -6941,6 +7232,7 @@ B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */, E0B6A569AC3E81D233B43D60 /* SettingsScreenViewModel.swift in Sources */, A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */, + 5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */, 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */, 274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */, 77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */, @@ -6951,6 +7243,7 @@ F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */, CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */, DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, + E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */, 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */, 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */, C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */, @@ -7003,6 +7296,7 @@ 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */, B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */, E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */, + 16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */, 2FEC6652055984389CE1BBEC /* TimelineProxyProtocol.swift in Sources */, 8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */, 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */, @@ -7169,6 +7463,11 @@ target = C0FAEB81CFD9776CD78CE489 /* ElementX */; targetProxy = 6848AF4480814C5F810FB7EB /* PBXContainerItemProxy */; }; + 58C473A5DEA945AACFEA8E9F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 19F0C845D67E9BEA4BE7133E /* ShareExtension */; + targetProxy = 52A426E590105174D83B9532 /* PBXContainerItemProxy */; + }; 8E24DC048A099AAFEE13B4F5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C0FAEB81CFD9776CD78CE489 /* ElementX */; @@ -7187,6 +7486,7 @@ 6722709BD6178E10B70C9641 /* es */, F3C7252B3461D06175D975A4 /* et */, C715CFE00686DACA59D836EA /* fa */, + A9E88667D393612FD5D84718 /* fi */, CEE20623EB4A9B88FB29F2BA /* fr */, D196116D2DD3F2757D45FCB7 /* hu */, 330AF4D121C3396F7A14B21D /* id */, @@ -7214,6 +7514,8 @@ 13802897C7AFA360EA74C0B0 /* en */, AACE9B8E1A4AE79A7E2914F6 /* es */, 4F5F0662483ED69791D63B16 /* et */, + 48CE6BF18E542B32FA52CE06 /* fa */, + 057B747CF045D3C6C30EAB2C /* fi */, 653610CB5F9776EAAAB98155 /* fr */, C95ADE8D9527523572532219 /* hu */, 475D47D0BFE961B02BAC5D49 /* id */, @@ -7247,6 +7549,7 @@ CBBCC6E74774E79B599625D0 /* es */, A443FAE2EE820A5790C35C8D /* et */, A9873374E72AA53260AE90A2 /* fa */, + 434522ED2BDED08759048077 /* fi */, CC680E0E79D818706CB28CF8 /* fr */, 624244C398804ADC885239AA /* hu */, EF98A02DED04075F7CF0C721 /* id */, @@ -7279,6 +7582,7 @@ 1215A4FC53D2319E81AE8970 /* en */, 2525D78FEA7E7B132ED85C58 /* es */, 2C39D91A31409775B0F4268F /* et */, + 24E637CF570711FB5FD63DEA /* fi */, ACD7BD6BEE21264F6677904A /* fr */, 1D652E78832289CD9EB64488 /* hu */, 7199693797B66245EF97BCF5 /* id */, @@ -7373,6 +7677,7 @@ CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_ASSET_PATHS = DevelopmentAssets/Media; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; INFOPLIST_FILE = ElementX/SupportingFiles/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -7401,6 +7706,7 @@ CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_ASSET_PATHS = DevelopmentAssets/Media; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; INFOPLIST_FILE = ElementX/SupportingFiles/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -7439,6 +7745,27 @@ }; name = Debug; }; + 7620CDAB1B38B30431DA8878 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = ShareExtension/SupportingFiles/ShareExtension.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = ShareExtension/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.shareextension"; + PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; 7A90A3EBE1ABAB9EAE0952F0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7497,7 +7824,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.6; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER)"; MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.9.4; + MARKETING_VERSION = 1.9.8; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCTION_APP_NAME = Element; @@ -7574,7 +7901,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 17.6; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER)"; MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.9.4; + MARKETING_VERSION = 1.9.8; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -7691,6 +8018,27 @@ }; name = Release; }; + E57C898C511BBC8215673DEF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = ShareExtension/SupportingFiles/ShareExtension.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = ShareExtension/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.shareextension"; + PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; F0A74453D306F668178A859E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7748,6 +8096,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + A60414DDC2A95B206C91D4A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7620CDAB1B38B30431DA8878 /* Debug */, + E57C898C511BBC8215673DEF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; B15427F8699AD5A5FC75C17E /* Build configuration list for PBXNativeTarget "ElementX" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -7858,7 +8215,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.65; + version = 1.0.77; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { @@ -7978,7 +8335,7 @@ repositoryURL = "https://github.com/element-hq/compound-ios"; requirement = { kind = revision; - revision = e3f9665621872f60d3652579c3f0dc7bf806e72c; + revision = 901f3f2fc150db82cf8a2c4da53914b31f681b56; }; }; F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = { @@ -8062,6 +8419,11 @@ package = EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */; productName = Version; }; + 3262F08E1C3483C22A7A319F /* Compound */ = { + isa = XCSwiftPackageProductDependency; + package = F71C70A4404CC6D9C4AF35F2 /* XCRemoteSwiftPackageReference "compound-ios" */; + productName = Compound; + }; 36B7FC232711031AA2B0D188 /* DTCoreText */ = { isa = XCSwiftPackageProductDependency; package = C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */; @@ -8227,6 +8589,11 @@ package = F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */; productName = Collections; }; + BB111AE9D390233CDD2C7FD5 /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; BC01130651CB23340B899032 /* DeviceKit */ = { isa = XCSwiftPackageProductDependency; package = D5F7D47BBAAE0CF1DDEB3034 /* XCRemoteSwiftPackageReference "DeviceKit" */; @@ -8237,6 +8604,11 @@ package = 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */; productName = Emojibase; }; + C07EA60CAB296D7726210F5B /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; C1BF15833233CD3BDB7E2B1D /* Mapbox */ = { isa = XCSwiftPackageProductDependency; package = 0CBF57301AA172C21F76CE86 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */; @@ -8247,11 +8619,21 @@ package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; productName = MatrixRustSDK; }; + C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */ = { isa = XCSwiftPackageProductDependency; package = EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */; productName = WysiwygComposer; }; + CAA3B9DF998B397C9EE64E8B /* Collections */ = { + isa = XCSwiftPackageProductDependency; + package = F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = Collections; + }; CCE5BF78B125320CBF3BB834 /* PostHog */ = { isa = XCSwiftPackageProductDependency; package = 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4cd886738e..6b6570ef8c 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/compound-design-tokens", "state" : { - "revision" : "976db67b849775799b4153e7894d61e90fc96888", - "version" : "1.9.0" + "revision" : "31b236f02c811704b68e8aae429865fe8eb8d8ba", + "version" : "2.1.1" } }, { @@ -15,7 +15,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/compound-ios", "state" : { - "revision" : "e3f9665621872f60d3652579c3f0dc7bf806e72c" + "revision" : "901f3f2fc150db82cf8a2c4da53914b31f681b56" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "399cc70987856c73e24b8888ac1ecc0eecf1716b", - "version" : "1.0.65" + "revision" : "4e01a9482cfefae4f06c6f3a5a33d21a2bf73d84", + "version" : "1.0.77" } }, { diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme new file mode 100644 index 0000000000..6bbbe2daaa --- /dev/null +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Resources/Localizations/be.lproj/Localizable.strings b/ElementX/Resources/Localizations/be.lproj/Localizable.strings index 5e496bb337..4fa69ce0df 100644 --- a/ElementX/Resources/Localizations/be.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/be.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Запісаць галасавое паведамленне."; "a11y_voice_message_stop_recording" = "Спыніць запіс"; "action_accept" = "Прыняць"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Дадаць у хроніку"; "action_back" = "Назад"; "action_call" = "Званок"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Пацвердзіць пароль"; "action_continue" = "Працягнуць"; "action_copy" = "Капіраваць"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Скапіраваць спасылку"; "action_copy_link_to_message" = "Скапіраваць спасылку на паведамленне"; +"action_copy_text" = "Copy text"; "action_create" = "Стварыць"; "action_create_a_room" = "Стварыце пакой"; "action_deactivate" = "Дэактываваць"; @@ -47,6 +50,7 @@ "action_discard" = "Адмяніць"; "action_done" = "Гатова"; "action_edit" = "Рэдагаваць"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Рэдагаваць апытанне"; "action_enable" = "Уключыць"; "action_end_poll" = "Скончыць апытанне"; @@ -81,6 +85,8 @@ "action_react" = "Рэакцыя"; "action_reject" = "Адхіліць"; "action_remove" = "Выдаліць"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Адказаць"; "action_reply_in_thread" = "Адказаць у гутаркі"; "action_report_bug" = "Паведаміць пра памылку"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Наладзіць аднаўленне"; "common_about" = "Аб праграме"; "common_acceptable_use_policy" = "Палітыка дапушчальнага выкарыстання"; +"common_adding_caption" = "Adding caption"; "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" = "Аўтарскае права"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Прамы чат"; "common_edited_suffix" = "(Адрэдагавана)"; "common_editing" = "Рэдагаванне"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Шыфраванне ўключана"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Немагчыма адправіць запрашэнне(я)"; "common_unlock" = "Разблакіраваць"; "common_unmute" = "Укл. гук"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Падзея не падтрымліваецца"; "common_username" = "Імя карыстальніка"; "common_verification_cancelled" = "Праверка адменена"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Хто заўгодна"; +"screen_create_room_room_access_section_header" = "Доступ у пакой"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Папрасіце далучыцца"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."; +"screen_media_upload_preview_error_failed_sending" = "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."; "screen_pinned_timeline_empty_state_description" = "Націсніце на паведамленне і абярыце «%1$@ », каб уключыць сюды."; "screen_pinned_timeline_empty_state_headline" = "Замацуеце важныя паведамленні, каб іх можна было лёгка знайсці"; "screen_reset_encryption_password_error" = "Адбылася невядомая памылка. Калі ласка, праверце правільнасць пароля вашага ўліковага запісу і паўтарыце спробу."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Замацаваныя паведамленні"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "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" = "Вы можаце змяніць налады пазней."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Дадаць эмодзі"; "screen_room_timeline_beginning_of_room" = "Гэта пачатак %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Гэта пачатак гэтай размовы."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "Паказаць менш"; "screen_room_timeline_message_copied" = "Паведамленне скапіравана"; "screen_room_timeline_no_permission_to_post" = "У Вас няма дазволу на публікацыю ў гэтым пакоі"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Яны не супадаюць"; "screen_session_verification_they_match" = "Яны супадаюць"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Для працягу працы прыміце запыт на запуск працэсу праверкі ў іншым сеансе."; "screen_session_verification_waiting_to_accept_title" = "Чаканне прыняцця запыту"; "screen_share_location_title" = "Падзяліцца месцазнаходжаннем"; diff --git a/ElementX/Resources/Localizations/be.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/be.lproj/Localizable.stringsdict index 954044d393..d6ff532944 100644 --- a/ElementX/Resources/Localizations/be.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/be.lproj/Localizable.stringsdict @@ -254,6 +254,22 @@ %1$d удзельнікаў + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/bg.lproj/Localizable.strings b/ElementX/Resources/Localizations/bg.lproj/Localizable.strings index 8e7a783ffe..df5429a5e1 100644 --- a/ElementX/Resources/Localizations/bg.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/bg.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Record voice message."; "a11y_voice_message_stop_recording" = "Stop recording"; "action_accept" = "Приемане"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Add to timeline"; "action_back" = "Назад"; "action_call" = "Call"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Продължаване"; "action_copy" = "Копиране"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Копиране на връзката"; "action_copy_link_to_message" = "Копиране на връзката към съобщението"; +"action_copy_text" = "Copy text"; "action_create" = "Създаване"; "action_create_a_room" = "Създаване на стая"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Discard"; "action_done" = "Готово"; "action_edit" = "Редактиране"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Редактиране на анкетата"; "action_enable" = "Активиране"; "action_end_poll" = "Приключване на анкетата"; @@ -81,6 +85,8 @@ "action_react" = "Реакция"; "action_reject" = "Reject"; "action_remove" = "Премахване"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Отговор"; "action_reply_in_thread" = "Отговор в нишка"; "action_report_bug" = "Report bug"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Относно"; "common_acceptable_use_policy" = "Acceptable use policy"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Разширени настройки"; "common_analytics" = "Статистика"; "common_appearance" = "Облик"; "common_audio" = "Аудио"; "common_blocked_users" = "Блокирани потребители"; "common_bubbles" = "Bubbles"; -"common_call_invite" = "Call in progress (unsupported)"; "common_call_started" = "Call started"; "common_chat_backup" = "Резервно копие на чатовете"; "common_copyright" = "Copyright"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Директен чат"; "common_edited_suffix" = "(редактирано)"; "common_editing" = "Редактиране"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Шифроването е включено"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Не може да се изпрати покана(и)"; "common_unlock" = "Отключване"; "common_unmute" = "Раззаглушаване"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Неподдържано събитие"; "common_username" = "Потребителско име"; "common_verification_cancelled" = "Потвърждаването е отменено"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; +"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; "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."; @@ -375,11 +402,16 @@ "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" = "Notify the whole room"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Добре дошли отново!"; "screen_login_title_with_homeserver" = "Влизане в %1$@"; "screen_media_picker_error_failed_selection" = "Failed selecting media, please try again."; -"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; -"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; "screen_migration_message" = "This is a one time process, thanks for waiting."; "screen_migration_title" = "Setting up your account."; "screen_notification_optin_subtitle" = "Можете да промените настройките си по-късно."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Добавяне на емоджи"; "screen_room_timeline_beginning_of_room" = "Това е началото на %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Това е началото на този разговор."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Те не съвпадат"; "screen_session_verification_they_match" = "Те съвпадат"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите."; "screen_session_verification_waiting_to_accept_title" = "В очакване на приемане на заявка"; "screen_share_location_title" = "Споделяне на местоположение"; diff --git a/ElementX/Resources/Localizations/bg.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/bg.lproj/Localizable.stringsdict index 4cfc90f2ad..6336536516 100644 --- a/ElementX/Resources/Localizations/bg.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/bg.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d души + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/cs.lproj/Localizable.strings b/ElementX/Resources/Localizations/cs.lproj/Localizable.strings index bc72b44767..1a5f11e52f 100644 --- a/ElementX/Resources/Localizations/cs.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/cs.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Nahrajte hlasovou zprávu."; "a11y_voice_message_stop_recording" = "Zastavit nahrávání"; "action_accept" = "Přijmout"; +"action_add_caption" = "Přidat titulek"; "action_add_to_timeline" = "Přidat na časovou osu"; "action_back" = "Zpět"; "action_call" = "Hovor"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Potvrdit heslo"; "action_continue" = "Pokračovat"; "action_copy" = "Kopírovat"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Kopírovat odkaz"; "action_copy_link_to_message" = "Kopírovat odkaz na zprávu"; +"action_copy_text" = "Copy text"; "action_create" = "Vytvořit"; "action_create_a_room" = "Vytvořit místnost"; "action_deactivate" = "Deaktivovat"; @@ -47,6 +50,7 @@ "action_discard" = "Vyřadit"; "action_done" = "Hotovo"; "action_edit" = "Upravit"; +"action_edit_caption" = "Upravit titulek"; "action_edit_poll" = "Upravit hlasování"; "action_enable" = "Povolit"; "action_end_poll" = "Ukončit hlasování"; @@ -81,6 +85,8 @@ "action_react" = "Reagovat"; "action_reject" = "Odmítnout"; "action_remove" = "Odstranit"; +"action_remove_caption" = "Odstranit titulek"; +"action_remove_message" = "Remove message"; "action_reply" = "Odpovědět"; "action_reply_in_thread" = "Odpovědět ve vlákně"; "action_report_bug" = "Nahlásit chybu"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Nastavení obnovy"; "common_about" = "O aplikaci"; "common_acceptable_use_policy" = "Zásady používání"; +"common_adding_caption" = "Přidání titulku"; "common_advanced_settings" = "Pokročilá nastavení"; "common_analytics" = "Analytika"; "common_appearance" = "Vzhled"; "common_audio" = "Zvuk"; "common_blocked_users" = "Blokovaní uživatelé"; "common_bubbles" = "Bubliny"; -"common_call_invite" = "Probíhá hovor (nepodporováno)"; "common_call_started" = "Hovor zahájen"; "common_chat_backup" = "Záloha chatu"; "common_copyright" = "Autorská práva"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Přímý chat"; "common_edited_suffix" = "(upraveno)"; "common_editing" = "Úpravy"; +"common_editing_caption" = "Úprava titulku"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Šifrování"; "common_encryption_enabled" = "Šifrování povoleno"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Nelze odeslat pozvánky"; "common_unlock" = "Odemknout"; "common_unmute" = "Zrušit ztlumení"; +"common_unsupported_call" = "Nepodporované volání"; "common_unsupported_event" = "Nepodporovaná událost"; "common_username" = "Uživatelské jméno"; "common_verification_cancelled" = "Ověření zrušeno"; @@ -246,10 +254,10 @@ "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í"; +"confirm_recovery_key_banner_message" = "Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv."; +"confirm_recovery_key_banner_primary_button_title" = "Zadejte klíč pro obnovení"; +"confirm_recovery_key_banner_secondary_button_title" = "Zapomněli jste klíč pro obnovení?"; +"confirm_recovery_key_banner_title" = "Vaše úložiště klíčů není synchronizováno"; "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$@"; @@ -344,18 +352,20 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Volitelný titulek..."; "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_access_section_anyone_option_description" = "Do této místnosti může vstoupit kdokoli"; +"screen_create_room_room_access_section_anyone_option_title" = "Kdokoliv"; +"screen_create_room_room_access_section_header" = "Přístup do místnosti"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Požádat o připojení"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Některé znaky nejsou povoleny. Podporovány jsou pouze písmena, číslice a následující symboly ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Tato adresa místnosti již existuje, zkuste prosím upravit pole adresy místnosti nebo změnit název místnosti"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Přijmout vše"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Odmítnout a vykázat"; +"screen_knock_requests_list_empty_state_description" = "Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."; +"screen_knock_requests_list_empty_state_title" = "Žádná čekající žádost o vstup"; +"screen_knock_requests_list_title" = "Žádosti o vstup"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Nahrání média se nezdařilo, zkuste to prosím znovu."; +"screen_media_upload_preview_error_failed_sending" = "Nahrání média se nezdařilo, zkuste to prosím znovu."; "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_reset_encryption_password_error" = "Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu."; @@ -375,11 +402,16 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Jedno nebo více vašich zařízení není ověřeno. Zprávu můžete přesto odeslat, nebo ji můžete prozatím zrušit a zkusit to znovu později, až ověříte všechna svá zařízení."; "screen_resolve_send_failure_you_unsigned_device_title" = "Vaše zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení"; "screen_room_mentions_at_room_subtitle" = "Informujte celou místnost"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Zobrazit vše"; "screen_room_pinned_banner_indicator" = "%1$@ z %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Připnuté zprávy"; "screen_room_pinned_banner_loading_description" = "Načítání zprávy..."; "screen_room_pinned_banner_view_all_button_title" = "Zobrazit vše"; +"screen_room_single_knock_request_accept_button_title" = "Přijmout"; +"screen_room_single_knock_request_title" = "%1$@ chce vstoupit do této místnosti"; +"screen_room_single_knock_request_view_button_title" = "Zobrazit"; "screen_room_details_pinned_events_row_title" = "Připnuté zprávy"; +"screen_room_details_requests_to_join_title" = "Žádosti o vstup"; "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í."; @@ -392,8 +424,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_media_compression_description" = "Rychlejší nahrávání fotografií a videí a snížení spotřeby dat"; +"screen_advanced_settings_media_compression_title" = "Optimalizace kvality médií"; "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ů."; @@ -467,7 +499,7 @@ "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_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_confirm_description" = "Vaše úložiště klíčů je momentálně nesynchronizované."; "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"; @@ -555,8 +587,6 @@ "screen_login_title" = "Vítejte zpět!"; "screen_login_title_with_homeserver" = "Přihlaste se k %1$@"; "screen_media_picker_error_failed_selection" = "Výběr média se nezdařil, zkuste to prosím znovu."; -"screen_media_upload_preview_error_failed_processing" = "Nahrání média se nezdařilo, zkuste to prosím znovu."; -"screen_media_upload_preview_error_failed_sending" = "Nahrání média se nezdařilo, zkuste to prosím znovu."; "screen_migration_message" = "Jedná se o jednorázový proces, prosíme o strpení."; "screen_migration_title" = "Nastavení vašeho účtu"; "screen_notification_optin_subtitle" = "Nastavení můžete později změnit."; @@ -642,7 +672,7 @@ "screen_recovery_key_change_title" = "Změnit klíč pro obnovení?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Vytvořit nový klíč pro obnovení"; "screen_recovery_key_confirm_description" = "Ujistěte se, že tuto obrazovku nikdo nevidí!"; -"screen_recovery_key_confirm_error_content" = "Zkuste prosím znovu potvrdit přístup k záloze chatu."; +"screen_recovery_key_confirm_error_content" = "Zkuste prosím znovu potvrdit přístup k úložišti klíčů."; "screen_recovery_key_confirm_error_title" = "Nesprávný klíč pro obnovení"; "screen_recovery_key_confirm_key_description" = "Pokud máte bezpečnostní klíč nebo bezpečnostní frázi, bude to fungovat také."; "screen_recovery_key_confirm_key_placeholder" = "Zadejte..."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Přidat emoji"; "screen_room_timeline_beginning_of_room" = "Toto je začátek %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Toto je začátek této konverzace."; +"screen_room_timeline_legacy_call" = "Nepodporované volání. Zeptejte se, zda volající může používat novou aplikaci Element X."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Před zahájením ověřování se ujistěte, že máte aplikaci otevřenou na druhém zařízení."; +"screen_session_verification_use_another_device_title" = "Otevřete aplikaci na jiném ověřeném zařízení"; +"screen_session_verification_waiting_another_device_subtitle" = "Na druhém zařízení byste měli vidět vyskakovací okno. Začněte s ověrením tam."; +"screen_session_verification_waiting_another_device_title" = "Spusťte ověření na druhém zařízení"; "screen_session_verification_waiting_to_accept_subtitle" = "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci."; "screen_session_verification_waiting_to_accept_title" = "Čekání na přijetí žádosti"; "screen_share_location_title" = "Sdílet polohu"; @@ -911,7 +946,7 @@ "state_event_room_invite_you" = "Pozvali jste %1$@"; "state_event_room_join" = "%1$@ vstoupil(a) do místnosti"; "state_event_room_join_by_you" = "Vstoupili jste do místnosti"; -"state_event_room_knock" = "%1$@ požádal(a) o vstup"; +"state_event_room_knock" = "%1$@ žádá o vstup"; "state_event_room_knock_accepted" = "%1$@ povolil(a) vstoupit %2$@"; "state_event_room_knock_accepted_by_you" = "Povolili jste %1$@ vstoupit"; "state_event_room_knock_by_you" = "Požádali jste o vstup"; @@ -1033,7 +1068,7 @@ "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_recovery_key_confirm_title" = "Zadejte klíč pro obnovení"; "screen_report_content_block_user" = "Zablokovat uživatele"; "screen_reset_encryption_password_placeholder" = "Zadejte..."; "screen_room_attachment_source_camera_photo" = "Vyfotit"; diff --git a/ElementX/Resources/Localizations/cs.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/cs.lproj/Localizable.stringsdict index 4362b120c3..6c3a3959eb 100644 --- a/ElementX/Resources/Localizations/cs.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/cs.lproj/Localizable.stringsdict @@ -254,6 +254,24 @@ %1$d osob + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d další chce vstoupit do této místnosti + few + %1$@ +%2$d další chtějí vstoupit do této místnosti + other + %1$@ +%2$d dalších chce vstoupit do této místnosti + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/de.lproj/Localizable.strings b/ElementX/Resources/Localizations/de.lproj/Localizable.strings index 0ba57e7b95..8e410678d5 100644 --- a/ElementX/Resources/Localizations/de.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/de.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Sprachnachricht aufnehmen."; "a11y_voice_message_stop_recording" = "Aufnahme beenden"; "action_accept" = "Akzeptieren"; +"action_add_caption" = "Bildunterschrift hinzufügen"; "action_add_to_timeline" = "Zum Nachrichtenverlauf hinzufügen"; "action_back" = "Zurück"; "action_call" = "Anruf"; @@ -32,21 +33,24 @@ "action_close" = "Schließen"; "action_complete_verification" = "Verifizierung abschließen"; "action_confirm" = "Bestätigen"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Passwort bestätigen"; "action_continue" = "Weiter"; "action_copy" = "Kopieren"; +"action_copy_caption" = "Bildunterschrift kopieren"; "action_copy_link" = "Link kopieren"; "action_copy_link_to_message" = "Link zur Nachricht kopieren"; +"action_copy_text" = "Text kopieren"; "action_create" = "Erstellen"; "action_create_a_room" = "Raum erstellen"; -"action_deactivate" = "Deactivate"; -"action_deactivate_account" = "Deactivate account"; +"action_deactivate" = "Deaktivieren"; +"action_deactivate_account" = "Benutzerkonto deaktivieren"; "action_decline" = "Ablehnen"; "action_delete_poll" = "Umfrage löschen"; "action_disable" = "Deaktivieren"; "action_discard" = "Verwerfen"; "action_done" = "Erledigt"; "action_edit" = "Bearbeiten"; +"action_edit_caption" = "Bildunterschrift bearbeiten"; "action_edit_poll" = "Umfrage bearbeiten"; "action_enable" = "Aktivieren"; "action_end_poll" = "Umfrage beenden"; @@ -54,7 +58,7 @@ "action_forgot_password" = "Passwort vergessen?"; "action_forward" = "Weiterleiten"; "action_go_back" = "Zurück"; -"action_ignore" = "Ignore"; +"action_ignore" = "Ignorieren"; "action_invite" = "Einladen"; "action_invite_friends" = "Personen einladen"; "action_invite_friends_to_app" = "Zu %1$@ einladen"; @@ -81,6 +85,8 @@ "action_react" = "Reagieren"; "action_reject" = "Ablehnen"; "action_remove" = "Entfernen"; +"action_remove_caption" = "Bildunterschrift entfernen"; +"action_remove_message" = "Nachricht löschen"; "action_reply" = "Antworten"; "action_reply_in_thread" = "Im Thread antworten"; "action_report_bug" = "Fehler melden"; @@ -95,7 +101,7 @@ "action_send_message" = "Nachricht senden"; "action_share" = "Teilen"; "action_share_link" = "Link teilen"; -"action_show" = "Show"; +"action_show" = "Zeige"; "action_sign_in_again" = "Erneut anmelden"; "action_signout" = "Abmelden"; "action_signout_anyway" = "Trotzdem abmelden"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Wiederherstellung einrichten"; "common_about" = "Über"; "common_acceptable_use_policy" = "Nutzungsrichtlinie"; +"common_adding_caption" = "Hinzufügen einer Bildunterschrift"; "common_advanced_settings" = "Erweiterte Einstellungen"; "common_analytics" = "Analysedaten"; "common_appearance" = "Erscheinungsbild"; "common_audio" = "Audio"; "common_blocked_users" = "Blockierte Nutzer"; "common_bubbles" = "Sprechblasen"; -"common_call_invite" = "Aktiver Anruf (nicht unterstützt)"; "common_call_started" = "Aufruf gestartet"; "common_chat_backup" = "Chat-Backup"; "common_copyright" = "Copyright"; @@ -134,12 +140,13 @@ "common_dark" = "Dunkel"; "common_decryption_error" = "Dekodierungsfehler"; "common_developer_options" = "Entwickleroptionen"; -"common_device_id" = "Device ID"; +"common_device_id" = "Geräte-ID"; "common_direct_chat" = "Direktnachricht"; "common_edited_suffix" = "(bearbeitet)"; "common_editing" = "Bearbeitung"; +"common_editing_caption" = "Bearbeitung der Bildunterschrift"; "common_emote" = "* %1$@ %2$@"; -"common_encryption" = "Encryption"; +"common_encryption" = "Verschlüsselung"; "common_encryption_enabled" = "Verschlüsselung aktiviert"; "common_enter_your_pin" = "PIN eingeben"; "common_error" = "Fehler"; @@ -150,7 +157,7 @@ "common_favourited" = "Favorit"; "common_file" = "Datei"; "common_forward_message" = "Nachricht weiterleiten"; -"common_frequently_used" = "Frequently used"; +"common_frequently_used" = "Häufig verwendet"; "common_gif" = "GIF"; "common_image" = "Bild"; "common_in_reply_to" = "Als Antwort auf %1$@"; @@ -226,33 +233,34 @@ "common_unable_to_invite_title" = "Einladung(en) konnte(n) nicht gesendet werden"; "common_unlock" = "Entsperren"; "common_unmute" = "Stummschaltung aufheben"; +"common_unsupported_call" = "Anruf nicht unterstützt"; "common_unsupported_event" = "Nicht unterstütztes Ereignis"; "common_username" = "Benutzername"; "common_verification_cancelled" = "Verifizierung abgebrochen"; "common_verification_complete" = "Verifizierung abgeschlossen"; -"common_verification_failed" = "Verification failed"; -"common_verified" = "Verified"; +"common_verification_failed" = "Verifizierung fehlgeschlagen"; +"common_verified" = "Verifiziert"; "common_verify_device" = "Gerät verifizieren"; -"common_verify_identity" = "Verify identity"; +"common_verify_identity" = "Identität überprüfen"; "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.copied_to_clipboard" = "In die Zwischenablage kopiert"; "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.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."; +"common.you" = "Sie"; +"common_unable_to_decrypt_insecure_device" = "Von einem ungesicherten Gerät gesendet"; +"common_unable_to_decrypt_verification_violation" = "Die verifizierte Identität des Senders hat sich geändert"; +"confirm_recovery_key_banner_message" = "Bestätigen Sie die Validität Ihres Wiederherstellungsschlüssels, um weiterhin auf Ihren Schlüsselspeicher und den Nachrichtenverlauf zugreifen zu können."; +"confirm_recovery_key_banner_primary_button_title" = "Geben Sie Ihren Wiederherstellungsschlüssel ein"; +"confirm_recovery_key_banner_secondary_button_title" = "Haben Sie Ihren Wiederherstellungsschlüssel vergessen?"; +"confirm_recovery_key_banner_title" = "Ihr Schlüsselspeicher ist nicht synchronisiert"; "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" = "%1$@'s Identität has sich offenbar geändert. %2$@"; +"crypto_identity_change_pin_violation_new" = "%1$@'s %2$@ Identität hat sich offenbar geändert. %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."; @@ -307,12 +315,12 @@ "notification_incoming_call" = "Eingehender Anruf"; "notification_inline_reply_failed" = "** Fehler beim Senden - bitte Raum öffnen"; "notification_invite_body" = "Du wurdest zu einem Chat eingeladen"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; +"notification_invite_body_with_sender" = "%1$@ hat dich zum Chatten eingeladen"; "notification_mentioned_you_body" = "Hat Dich erwähnt: %1$@"; "notification_new_messages" = "Neue Nachrichten"; "notification_reaction_body" = "Reagiert mit %1$@"; "notification_room_invite_body" = "Du wurdest eingeladen, den Raum zu betreten"; -"notification_room_invite_body_with_sender" = "%1$@ invited you to join the room"; +"notification_room_invite_body_with_sender" = "%1$@ hat dich eingeladen, dem Chatroom beizutreten"; "notification_sender_me" = "Ich"; "notification_sender_mention_reply" = "%1$@ hat Dich erwähnt oder geantwortet"; "notification_test_push_notification_content" = "Du siehst dir die Benachrichtigung an! Klicke hier!"; @@ -344,25 +352,44 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Optionale Bildunterschrift..."; "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_create_room_room_access_section_anyone_option_description" = "Jeder kann diesem Chatroom beitreten"; +"screen_create_room_room_access_section_anyone_option_title" = "Jemand"; +"screen_create_room_room_access_section_header" = "Chatroom Zugang"; +"screen_create_room_room_access_section_knocking_option_description" = "Jeder kann darum bitten, dem Chatroom beizutreten, aber ein Administrator oder ein Moderator muss die Anfrage akzeptieren."; +"screen_create_room_room_access_section_knocking_option_title" = "Beitritt beantragen"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Einige Zeichen sind nicht erlaubt. Es werden nur Buchstaben, Ziffern und die folgenden Symbole unterstützt: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Diese Chatroomadresse existiert bereits. Bitte versuchen Sie, das Adressenfeld des Chatrooms zu bearbeiten oder den Namen des Chatrooms zu ändern"; +"screen_create_room_room_address_section_footer" = "Damit dieser Chatroom im öffentlichen Chatroomverzeichnis sichtbar ist, benötigen Sie eine Chatroomadresse."; +"screen_create_room_room_address_section_title" = "Chatroom Adresse"; +"screen_create_room_room_visibility_section_title" = " Sichtbarkeit des Chatrooms"; +"screen_join_room_cancel_knock_action" = "Anfrage abbrechen"; +"screen_join_room_cancel_knock_alert_confirmation" = "Ja, abbrechen"; +"screen_join_room_cancel_knock_alert_description" = "Möchten Sie Ihre Beitrittsanfrage für diesen Chatroom wirklich stornieren?"; +"screen_join_room_cancel_knock_alert_title" = "Beitrittsanfrage stornieren"; +"screen_join_room_knock_message_description" = "Nachricht (optional)"; +"screen_join_room_knock_sent_description" = "Falls Ihre Anfrage, dem Raum beizutreten, akzeptiert wird, werden Sie eine Einladung erhalten."; +"screen_join_room_knock_sent_title" = "Beitrittsanfrage geschickt"; +"screen_knock_requests_list_accept_all_alert_confirm_button_title" = "Ja, akzeptiere alle"; +"screen_knock_requests_list_accept_all_alert_description" = "Sind Sie sicher, dass Sie alle Beitrittsanfragen akzeptieren möchten?"; +"screen_knock_requests_list_accept_all_alert_title" = "Akzeptiere alle Anfragen"; +"screen_knock_requests_list_accept_all_button_title" = "Alle akzeptieren"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Ja, ablehnen und sperren"; +"screen_knock_requests_list_ban_alert_description" = "Sind Sie sicher, dass Sie %1$@ ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern."; +"screen_knock_requests_list_ban_alert_title" = "Ablehnen und Zugriff verbieten"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Ja, ablehnen"; +"screen_knock_requests_list_decline_alert_description" = "Sind Sie sicher, dass Sie die %1$@ Anfrage, diesem Chatroom beizutreten, ablehnen möchten ?"; +"screen_knock_requests_list_decline_alert_title" = "Zugriff verweigern"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Ablehnen und sperren"; +"screen_knock_requests_list_empty_state_description" = "Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen."; +"screen_knock_requests_list_empty_state_title" = "Keine ausstehende Beitrittsanfrage"; +"screen_knock_requests_list_title" = "Beitrittsanfragen"; +"screen_media_upload_preview_caption_warning" = "Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."; +"screen_media_upload_preview_error_failed_processing" = "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."; +"screen_media_upload_preview_error_failed_sending" = "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."; "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_reset_encryption_password_error" = "Es ist ein unbekannter Fehler aufgetreten. Bitte überprüfe das Passwort deines Kontos und versuche es erneut."; @@ -372,18 +399,23 @@ "screen_resolve_send_failure_unsigned_device_primary_button_title" = "Nachricht trotzdem senden"; "screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ verwendet wenigstens ein nicht verifiziertes Gerät. Du kannst die Nachricht trotzdem verschicken, oder vorerst abbrechen und später erneut versuchen, nachdem %2$@ alle Geräte verifiziert hat."; "screen_resolve_send_failure_unsigned_device_title" = "Deine Nachricht wurde nicht gesendet, weil %1$@ nicht alle Geräte verifiziert hat"; -"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" = "Mindestens eines Ihrer Geräte ist nicht verifiziert worden. Sie können die Nachricht trotzdem senden, oder den Vorgang zunächst abbrechen und es später erneut versuchen, nachdem Sie alle Ihrer Geräte verifiziert haben."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Ihre Nachricht wurde nicht geschickt, da Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben."; "screen_room_mentions_at_room_subtitle" = "Alle Mitglieder benachrichtigen"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Alles ansehen"; "screen_room_pinned_banner_indicator" = "%1$@ von %2$@"; "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_single_knock_request_accept_button_title" = "Akzeptieren"; +"screen_room_single_knock_request_title" = "%1$@ möchte diesem Chatroom beitreten"; +"screen_room_single_knock_request_view_button_title" = "Ansicht"; "screen_room_details_pinned_events_row_title" = "Fixierte Nachrichten"; -"screen_roomlist_knock_event_sent_description" = "Request to join sent"; +"screen_room_details_requests_to_join_title" = "Beitrittsanfragen"; +"screen_roomlist_knock_event_sent_description" = "Beitrittsanfrage gesendet"; "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_timeline_item_menu_send_failure_you_unsigned_device" = "Die Nachricht wurde nicht gesendet, weil Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben."; "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."; @@ -392,8 +424,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_media_compression_description" = "Laden Sie Fotos und Videos schneller hoch und reduzieren Sie die Datennutzung"; +"screen_advanced_settings_media_compression_title" = "Optimieren Sie die Medienqualität"; "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."; @@ -460,14 +492,14 @@ "screen_change_server_title" = "Wähle deinen Server aus"; "screen_chat_backup_key_backup_action_disable" = "Backup deaktivieren"; "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_key_backup_description" = "Speichern Sie Ihre verschlüsselte Identität und Ihre codierten Nachrichtenschlüssel auf dem Server. Auf diese Weise können Sie Ihren Nachrichtenverlauf auf allen neuen Geräten einsehen. %1$@."; +"screen_chat_backup_key_backup_title" = "Schlüsselspeicher"; +"screen_chat_backup_key_storage_disabled_error" = "Der Schlüsselspeicher muss aktiviert sein, um Datenwiederherstellung zu ermöglichen."; +"screen_chat_backup_key_storage_toggle_description" = "Schlüssel von diesem Gerät hochladen"; +"screen_chat_backup_key_storage_toggle_title" = "Schlüsselspeicherung zulassen"; "screen_chat_backup_recovery_action_change" = "Wiederherstellungsschlüssel ändern"; -"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_change_description" = "Stellen Sie Ihre verschlüsselte Identität und Ihren Nachrichtenverlauf mit einem Wiederherstellungsschlüssel wieder her, falls Sie den Zugang zu allen Ihren Geräten verloren haben."; +"screen_chat_backup_recovery_action_confirm_description" = "Dein Schlüssel ist derzeit nicht synchronisiert."; "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"; @@ -487,22 +519,22 @@ "screen_create_poll_title" = "Umfrage erstellen"; "screen_create_room_action_create_room" = "Neuer Raum"; "screen_create_room_error_creating_room" = "Beim Erstellen des Chats ist ein Fehler aufgetreten"; -"screen_create_room_private_option_description" = "Die Nachrichten in diesem Chat sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."; -"screen_create_room_private_option_title" = "Privater Raum (nur auf Einladung)"; -"screen_create_room_public_option_description" = "Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."; -"screen_create_room_public_option_title" = "Öffentlicher Raum (für alle)"; +"screen_create_room_private_option_description" = "Nur eingeladene Personen haben Zutritt zu diesem Chatroom. Alle Nachrichten sind durchgehend verschlüsselt."; +"screen_create_room_private_option_title" = "Privater Chatroom"; +"screen_create_room_public_option_description" = "Jeder kann diesen Chatroom finden.\nSie können dies aber jederzeit in den Chatroomeinstellungen ändern."; +"screen_create_room_public_option_title" = "Öffentlicher Chatroom"; "screen_create_room_topic_label" = "Thema (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"; -"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" = "Bitte bestätigen Sie, dass Sie Ihr Benutzerkonto deaktivieren möchten. Diese Aktion kann nicht rückgängig gemacht werden."; +"screen_deactivate_account_delete_all_messages" = "Lösche alle meine Nachrichten"; +"screen_deactivate_account_delete_all_messages_notice" = "Warnung: Benutzern werden möglicherweise unvollständige Konversationen angezeigt."; +"screen_deactivate_account_description" = "Wenn Sie Ihr Konto deaktivieren%1$@, wird es:"; +"screen_deactivate_account_description_bold_part" = "irreversibel"; +"screen_deactivate_account_list_item_1" = "%1$@ Ihr Konto (Sie können sich nicht erneut anmelden und Ihre ID kann nicht wiederverwendet werden)."; +"screen_deactivate_account_list_item_1_bold_part" = "Dauerhaft deaktivieren"; +"screen_deactivate_account_list_item_2" = "Sie werden aus allen Chatrooms entfernt."; +"screen_deactivate_account_list_item_3" = "Löschen Sie Ihre Kontoinformationen von unserem Identitätsserver."; +"screen_deactivate_account_list_item_4" = "Gelöschte Nachrichten werden für bereits registrierte Benutzer weiterhin sichtbar sein, wenn sie auch neuen oder nicht registrierten Benutzern nicht mehr zur Verfügung stehen."; +"screen_deactivate_account_title" = "Benutzerkonto deaktivieren"; "screen_edit_poll_delete_confirmation" = "Bist du dir sicher, dass du diese Umfrage löschen möchtest?"; "screen_edit_profile_display_name" = "Anzeigename"; "screen_edit_profile_display_name_placeholder" = "Dein Anzeigename"; @@ -514,9 +546,9 @@ "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?"; +"screen_encryption_reset_footer" = "Setzen Sie Ihre Identität nur dann zurück, wenn Sie keinen Zugriff auf ein anderes Ihrer angemeldeten Geräte und auch Ihren Wiederherstellungsschlüssel verloren haben."; +"screen_encryption_reset_title" = "Sie können es nicht bestätigen? Dann müssen Sie Ihre Identität zurücksetzen."; +"screen_identity_confirmation_cannot_confirm" = "Sie können es nicht bestätigen?"; "screen_identity_confirmation_create_new_recovery_key" = "Erstelle einen neuen Wiederherstellungsschlüssel"; "screen_identity_confirmation_subtitle" = "Verifiziere dieses Gerät, um sicheres Messaging einzurichten."; "screen_identity_confirmation_title" = "Bestätige, dass du es bist"; @@ -555,8 +587,6 @@ "screen_login_title" = "Willkommen zurück!"; "screen_login_title_with_homeserver" = "Anmelden bei %1$@"; "screen_media_picker_error_failed_selection" = "Medienauswahl fehlgeschlagen, bitte versuche es erneut."; -"screen_media_upload_preview_error_failed_processing" = "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."; -"screen_media_upload_preview_error_failed_sending" = "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."; "screen_migration_message" = "Dies ist ein einmaliger Vorgang, danke fürs Warten."; "screen_migration_title" = "Dein Konto wird eingerichtet."; "screen_notification_optin_subtitle" = "Du kannst deine Einstellungen später ändern."; @@ -623,7 +653,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_subtitle" = "Nur verfügbar für den Fall dass Ihr Kontoanbieter dies unterstützt."; "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"; @@ -642,7 +672,7 @@ "screen_recovery_key_change_title" = "Wiederherstellungsschlüssel ändern?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Neuen Wiederherstellungsschlüssel erstellen"; "screen_recovery_key_confirm_description" = "Sorge dafür, dass niemand diesen Bildschirm sehen kann!"; -"screen_recovery_key_confirm_error_content" = "Bitte versuche es noch einmal, um den Zugriff auf dein Chat-Backup zu bestätigen."; +"screen_recovery_key_confirm_error_content" = "Bitte versuchen Sie erneut, den Zugriff auf Ihren Schlüsselspeicher zu bestätigen."; "screen_recovery_key_confirm_error_title" = "Falscher Wiederherstellungsschlüssel"; "screen_recovery_key_confirm_key_description" = "Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase."; "screen_recovery_key_confirm_key_placeholder" = "Eingeben..."; @@ -651,14 +681,14 @@ "screen_recovery_key_copied_to_clipboard" = "Wiederherstellungsschlüssel kopiert"; "screen_recovery_key_generating_key" = "Generieren…"; "screen_recovery_key_save_action" = "Wiederherstellungsschlüssel speichern"; -"screen_recovery_key_save_description" = "Notiere dir deinen Wiederherstellungsschlüssel an einem sicheren Ort oder speichere ihn in einem Passwort-Manager."; +"screen_recovery_key_save_description" = "Schreiben Sie Ihren Wiederherstellungsschlüssel in eine verschlüsselte Datei, oder in einem Passwort-Manager oder in einem Safe. "; "screen_recovery_key_save_key_description" = "Tippe, um den Wiederherstellungsschlüssel zu kopieren"; "screen_recovery_key_save_title" = "Speichere deinen Wiederherstellungsschlüssel"; "screen_recovery_key_setup_confirmation_description" = "Nach diesem Schritt kannst du nicht mehr auf deinen neuen Wiederherstellungsschlüssel zugreifen."; "screen_recovery_key_setup_confirmation_title" = "Hast du deinen Wiederherstellungsschlüssel gespeichert?"; "screen_recovery_key_setup_description" = "Dein Chat-Backup ist durch einen Wiederherstellungsschlüssel geschützt. Wenn du nach der Einrichtung einen neuen Wiederherstellungsschlüssel brauchst, kannst du ihn über die Option \"Wiederherstellungsschlüssel ändern\" neu erstellen."; "screen_recovery_key_setup_generate_key" = "Wiederherstellungsschlüssel erstellen"; -"screen_recovery_key_setup_generate_key_description" = "Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"; +"screen_recovery_key_setup_generate_key_description" = "Geben Sie dies an niemanden weiter!"; "screen_recovery_key_setup_success" = "Einrichtung der Wiederherstellung erfolgreich"; "screen_recovery_key_setup_title" = "Wiederherstellung einrichten"; "screen_report_content_block_user_hint" = "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"; @@ -732,8 +762,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_details_verify_button_subtitle" = "Verwenden Sie die Web-App, um diesen Benutzer zu verifizieren."; +"screen_room_member_details_verify_button_title" = "Überprüfen Sie %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?"; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Emoji hinzufügen"; "screen_room_timeline_beginning_of_room" = "Dies ist der Anfang von %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Dies ist der Anfang dieses Gesprächs."; +"screen_room_timeline_legacy_call" = "Anruftyp wird nicht unterstützt. Fragen Sie nach, ob der Anrufer die neue Element X-App verwenden kann."; "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"; @@ -830,7 +861,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_failed_subtitle" = "Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung."; "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"; @@ -838,15 +869,19 @@ "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_request_details_timestamp" = "Angemeldet"; +"screen_session_verification_request_failure_title" = "Überprüfung fehlgeschlagen"; +"screen_session_verification_request_footer" = "Fahren Sie nur fort, falls Sie für diese Überprüfung verantwortlich sind.."; +"screen_session_verification_request_subtitle" = "Verifizieren Sie das andere Gerät, um die Sicherheit Ihres Nachrichtenverlaufs zu gewährleisten."; +"screen_session_verification_request_success_subtitle" = "Jetzt können Sie gesichert Nachrichten auf Ihrem anderen Gerät lesen oder senden."; +"screen_session_verification_request_success_title" = "Gerät verifiziert"; +"screen_session_verification_request_title" = "Verifizierung angefordert"; "screen_session_verification_they_dont_match" = "Sie stimmen nicht überein"; "screen_session_verification_they_match" = "Sie stimmen überein"; +"screen_session_verification_use_another_device_subtitle" = "Stellen Sie sicher, dass die App auf dem anderen Gerät geöffnet ist, bevor Sie die Überprüfung auf diesem Gerät aus starten."; +"screen_session_verification_use_another_device_title" = "Öffnen Sie die App auf einem anderen verifizierten Gerät"; +"screen_session_verification_waiting_another_device_subtitle" = "Sie sollten ein Popup-Fenster auf dem anderen Gerät sehen. Starten Sie die Überprüfung von dort aus."; +"screen_session_verification_waiting_another_device_title" = "Starten Sie die Überprüfung auf dem anderen Gerät"; "screen_session_verification_waiting_to_accept_subtitle" = "Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren."; "screen_session_verification_waiting_to_accept_title" = "Warten auf die Annahme der Anfrage"; "screen_share_location_title" = "Standort teilen"; @@ -911,7 +946,7 @@ "state_event_room_invite_you" = "%1$@ hat dich eingeladen"; "state_event_room_join" = "%1$@ hat den Raum betreten"; "state_event_room_join_by_you" = "Du hast den Raum betreten"; -"state_event_room_knock" = "%1$@ hat angefragt beizutreten"; +"state_event_room_knock" = "%1$@ beantragt den Beitritt"; "state_event_room_knock_accepted" = "%1$@ hat %2$@ den Beitritt erlaubt"; "state_event_room_knock_accepted_by_you" = "Du hast %1$@ den Beitritt erlaubt."; "state_event_room_knock_by_you" = "Du hast angefragt beizutreten"; @@ -1032,8 +1067,8 @@ "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_recovery_key_change_generate_key_description" = "Geben Sie dies an niemanden weiter!"; +"screen_recovery_key_confirm_title" = "Geben Sie Ihren Wiederherstellungsschlüssel ein"; "screen_report_content_block_user" = "Benutzer blockieren"; "screen_reset_encryption_password_placeholder" = "Eingeben..."; "screen_room_attachment_source_camera_photo" = "Foto aufnehmen"; @@ -1059,7 +1094,7 @@ "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_session_verification_request_failure_subtitle" = "Entweder ist bei der Anfrage ein Timeout aufgetreten, oder die Anfrage wurde abgelehnt, oder es gab eine Nichtübereinstimmung bei der Überprüfung."; "screen_signout_confirmation_dialog_submit" = "Abmelden"; "screen_signout_confirmation_dialog_title" = "Abmelden"; "screen_signout_key_backup_offline_title" = "Deine Schlüssel werden noch gesichert"; diff --git a/ElementX/Resources/Localizations/de.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/de.lproj/Localizable.stringsdict index a0b5aa8da7..6ce7634c6b 100644 --- a/ElementX/Resources/Localizations/de.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/de.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d Personen + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@+ %2$d andere wollen diesem Chatroom beitreten + other + %1$@+ %2$d andere wollen diesem Chatroom beitreten + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/el.lproj/Localizable.strings b/ElementX/Resources/Localizations/el.lproj/Localizable.strings index 9c73cfbee0..4c56b70a3c 100644 --- a/ElementX/Resources/Localizations/el.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/el.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Εγγραφή φωνητικού μηνύματος."; "a11y_voice_message_stop_recording" = "Διακοπή καταγραφής"; "action_accept" = "Αποδοχή"; +"action_add_caption" = "Προσθήκη λεζάντας"; "action_add_to_timeline" = "Προσθήκη στο χρονοδιάγραμμα"; "action_back" = "Πίσω"; "action_call" = "Κάλεσε"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Επιβεβαίωση κωδικού πρόσβασης"; "action_continue" = "Συνέχεια"; "action_copy" = "Αντιγραφή"; +"action_copy_caption" = "Αντιγραφή λεζάντας"; "action_copy_link" = "Αντιγραφή συνδέσμου"; "action_copy_link_to_message" = "Αντιγραφή συνδέσμου στο μήνυμα"; +"action_copy_text" = "Αντιγραφή κειμένου"; "action_create" = "Δημιουργία"; "action_create_a_room" = "Δημιούργησε ένα δωμάτιο"; "action_deactivate" = "Απενεργοποίηση"; @@ -47,6 +50,7 @@ "action_discard" = "Απόρριψη"; "action_done" = "Έγινε"; "action_edit" = "Επεξεργασία"; +"action_edit_caption" = "Επεξεργασία λεζάντας"; "action_edit_poll" = "Επεξεργασία δημοσκόπησης"; "action_enable" = "Ενεργοποίηση"; "action_end_poll" = "Λήξη δημοσκόπησης"; @@ -81,6 +85,8 @@ "action_react" = "Αντέδρασε"; "action_reject" = "Απόρριψη"; "action_remove" = "Αφαίρεση"; +"action_remove_caption" = "Αφαίρεση λεζάντας"; +"action_remove_message" = "Αφαίρεση μηνύματος"; "action_reply" = "Απάντηση"; "action_reply_in_thread" = "Απάντηση στο θέμα"; "action_report_bug" = "Αναφορά σφάλματος"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Ρύθμιση ανάκτησης"; "common_about" = "Σχετικά"; "common_acceptable_use_policy" = "Πολιτική αποδεκτής χρήσης"; +"common_adding_caption" = "Η λεζάντα προστίθεται"; "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" = "Πνευματικά δικαιώματα"; @@ -138,8 +144,9 @@ "common_direct_chat" = "Άμεση συνομιλία"; "common_edited_suffix" = "(επεξεργάστηκε)"; "common_editing" = "Επεξεργάζεται"; +"common_editing_caption" = "Η λεζάντα επεξεργάζεται"; "common_emote" = "* %1$@ %2$@"; -"common_encryption" = "Encryption"; +"common_encryption" = "Κρυπτογράφηση"; "common_encryption_enabled" = "Η κρυπτογράφηση ενεργοποιήθηκε"; "common_enter_your_pin" = "Εισήγαγε το PIN σου"; "common_error" = "Σφάλμα"; @@ -150,7 +157,7 @@ "common_favourited" = "Είναι αγαπημένο"; "common_file" = "Αρχείο"; "common_forward_message" = "Προώθηση μηνύματος"; -"common_frequently_used" = "Frequently used"; +"common_frequently_used" = "Χρησιμοποιείται συχνά"; "common_gif" = "GIF"; "common_image" = "Εικόνα"; "common_in_reply_to" = "Σε απάντηση στον χρήστη %1$@"; @@ -226,14 +233,15 @@ "common_unable_to_invite_title" = "Δεν είναι δυνατή η αποστολή προσκλήσεων"; "common_unlock" = "Ξεκλείδωμα"; "common_unmute" = "Άρση σίγασης"; +"common_unsupported_call" = "Μη υποστηριζόμενη κλήση"; "common_unsupported_event" = "Μη υποστηριζόμενο συμβάν"; "common_username" = "Όνομα χρήστη"; "common_verification_cancelled" = "Η επαλήθευση ακυρώθηκε"; "common_verification_complete" = "Η επαλήθευση ολοκληρώθηκε"; -"common_verification_failed" = "Verification failed"; +"common_verification_failed" = "Αποτυχία επαλήθευσης"; "common_verified" = "Επαληθεύτηκε"; "common_verify_device" = "Επαλήθευση συσκευής"; -"common_verify_identity" = "Verify identity"; +"common_verify_identity" = "Επαλήθευση ταυτότητας"; "common_video" = "Βίντεο"; "common_voice_message" = "Φωνητικό μήνυμα"; "common_waiting" = "Αναμονή…"; @@ -244,12 +252,12 @@ "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_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" = "Εισήγαγε το κλειδί ανάκτησης"; +"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$@"; @@ -344,25 +352,44 @@ "rich_text_editor_unindent" = "Χωρίς εσοχή"; "rich_text_editor_url_placeholder" = "Σύνδεσμος"; "rich_text_editor_a11y_add_attachment" = "Προσθήκη συνημμένου"; -"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; +"rich_text_editor_composer_caption_placeholder" = "Προαιρετική λεζάντα..."; "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_create_room_room_access_section_anyone_option_description" = "Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο"; +"screen_create_room_room_access_section_anyone_option_title" = "Οποιοσδήποτε"; +"screen_create_room_room_access_section_header" = "Πρόσβαση Δωματίου"; +"screen_create_room_room_access_section_knocking_option_description" = "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"; +"screen_create_room_room_access_section_knocking_option_title" = "Αίτημα συμμετοχής"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Ορισμένοι χαρακτήρες δεν επιτρέπονται. Υποστηρίζονται μόνο γράμματα, ψηφία και τα ακόλουθα σύμβολα ! $ & '() * +/; = ? @ [] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Αυτή η διεύθυνση δωματίου υπάρχει ήδη, δοκίμασε να επεξεργαστείς το πεδίο διεύθυνσης δωματίου ή να αλλάξεις το όνομα δωματίου"; +"screen_create_room_room_address_section_footer" = "Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου."; +"screen_create_room_room_address_section_title" = "Διεύθυνση δωματίου"; +"screen_create_room_room_visibility_section_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_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_knock_requests_list_accept_all_alert_confirm_button_title" = "Ναι, αποδοχή όλων"; +"screen_knock_requests_list_accept_all_alert_description" = "Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"; +"screen_knock_requests_list_accept_all_alert_title" = "Αποδοχή όλων των αιτημάτων"; +"screen_knock_requests_list_accept_all_button_title" = "Αποδοχή όλων"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Ναι, απόρριψη και αποκλεισμός"; +"screen_knock_requests_list_ban_alert_description" = "Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$@; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."; +"screen_knock_requests_list_ban_alert_title" = "Απόρριψη και αποκλεισμός πρόσβασης"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Ναι, απόρριψη"; +"screen_knock_requests_list_decline_alert_description" = "Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$@ να συμμετάσχει στο δωμάτιο;"; +"screen_knock_requests_list_decline_alert_title" = "Απόρριψη πρόσβασης"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Απόρριψη και αποκλεισμός"; +"screen_knock_requests_list_empty_state_description" = "Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."; +"screen_knock_requests_list_empty_state_title" = "Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"; +"screen_knock_requests_list_title" = "Αιτήματα συμμετοχής"; +"screen_media_upload_preview_caption_warning" = "Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."; +"screen_media_upload_preview_error_failed_processing" = "Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."; +"screen_media_upload_preview_error_failed_sending" = "Αποτυχία μεταφόρτωσης πολυμέσων, δοκίμασε ξανά."; "screen_pinned_timeline_empty_state_description" = "Πάτα σε ένα μήνυμα και επέλεξε «%1$@» για να συμπεριληφθεί εδώ."; "screen_pinned_timeline_empty_state_headline" = "Καρφίτσωσε σημαντικά μηνύματα, ώστε να μπορούν να εντοπιστούν εύκολα"; "screen_reset_encryption_password_error" = "Συνέβη ένα άγνωστο σφάλμα. Έλεγξε ότι ο κωδικός πρόσβασης του λογαριασμού σου είναι σωστός και δοκίμασε ξανά."; @@ -375,12 +402,17 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Μία ή περισσότερες από τις συσκευές σου δεν έχουν επαληθευτεί. Μπορείς να στείλεις το μήνυμα ούτως ή άλλως, ή μπορείς να το ακυρώσεις προς το παρόν και να προσπαθήσεις ξανά αργότερα αφού επαληθεύσεις όλες τις συσκευές σου."; "screen_resolve_send_failure_you_unsigned_device_title" = "Το μήνυμά σου δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου"; "screen_room_mentions_at_room_subtitle" = "Ειδοποίησε όλο το δωμάτιο"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Προβολή όλων"; "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_single_knock_request_accept_button_title" = "Αποδοχή"; +"screen_room_single_knock_request_title" = "Ο χρήστης %1$@ θέλει να μπει σε αυτό το δωμάτιο"; +"screen_room_single_knock_request_view_button_title" = "Προβολή"; "screen_room_details_pinned_events_row_title" = "Καρφιτσωμένα μηνύματα"; -"screen_roomlist_knock_event_sent_description" = "Request to join sent"; +"screen_room_details_requests_to_join_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" = "Το μήνυμα δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου."; @@ -392,8 +424,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_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" = "Εάν απενεργοποιηθεί, τα αποδεικτικά ανάγνωσης δεν θα στέλνονται σε κανέναν. Θα εξακολουθείς να λαμβάνεις αποδεικτικά ανάγνωσης από άλλους χρήστες."; @@ -462,12 +494,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_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" = "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_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$@ σε συσκευή υπολογιστή"; @@ -555,8 +587,6 @@ "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" = "Μπορείς να αλλάξεις τις ρυθμίσεις σου αργότερα."; @@ -642,7 +672,7 @@ "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_content" = "Προσπάθησε ξανά για να επιβεβαιώσεις την πρόσβαση στον αποθηκευτικό χώρο κλειδιών σου."; "screen_recovery_key_confirm_error_title" = "Λανθασμένο κλειδί ανάκτησης"; "screen_recovery_key_confirm_key_description" = "Εάν έχεις ένα κλειδί ασφαλείας ή μια φράση ασφαλείας, θα λειτουργήσει επίσης."; "screen_recovery_key_confirm_key_placeholder" = "Εισαγωγή..."; @@ -732,8 +762,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_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" = "Θες σίγουρα να αποκλείσεις αυτό το μέλος;"; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Προσθήκη emoji"; "screen_room_timeline_beginning_of_room" = "Αυτή είναι η αρχή του %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Αυτή είναι η αρχή τούτης της συνομιλίας."; +"screen_room_timeline_legacy_call" = "Μη υποστηριζόμενη κλήση. Ρώτα εάν ο καλών μπορεί να χρησιμοποιήσει τη νέα εφαρμογή Element X."; "screen_room_timeline_less_reactions" = "Εμφάνιση λιγότερων"; "screen_room_timeline_message_copied" = "Το μήνυμα αντιγράφηκε"; "screen_room_timeline_no_permission_to_post" = "Δεν έχεις άδεια να δημοσιεύσεις σε αυτό το δωμάτιο"; @@ -830,7 +861,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_failed_subtitle" = "Είτε το αίτημα έληξε είτε απορρίφθηκε είτε υπήρξε αναντιστοιχία επαλήθευσης."; "screen_session_verification_open_existing_session_subtitle" = "Απέδειξε ότι είσαι εσύ για να αποκτήσεις πρόσβαση στο κρυπτογραφημένο ιστορικό μηνυμάτων σου."; "screen_session_verification_open_existing_session_title" = "Άνοιξε μια υπάρχουσα συνεδρία"; "screen_session_verification_positive_button_canceled" = "Επανάληψη επαλήθευσης"; @@ -838,15 +869,19 @@ "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_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_use_another_device_subtitle" = "Βεβαιώσου ότι έχεις ανοιχτή την εφαρμογή στην άλλη συσκευή πριν ξεκινήσεις την επαλήθευση από εδώ."; +"screen_session_verification_use_another_device_title" = "Άνοιξε την εφαρμογή σε άλλη επαληθευμένη συσκευή"; +"screen_session_verification_waiting_another_device_subtitle" = "Πρόκειται να δεις ένα αναδυόμενο παράθυρο στην άλλη συσκευή. Ξεκίνα την επαλήθευση από εκεί τώρα."; +"screen_session_verification_waiting_another_device_title" = "Έναρξη επαλήθευσης στην άλλη συσκευή"; "screen_session_verification_waiting_to_accept_subtitle" = "Αποδέξου το αίτημα για να ξεκινήσεις τη διαδικασία επαλήθευσης στην άλλη συνεδρία σου για να συνεχίσεις."; "screen_session_verification_waiting_to_accept_title" = "Αναμονή για αποδοχή αιτήματος"; "screen_share_location_title" = "Κοινή χρήση τοποθεσίας"; @@ -911,8 +946,8 @@ "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" = "Ο χρήστης %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" = "Ο χρήστης %1$@ απέρριψε το αίτημα του χρήστη %2$@ να συμμετάσχει"; @@ -1033,7 +1068,7 @@ "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_recovery_key_confirm_title" = "Εισήγαγε το κλειδί ανάκτησης"; "screen_report_content_block_user" = "Αποκλεισμός χρήστη"; "screen_reset_encryption_password_placeholder" = "Εισαγωγή..."; "screen_room_attachment_source_camera_photo" = "Τράβηξε φωτογραφία"; @@ -1059,7 +1094,7 @@ "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_session_verification_request_failure_subtitle" = "Είτε το αίτημα έληξε είτε απορρίφθηκε είτε υπήρξε αναντιστοιχία επαλήθευσης."; "screen_signout_confirmation_dialog_submit" = "Αποσύνδεση"; "screen_signout_confirmation_dialog_title" = "Αποσύνδεση"; "screen_signout_key_backup_offline_title" = "Εξακολουθούν να δημιουργούνται αντίγραφα ασφαλείας των κλειδιών σου"; diff --git a/ElementX/Resources/Localizations/el.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/el.lproj/Localizable.stringsdict index d17d98a85e..de66f82709 100644 --- a/ElementX/Resources/Localizations/el.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/el.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d άτομα + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Οι χρήστες %1$@ +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο + other + Οι χρήστες %1$@ +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 4200cde50f..9abc9ec077 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Record voice message."; "a11y_voice_message_stop_recording" = "Stop recording"; "action_accept" = "Accept"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Add to timeline"; "action_back" = "Back"; "action_call" = "Call"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Continue"; "action_copy" = "Copy"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copy link"; "action_copy_link_to_message" = "Copy link to message"; +"action_copy_text" = "Copy text"; "action_create" = "Create"; "action_create_a_room" = "Create a room"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Discard"; "action_done" = "Done"; "action_edit" = "Edit"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Edit poll"; "action_enable" = "Enable"; "action_end_poll" = "End poll"; @@ -81,6 +85,8 @@ "action_react" = "React"; "action_reject" = "Reject"; "action_remove" = "Remove"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Reply"; "action_reply_in_thread" = "Reply in thread"; "action_report_bug" = "Report bug"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "About"; "common_acceptable_use_policy" = "Acceptable use policy"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Advanced settings"; "common_analytics" = "Analytics"; "common_appearance" = "Appearance"; "common_audio" = "Audio"; "common_blocked_users" = "Blocked users"; "common_bubbles" = "Bubbles"; -"common_call_invite" = "Call in progress (unsupported)"; "common_call_started" = "Call started"; "common_chat_backup" = "Chat backup"; "common_copyright" = "Copyright"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Direct chat"; "common_edited_suffix" = "(edited)"; "common_editing" = "Editing"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryption enabled"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Unable to send invite(s)"; "common_unlock" = "Unlock"; "common_unmute" = "Unmute"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Unsupported event"; "common_username" = "Username"; "common_verification_cancelled" = "Verification cancelled"; @@ -344,18 +352,20 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Add a 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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; +"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; "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."; @@ -375,11 +402,16 @@ "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" = "Notify the whole room"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Welcome back!"; "screen_login_title_with_homeserver" = "Sign in to %1$@"; "screen_media_picker_error_failed_selection" = "Failed selecting media, please try again."; -"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; -"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; "screen_migration_message" = "This is a one time process, thanks for waiting."; "screen_migration_title" = "Setting up your account."; "screen_notification_optin_subtitle" = "You can change your settings later."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Add emoji"; "screen_room_timeline_beginning_of_room" = "This is the beginning of %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "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" = "Waiting to accept request"; "screen_share_location_title" = "Share location"; @@ -911,8 +946,8 @@ "state_event_room_invite_you" = "%1$@ invited you"; "state_event_room_join" = "%1$@ joined the room"; "state_event_room_join_by_you" = "You joined the room"; -"state_event_room_knock" = "%1$@ requested to join"; -"state_event_room_knock_accepted" = "%1$@ allowed %2$@ to join"; +"state_event_room_knock" = "%1$@ is requesting to join"; +"state_event_room_knock_accepted" = "%1$@ granted access to %2$@"; "state_event_room_knock_accepted_by_you" = "You allowed %1$@ to join"; "state_event_room_knock_by_you" = "You requested to join"; "state_event_room_knock_denied" = "%1$@ rejected %2$@'s request to join"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict index 60f8827b54..31f348283d 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d people + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/es.lproj/Localizable.strings b/ElementX/Resources/Localizations/es.lproj/Localizable.strings index ccbcbd2cdb..1ac660bde8 100644 --- a/ElementX/Resources/Localizations/es.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/es.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Grabar mensaje de voz"; "a11y_voice_message_stop_recording" = "Detener grabación"; "action_accept" = "Aceptar"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Añadir a la cronología"; "action_back" = "Atrás"; "action_call" = "Call"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Continuar"; "action_copy" = "Copiar"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copiar enlace"; "action_copy_link_to_message" = "Copiar enlace al mensaje"; +"action_copy_text" = "Copy text"; "action_create" = "Crear"; "action_create_a_room" = "Crear una sala"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Descartar"; "action_done" = "Hecho"; "action_edit" = "Editar"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Editar encuesta"; "action_enable" = "Activar"; "action_end_poll" = "Finalizar encuesta"; @@ -81,6 +85,8 @@ "action_react" = "Reaccionar"; "action_reject" = "Reject"; "action_remove" = "Eliminar"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Responder"; "action_reply_in_thread" = "Responder en el hilo"; "action_report_bug" = "Informar de un error"; @@ -119,13 +125,13 @@ "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_adding_caption" = "Adding caption"; "common_advanced_settings" = "Ajustes avanzados"; "common_analytics" = "Estadísticas"; "common_appearance" = "Apariencia"; "common_audio" = "Sonido"; "common_blocked_users" = "Usuarios bloqueados"; "common_bubbles" = "Burbujas"; -"common_call_invite" = "Llamada en curso (no admitida)"; "common_call_started" = "Call started"; "common_chat_backup" = "Copia de seguridad del chat"; "common_copyright" = "Derechos de autor"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Chat directo"; "common_edited_suffix" = "(editado)"; "common_editing" = "Edición"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Cifrado activado"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "No se pudo enviar la(s) invitación(es)"; "common_unlock" = "Desbloquear"; "common_unmute" = "Dejar de silenciar"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Evento no compatible"; "common_username" = "Usuario"; "common_verification_cancelled" = "Verificación cancelada"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Error al procesar el contenido multimedia, por favor inténtalo de nuevo."; +"screen_media_upload_preview_error_failed_sending" = "Error al subir el contenido multimedia, por favor inténtalo de nuevo."; "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."; @@ -375,11 +402,16 @@ "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" = "Notificar a toda la sala"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "¡Hola de nuevo!"; "screen_login_title_with_homeserver" = "Iniciar sesión en %1$@"; "screen_media_picker_error_failed_selection" = "Error al seleccionar archivos multimedia, por favor inténtalo de nuevo."; -"screen_media_upload_preview_error_failed_processing" = "Error al procesar el contenido multimedia, por favor inténtalo de nuevo."; -"screen_media_upload_preview_error_failed_sending" = "Error al subir el contenido multimedia, por favor inténtalo de nuevo."; "screen_migration_message" = "Este proceso solo se hace una vez, gracias por esperar."; "screen_migration_title" = "Configura tu cuenta"; "screen_notification_optin_subtitle" = "Puedes cambiar la configuración más tarde."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Añadir emoji"; "screen_room_timeline_beginning_of_room" = "Este es el principio de %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Este es el principio de esta conversación."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "No coinciden"; "screen_session_verification_they_match" = "Coinciden"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar."; "screen_session_verification_waiting_to_accept_title" = "A la espera de aceptar la solicitud"; "screen_share_location_title" = "Compartir ubicación"; diff --git a/ElementX/Resources/Localizations/es.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/es.lproj/Localizable.stringsdict index 738d81de9b..e57997a40c 100644 --- a/ElementX/Resources/Localizations/es.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/es.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d personas + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/et.lproj/Localizable.strings b/ElementX/Resources/Localizations/et.lproj/Localizable.strings index abf2da5bbd..ea7f3cd3f9 100644 --- a/ElementX/Resources/Localizations/et.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/et.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Salvesta häälsõnum"; "a11y_voice_message_stop_recording" = "Lõpeta salvestamine"; "action_accept" = "Nõustu"; +"action_add_caption" = "Lisa selgitus"; "action_add_to_timeline" = "Lisa ajajoonele"; "action_back" = "Tagasi"; "action_call" = "Helista"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Kinnita otsust oma salasõnaga"; "action_continue" = "Jätka"; "action_copy" = "Kopeeri"; +"action_copy_caption" = "Kopeeri selgitus"; "action_copy_link" = "Kopeeri link"; "action_copy_link_to_message" = "Kopeeri sõnumi link"; +"action_copy_text" = "Kopeeri tekst"; "action_create" = "Loo"; "action_create_a_room" = "Loo jututuba"; "action_deactivate" = "Eemalda konto"; @@ -47,6 +50,7 @@ "action_discard" = "Loobu"; "action_done" = "Valmis"; "action_edit" = "Muuda"; +"action_edit_caption" = "Muuda selgitust"; "action_edit_poll" = "Muuda küsitlust"; "action_enable" = "Võta kasutusele"; "action_end_poll" = "Lõpeta küsitlus"; @@ -81,6 +85,8 @@ "action_react" = "Reageeri"; "action_reject" = "Keeldu"; "action_remove" = "Eemalda"; +"action_remove_caption" = "Eemalda selgitus"; +"action_remove_message" = "Eemalda sõnum"; "action_reply" = "Vasta"; "action_reply_in_thread" = "Vasta jutulõngas"; "action_report_bug" = "Teata veast"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Seadista taastamine"; "common_about" = "Rakenduse teave"; "common_acceptable_use_policy" = "Vastuvõetava kasutamise põhimõtted"; +"common_adding_caption" = "Lisame selgitust"; "common_advanced_settings" = "Täiendavad seadistused"; "common_analytics" = "Analüütika"; "common_appearance" = "Välimus"; "common_audio" = "Heli"; "common_blocked_users" = "Blokeeritud kasutajad"; "common_bubbles" = "Mullid"; -"common_call_invite" = "Kõne on pooleli (pole toetatud)"; "common_call_started" = "Kõne algas"; "common_chat_backup" = "Vestluse varukoopia"; "common_copyright" = "Autoriõigused"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Otsevestlus"; "common_edited_suffix" = "(muudetud)"; "common_editing" = "Muutmine"; +"common_editing_caption" = "Muudame selgitust"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Krüptimine"; "common_encryption_enabled" = "Krüptimine on kasutusel"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Kutse(te) saatmine ei õnnestunud"; "common_unlock" = "Eemalda lukustus"; "common_unmute" = "Lõpeta summutamine"; +"common_unsupported_call" = "See kõne pole toetatud"; "common_unsupported_event" = "Toetamata sündmus"; "common_username" = "Kasutajanimi"; "common_verification_cancelled" = "Verifitseerimine on katkestatud"; @@ -344,18 +352,20 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Selgitus või nimi, 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_access_section_anyone_option_description" = "Kõik võivad selle jututoaga liituda"; +"screen_create_room_room_access_section_anyone_option_title" = "Kõik"; +"screen_create_room_room_access_section_header" = "Ligipääs jututoale"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Küsi võimalust liitumiseks"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Mõned tähemärgid pole lubatud. Kasuta vaid tähti, numbreid ja neid kirjavahemärke ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Selline jututoa aadress on juba olemas. Palun proovi muuta kas aadressi või jututoa nime"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Jah, võta kõik vastu"; +"screen_knock_requests_list_accept_all_alert_description" = "Kas sa oled kindel, et soovid kõik vastu liitumist soovinud võtta?"; +"screen_knock_requests_list_accept_all_alert_title" = "Võta kõik vastu"; +"screen_knock_requests_list_accept_all_button_title" = "Nõustu kõigiga"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Jah, keeldu liitumisest ning keela ligipääs"; +"screen_knock_requests_list_ban_alert_description" = "Kas sa oled kindel, et soovid kasutajale %1$@ keelata ligipääsu siia jututuppa ning seada talle suhtluskeelu? Seetõttu ta ei saa ka enam hiljem liitumispalvet saata."; +"screen_knock_requests_list_ban_alert_title" = "Keeldu liitumisest ja keela ligipääs"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Jah, keeldu"; +"screen_knock_requests_list_decline_alert_description" = "Kas sa oled kindel, et soovid kasutajale %1$@ keelata ligipääsu siia jututuppa?"; +"screen_knock_requests_list_decline_alert_title" = "Keela ligipääs"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Keeldu ja määra suhtluskeeld"; +"screen_knock_requests_list_empty_state_description" = "Kui keegi soovib jututoaga liituda, siis need päringud on kuvatud siin."; +"screen_knock_requests_list_empty_state_title" = "Pole ühtegi liitumispalvet"; +"screen_knock_requests_list_title" = "Liitumispalved"; +"screen_media_upload_preview_caption_warning" = "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."; +"screen_media_upload_preview_error_failed_processing" = "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."; +"screen_media_upload_preview_error_failed_sending" = "Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."; "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_reset_encryption_password_error" = "Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti."; @@ -375,11 +402,16 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Üks või enam sinu seadet on verifitseerimata. Sa võid sõnumi ikkagi ära saata või katkestad saatmise ning proovid uuesti, kui oled kõik oma seadmed verifitseerinud."; "screen_resolve_send_failure_you_unsigned_device_title" = "Kuna sul on üks või enam verifitseerimata seadet, siis sinu sõnum jäi saatmata"; "screen_room_mentions_at_room_subtitle" = "Teavita kogu jututuba"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Vaata kõiki"; "screen_room_pinned_banner_indicator" = "%1$@ / %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ esiletõstetud sõnumit"; "screen_room_pinned_banner_loading_description" = "Laadime sõnumit…"; "screen_room_pinned_banner_view_all_button_title" = "Näita kõiki"; +"screen_room_single_knock_request_accept_button_title" = "Nõustu"; +"screen_room_single_knock_request_title" = "%1$@ soovib selle jututoaga liituda"; +"screen_room_single_knock_request_view_button_title" = "Vaata"; "screen_room_details_pinned_events_row_title" = "Esiletõstetud sõnumid"; +"screen_room_details_requests_to_join_title" = "Liitumispalved"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Tere tulemast tagasi!"; "screen_login_title_with_homeserver" = "Logi sisse serverisse %1$@"; "screen_media_picker_error_failed_selection" = "Meediafaili valimine ei õnnestunud. Palun proovi uuesti."; -"screen_media_upload_preview_error_failed_processing" = "Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."; -"screen_media_upload_preview_error_failed_sending" = "Meediafaili üleslaadimine ei õnnestunud. Palun proovi uuesti."; "screen_migration_message" = "Tänud, et ootad - seda toimingut on vaja teha vaid üks kord."; "screen_migration_title" = "Seadistame sinu kasutajakontot."; "screen_notification_optin_subtitle" = "Sa võid seadistusi hiljem alati muuta."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Lisa emotikon"; "screen_room_timeline_beginning_of_room" = "See on %1$@ jututoa algus."; "screen_room_timeline_beginning_of_room_no_name" = "See on antud vestluse algus."; +"screen_room_timeline_legacy_call" = "Kõne pole nende rakenduste vahel toetatud. Palun küsi teiselt osapoolelt, kas ta oleks nõus kasutama Element X rakendust."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Enne kui alustad siin verifitseerimist, palun ava rakendus teises seadmes."; +"screen_session_verification_use_another_device_title" = "Ava rakendus teises verifitseeritud seadmes"; +"screen_session_verification_waiting_another_device_subtitle" = "Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist."; +"screen_session_verification_waiting_another_device_title" = "Alusta verifitseerimist teises seadmes"; "screen_session_verification_waiting_to_accept_subtitle" = "Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis."; "screen_session_verification_waiting_to_accept_title" = "Ootame nõustumist verifitseerimispäringuga"; "screen_share_location_title" = "Jaga asukohta"; @@ -911,7 +946,7 @@ "state_event_room_invite_you" = "%1$@ saatis sulle kutse"; "state_event_room_join" = "%1$@ liitus jututoaga"; "state_event_room_join_by_you" = "Sina liitusid jututoaga"; -"state_event_room_knock" = "%1$@ palus liitumist"; +"state_event_room_knock" = "%1$@ palus võimalust liituda"; "state_event_room_knock_accepted" = "%1$@ lubas kasutajal %2$@ liituda"; "state_event_room_knock_accepted_by_you" = "Sina lubasid kasutajal %1$@ liituda!"; "state_event_room_knock_by_you" = "Sina palusid liitumist"; diff --git a/ElementX/Resources/Localizations/et.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/et.lproj/Localizable.stringsdict index b25b512850..643243c91b 100644 --- a/ElementX/Resources/Localizations/et.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/et.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d osalejat + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ + veel %2$d kasutaja soovivad selle jututoaga liituda + other + %1$@ + veel %2$d kasutajat soovivad selle jututoaga liituda + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/fa.lproj/Localizable.strings b/ElementX/Resources/Localizations/fa.lproj/Localizable.strings index e8cce2aae0..03821d049a 100644 --- a/ElementX/Resources/Localizations/fa.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/fa.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "ضبط پیام صوتی."; "a11y_voice_message_stop_recording" = "توقّف ضبط"; "action_accept" = "پذیرش"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "افزودن به خط زمانی"; "action_back" = "بازگشت"; "action_call" = "تماس"; @@ -35,8 +36,10 @@ "action_confirm_password" = "تأیید گذرواژه"; "action_continue" = "ادامه"; "action_copy" = "رونوشت"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "رونوشت از پیوند"; "action_copy_link_to_message" = "رونوشت از پیوند پیام"; +"action_copy_text" = "Copy text"; "action_create" = "ایجاد"; "action_create_a_room" = "ایجاد اتاق"; "action_deactivate" = "غیرفعّال"; @@ -47,6 +50,7 @@ "action_discard" = "دور ریختن"; "action_done" = "انجام شد"; "action_edit" = "ویرایش"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "ویرایش نظرسنجی"; "action_enable" = "به کار انداختن"; "action_end_poll" = "پایان نظرسنجی"; @@ -81,6 +85,8 @@ "action_react" = "واکنش"; "action_reject" = "رد کردن"; "action_remove" = "برداشتن"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "پاسخ"; "action_reply_in_thread" = "پاسخ در رشته"; "action_report_bug" = "گزارش اشکال"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "برپایی بازیابی"; "common_about" = "درباره"; "common_acceptable_use_policy" = "سیاست استفادهٔ پذیرفتنی"; +"common_adding_caption" = "Adding caption"; "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" = "حق رونوشت"; @@ -138,6 +144,7 @@ "common_direct_chat" = "گپ مستقیم"; "common_edited_suffix" = "(ویراسته)"; "common_editing" = "ویرایش"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "رمزنگاری به کار افتاده"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "ناتوان در فرستادن دعوت(ها)"; "common_unlock" = "قفل‌گشایی"; "common_unmute" = "باصدا"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "رویداد پشتیبانی نشده"; "common_username" = "نام کاربری"; "common_verification_cancelled" = "تأیید لغو شد"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."; +"screen_media_upload_preview_error_failed_sending" = "بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "پیام‌های سنجاق شده"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "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" = "می‌توانید بعداً تنظیماتتان را تغییر دهید."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "افزودن شکلک"; "screen_room_timeline_beginning_of_room" = "آغاز %1$@ است."; "screen_room_timeline_beginning_of_room_no_name" = "این، آغاز گفت‌وگوست."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "نمایش کم‌تر"; "screen_room_timeline_message_copied" = "پیام رونوشت شد"; "screen_room_timeline_no_permission_to_post" = "اجازهٔ فرستادن به این اتاق را ندارید"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "مطابق نیستند"; "screen_session_verification_they_match" = "مطابقند"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "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" = "هم‌رسانی موقعیت"; @@ -911,8 +946,8 @@ "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" = "%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$@ لغو شد"; diff --git a/ElementX/Resources/Localizations/fa.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/fa.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..ab275b9162 --- /dev/null +++ b/ElementX/Resources/Localizations/fa.lproj/Localizable.stringsdict @@ -0,0 +1,342 @@ + + + + + a11y_digits_entered + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d digit entered + other + %1$d digits entered + + + a11y_read_receipts_multiple_with_others + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Read by %1$@ and %2$d other + other + Read by %1$@ and %2$d others + + + common_member_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d عضو + other + %1$d عضو + + + common_poll_votes_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d vote + other + %d votes + + + notification_compat_summary_line_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@: %2$d message + other + %1$@: %2$d messages + + + notification_compat_summary_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d notification + other + %d notifications + + + notification_invitations + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d invitation + other + %d invitations + + + notification_new_messages_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d new message + other + %d new messages + + + notification_unread_notified_messages + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d unread notified message + other + %d unread notified messages + + + notification_unread_notified_messages_in_room_rooms + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d room + other + %d rooms + + + screen_app_lock_subtitle + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + You have %1$d attempt to unlock + other + You have %1$d attempts to unlock + + + screen_app_lock_subtitle_wrong_pin + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Wrong PIN. You have %1$d more chance + other + Wrong PIN. You have %1$d more chances + + + screen_pinned_timeline_screen_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d Pinned message + other + %1$d Pinned messages + + + screen_room_member_list_header_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d person + other + %1$d people + + + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + + screen_room_timeline_state_changes + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d room change + other + %1$d room changes + + + screen_room_typing_many_members + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@, %2$@ and %3$d other + other + %1$@, %2$@ and %3$d others + + + screen_room_typing_many_members_second_component_ios + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d other + other + %d others + + + screen_room_typing_notification + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ is typing + other + %1$@ are typing + + + troubleshoot_notifications_test_detect_push_provider_success + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Found %1$d push provider: %2$@ + other + Found %1$d push providers: %2$@ + + + troubleshoot_notifications_test_unified_push_success + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d distributor found: %2$@. + other + %1$d distributors found: %2$@. + + + + \ No newline at end of file diff --git a/ElementX/Resources/Localizations/fi.lproj/InfoPlist.strings b/ElementX/Resources/Localizations/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9a792c8182 --- /dev/null +++ b/ElementX/Resources/Localizations/fi.lproj/InfoPlist.strings @@ -0,0 +1,5 @@ +"NSCameraUsageDescription" = "Ottaakseen kuvia tai videoita ja lähettääkseen ne viestinä, Element X tarvitsee luvan kameraan."; +"NSFaceIDUsageDescription" = "Face ID:tä käytetään sovelluksesi avaamiseen."; +"NSLocationWhenInUseUsageDescription" = "Myönnä lupa sijaintiisi, jotta Element X voi jakaa sen."; +"NSMicrophoneUsageDescription" = "Ääniviestien tallentamiseen ja lähettämiseen Element X:n on päästävä käsiksi mikrofoniin."; +"NSPhotoLibraryUsageDescription" = "Mahdollistaa valokuvien ja videoiden tallentamisen kirjastoosi."; diff --git a/ElementX/Resources/Localizations/fi.lproj/Localizable.strings b/ElementX/Resources/Localizations/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..3c44d7e5ae --- /dev/null +++ b/ElementX/Resources/Localizations/fi.lproj/Localizable.strings @@ -0,0 +1,1103 @@ +"Notification" = "Ilmoitus"; +"a11y_delete" = "Poista"; +"a11y_hide_password" = "Piilota salasana"; +"a11y_jump_to_bottom" = "Siirry loppuun"; +"a11y_notifications_mentions_only" = "Vain maininnat"; +"a11y_notifications_muted" = "Mykistetty"; +"a11y_page_n" = "Sivu %1$d"; +"a11y_pause" = "Keskeytä"; +"a11y_pin_field" = "PIN-kenttä"; +"a11y_play" = "Toista"; +"a11y_poll_end" = "Päättynyt kysely"; +"a11y_react_with" = "Reagoi emojilla %1$@"; +"a11y_react_with_other_emojis" = "Reagoi muilla emojeilla"; +"a11y_read_receipts_multiple" = "%1$@ ja %2$@ on lukenut viestin"; +"a11y_read_receipts_single" = "%1$@ on lukenut viestin"; +"a11y_read_receipts_tap_to_show_all" = "Näytä kaikki napauttamalla"; +"a11y_remove_reaction_with" = "Poista reaktio emojilla %1$@"; +"a11y_send_files" = "Lähetä tiedostoja"; +"a11y_show_password" = "Näytä salasana"; +"a11y_start_call" = "Aloita puhelu"; +"a11y_user_menu" = "Käyttäjävalikko"; +"a11y_voice_message_record" = "Nauhoita ääniviesti."; +"a11y_voice_message_stop_recording" = "Lopeta nauhoittaminen"; +"action_accept" = "Hyväksy"; +"action_add_caption" = "Add caption"; +"action_add_to_timeline" = "Lisää aikajanalle"; +"action_back" = "Takaisin"; +"action_call" = "Soita"; +"action_cancel" = "Peruuta"; +"action_cancel_for_now" = "Peruuta toistaiseksi"; +"action_choose_photo" = "Valitse kuva"; +"action_clear" = "Tyhjennä"; +"action_close" = "Sulje"; +"action_complete_verification" = "Viimeistele vahvistus"; +"action_confirm" = "Vahvista"; +"action_confirm_password" = "Vahvista salasana"; +"action_continue" = "Jatka"; +"action_copy" = "Kopioi"; +"action_copy_caption" = "Copy caption"; +"action_copy_link" = "Kopioi linkki"; +"action_copy_link_to_message" = "Kopioi linkki viestiin"; +"action_copy_text" = "Copy text"; +"action_create" = "Luo"; +"action_create_a_room" = "Luo huone"; +"action_deactivate" = "Deaktivoi"; +"action_deactivate_account" = "Deaktivoi tili"; +"action_decline" = "Hylkää"; +"action_delete_poll" = "Poista kysely"; +"action_disable" = "Poista käytöstä"; +"action_discard" = "Hylkää"; +"action_done" = "Valmis"; +"action_edit" = "Muokkaa"; +"action_edit_caption" = "Edit caption"; +"action_edit_poll" = "Muokkaa kyselyä"; +"action_enable" = "Ota käyttöön"; +"action_end_poll" = "Lopeta kysely"; +"action_enter_pin" = "Syötä PIN-koodi"; +"action_forgot_password" = "Unohditko salasanan?"; +"action_forward" = "Välitä"; +"action_go_back" = "Takaisin"; +"action_ignore" = "Ohita"; +"action_invite" = "Kutsu"; +"action_invite_friends" = "Kutsu ihmisiä"; +"action_invite_friends_to_app" = "Kutsu ihmisiä %1$@ -sovellukseen"; +"action_invite_people_to_app" = "Kutsu ihmisiä %1$@ -sovellukseen"; +"action_invites_list" = "Kutsut"; +"action_join" = "Liity"; +"action_learn_more" = "Lue lisää"; +"action_leave" = "Poistu"; +"action_leave_conversation" = "Poistu keskustelusta"; +"action_leave_room" = "Poistu huoneesta"; +"action_load_more" = "Lataa lisää"; +"action_manage_account" = "Hallitse tiliä"; +"action_manage_devices" = "Hallitse laitteita"; +"action_message" = "Lähetä viesti"; +"action_next" = "Seuraava"; +"action_no" = "Ei"; +"action_not_now" = "Ei nyt"; +"action_ok" = "OK"; +"action_open_settings" = "Asetukset"; +"action_open_with" = "Avaa sovelluksessa"; +"action_pin" = "Kiinnitä"; +"action_quick_reply" = "Pikavastaus"; +"action_quote" = "Lainaa"; +"action_react" = "Reagoi"; +"action_reject" = "Hylkää"; +"action_remove" = "Poista"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; +"action_reply" = "Vastaa"; +"action_reply_in_thread" = "Vastaa ketjuun"; +"action_report_bug" = "Ilmoita virheestä"; +"action_report_content" = "Ilmoita sisällöstä"; +"action_reset" = "Nollaa"; +"action_reset_identity" = "Nollaa identiteetti"; +"action_retry" = "Yritä uudelleen"; +"action_retry_decryption" = "Yritä salauksen purkamista uudelleen"; +"action_save" = "Tallenna"; +"action_search" = "Hae"; +"action_send" = "Lähetä"; +"action_send_message" = "Lähetä viesti"; +"action_share" = "Jaa"; +"action_share_link" = "Jaa linkki"; +"action_show" = "Näytä"; +"action_sign_in_again" = "Kirjaudu uudelleen"; +"action_signout" = "Kirjaudu ulos"; +"action_signout_anyway" = "Kirjaudu ulos silti"; +"action_skip" = "Ohita"; +"action_start" = "Aloita"; +"action_start_chat" = "Aloita keskustelu"; +"action_start_verification" = "Aloita vahvistus"; +"action_static_map_load" = "Lataa kartta napauttamalla"; +"action_take_photo" = "Ota kuva"; +"action_tap_for_options" = "Näytä vaihtoehdot napauttamalla"; +"action_try_again" = "Yritä uudelleen"; +"action_unpin" = "Poista kiinnitys"; +"action_view_in_timeline" = "Näytä aikajanalla"; +"action_view_source" = "Näytä lähde"; +"action_yes" = "Kyllä"; +"banner_migrate_to_native_sliding_sync_action" = "Kirjaudu Ulos & Päivitä"; +"banner_migrate_to_native_sliding_sync_description" = "Palvelimesi tukee nyt uutta, nopeampaa protokollaa. Kirjaudu ulos ja takaisin sisään päivittääksesi nyt. Jos teet tämän nyt, voit välttää pakotetun uloskirjautumisen, kun vanha protokolla poistetaan myöhemmin."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Kotipalvelimesi ei enää tue vanhaa protokollaa. Kirjaudu ulos ja takaisin sisään jatkaaksesi sovelluksen käyttöä."; +"banner_migrate_to_native_sliding_sync_title" = "Päivitys saatavilla"; +"banner_set_up_recovery_content" = "Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, mikäli menetät pääsyn kaikkiin laitteisiisi."; +"banner_set_up_recovery_title" = "Ota palautus käyttöön tilisi suojaamiseksi"; +"common_about" = "Tietoa"; +"common_acceptable_use_policy" = "Hyväksyttävän käytön käytäntö"; +"common_adding_caption" = "Adding caption"; +"common_advanced_settings" = "Edistyneet asetukset"; +"common_analytics" = "Analytiikka"; +"common_appearance" = "Ulkoasu"; +"common_audio" = "Ääni"; +"common_blocked_users" = "Estetyt käyttäjät"; +"common_bubbles" = "Kuplat"; +"common_call_started" = "Puhelu alkoi"; +"common_chat_backup" = "Keskustelujen varmuuskopiointi"; +"common_copyright" = "Tekijänoikeudet"; +"common_creating_room" = "Luodaan huonetta..."; +"common_current_user_left_room" = "Poistuit huoneesta"; +"common_dark" = "Tumma"; +"common_decryption_error" = "Salauksen purkuvirhe"; +"common_developer_options" = "Kehittäjän asetukset"; +"common_device_id" = "Laitteen tunnus"; +"common_direct_chat" = "Yksityinen keskustelu"; +"common_edited_suffix" = "(muokattu)"; +"common_editing" = "Muokataan viestiä"; +"common_editing_caption" = "Editing caption"; +"common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Salaus"; +"common_encryption_enabled" = "Salaus käytössä"; +"common_enter_your_pin" = "Syötä PIN-koodisi"; +"common_error" = "Virhe"; +"common_everyone" = "Kaikki"; +"common_face_id_ios" = "Face ID"; +"common_failed" = "Epäonnistui"; +"common_favourite" = "Lisää suosikkeihin"; +"common_favourited" = "Lisätty suosikkeihin"; +"common_file" = "Tiedosto"; +"common_forward_message" = "Välitä viesti"; +"common_frequently_used" = "Usein käytetyt"; +"common_gif" = "GIF"; +"common_image" = "Kuva"; +"common_in_reply_to" = "Vastauksena käyttäjälle %1$@"; +"common_invite_unknown_profile" = "Tätä Matrix-tunnusta ei löytynyt, joten kutsu ei välttämättä mene perille."; +"common_leaving_room" = "Poistutaan huoneesta"; +"common_light" = "Vaalea"; +"common_link_copied_to_clipboard" = "Linkki kopioitu leikepöydälle"; +"common_loading" = "Ladataan…"; +"common_message" = "Viesti"; +"common_message_actions" = "Viestitoiminnot"; +"common_message_layout" = "Viestien asettelu"; +"common_message_removed" = "Viesti poistettu"; +"common_modern" = "Moderni"; +"common_mute" = "Mykistä"; +"common_no_results" = "Ei tuloksia"; +"common_no_room_name" = "Nimetön huone"; +"common_offline" = "Ei yhteyttä"; +"common_optic_id_ios" = "Optic ID"; +"common_or" = "tai"; +"common_password" = "Salasana"; +"common_people" = "Ihmiset"; +"common_permalink" = "Pysyvä linkki"; +"common_permission" = "Lupa"; +"common_please_wait" = "Odota hetki..."; +"common_poll_end_confirmation" = "Haluatko varmasti lopettaa tämän kyselyn?"; +"common_poll_summary" = "Kysely: %1$@"; +"common_poll_total_votes" = "Ääniä yhteensä: %1$@"; +"common_poll_undisclosed_text" = "Tulokset näkyvät kyselyn päätyttyä"; +"common_privacy_policy" = "Tietosuojakäytäntö"; +"common_reaction" = "Reaktio"; +"common_reactions" = "Reaktiot"; +"common_recovery_key" = "Palautusavain"; +"common_refreshing" = "Päivitetään..."; +"common_replying_to" = "Vastataan käyttäjälle %1$@"; +"common_report_a_bug" = "Ilmoita virheestä"; +"common_report_a_problem" = "Ilmoita ongelmasta"; +"common_report_submitted" = "Ilmoitus lähetetty"; +"common_rich_text_editor" = "Rikastettu tekstieditori"; +"common_room" = "Huone"; +"common_room_name" = "Huoneen nimi"; +"common_room_name_placeholder" = "esim. projektisi nimi"; +"common_saved_changes" = "Muutokset tallennettu"; +"common_saving" = "Tallennetaan"; +"common_screen_lock" = "Näyttölukko"; +"common_search_for_someone" = "Etsi jotakuta"; +"common_search_results" = "Hakutulokset"; +"common_security" = "Turvallisuus"; +"common_seen_by" = "Nähneet henkilöt"; +"common_sending" = "Lähetetään…"; +"common_sending_failed" = "Lähetys epäonnistui"; +"common_sent" = "Lähetetty"; +"common_server_not_supported" = "Palvelin ei ole tuettu"; +"common_server_url" = "Palvelimen osoite"; +"common_settings" = "Asetukset"; +"common_shared_location" = "Jaettu sijainti"; +"common_signing_out" = "Kirjaudutaan ulos"; +"common_something_went_wrong" = "Jokin meni pieleen"; +"common_starting_chat" = "Aloitetaan keskustelua..."; +"common_sticker" = "Tarra"; +"common_success" = "Onnistui"; +"common_suggestions" = "Ehdotukset"; +"common_syncing" = "Synkronoidaan"; +"common_system" = "Järjestelmän oletus"; +"common_text" = "Teksti"; +"common_third_party_notices" = "Kolmannen osapuolen ilmoitukset"; +"common_thread" = "Viestiketju"; +"common_topic" = "Aihe"; +"common_topic_placeholder" = "Mistä tässä huoneessa on kyse?"; +"common_touch_id_ios" = "Touch ID"; +"common_unable_to_decrypt" = "Salauksen purkaminen ei onnistunut"; +"common_unable_to_decrypt_no_access" = "Sinulla ei ole oikeutta lukea tätä viestiä"; +"common_unable_to_invite_message" = "Kutsujen ei voitu lähettää yhdelle tai useammalle käyttäjälle."; +"common_unable_to_invite_title" = "Kutsujen lähettäminen ei onnistunut"; +"common_unlock" = "Avaa"; +"common_unmute" = "Poista mykistys"; +"common_unsupported_call" = "Puhelu, jota ei tueta"; +"common_unsupported_event" = "Tapahtumaa ei tueta"; +"common_username" = "Käyttäjänimi"; +"common_verification_cancelled" = "Vahvistus peruttu"; +"common_verification_complete" = "Vahvistus suoritettu"; +"common_verification_failed" = "Vahvistus epäonnistui"; +"common_verified" = "Vahvistettu"; +"common_verify_device" = "Vahvista laite"; +"common_verify_identity" = "Vahvista identiteetti"; +"common_video" = "Video"; +"common_voice_message" = "Ääniviesti"; +"common_waiting" = "Odotetaan…"; +"common_waiting_for_decryption_key" = "Odotetaan viestiä"; +"common.copied_to_clipboard" = "Kopioitu leikepöydälle"; +"common.do_not_show_this_again" = "Älä näytä tätä uudelleen"; +"common.open_source_licenses" = "Avoimen lähdekoodin lisenssit"; +"common.pinned" = "Kiinnitetty"; +"common.send_to" = "Jaa"; +"common.you" = "Sinä"; +"common_unable_to_decrypt_insecure_device" = "Lähetetty suojaamattomasta laitteesta"; +"common_unable_to_decrypt_verification_violation" = "Lähettäjän vahvistettu identiteetti on vaihtunut"; +"confirm_recovery_key_banner_message" = "Vahvista palautusavaimesi, jotta pääset edelleen käyttämään avainten säilytystä ja viestihistoriaa."; +"confirm_recovery_key_banner_primary_button_title" = "Syötä palautusavaimesi"; +"confirm_recovery_key_banner_secondary_button_title" = "Unohditko palautusavaimesi?"; +"confirm_recovery_key_banner_title" = "Avainten säilytys ei ole synkronoitu"; +"crash_detection_dialog_content" = "%1$@ kaatui edellisellä käyttökerralla. Haluatko jakaa virheraportin kanssamme?"; +"crypto_identity_change_pin_violation" = "Käyttäjän %1$@ identiteetti näyttää muuttuneen. %2$@"; +"crypto_identity_change_pin_violation_new" = "Käyttäjän %1$@ %2$@ identiteetti näyttää muuttuneen. %3$@"; +"crypto_identity_change_pin_violation_new_user_id" = "(%1$@)"; +"dialog_permission_camera" = "Jotta sovellus voisi käyttää kameraa, anna lupa järjestelmän asetuksista."; +"dialog_permission_generic" = "Anna lupa järjestelmän asetuksista."; +"dialog_permission_location_description_ios" = "Myönnä lupa laitteen sijaintiasetuksista."; +"dialog_permission_location_title_ios" = "%1$@ -sovelluksella ei ole lupaa sijaintiisi."; +"dialog_permission_microphone" = "Jotta sovellus voisi käyttää mikrofonia, anna lupa järjestelmän asetuksista."; +"dialog_permission_microphone_description_ios" = "Anna lupa, jotta voit nauhoittaa ja lähettää ääniviestejä."; +"dialog_permission_microphone_title_ios" = "%1$@ tarvitsee luvan mikrofonin käyttöön."; +"dialog_permission_notification" = "Jotta sovellus voisi näyttää ilmoituksia, anna lupa järjestelmän asetuksista."; +"dialog_title_confirmation" = "Vahvistus"; +"dialog_title_warning" = "Varoitus"; +"dialog_unsaved_changes_description_ios" = "Muutoksiasi ei tallenneta"; +"dialog_unsaved_changes_title" = "Tallenna muutokset?"; +"emoji_picker_category_activity" = "Aktiviteetit"; +"emoji_picker_category_flags" = "Liput"; +"emoji_picker_category_foods" = "Ruoka ja juoma"; +"emoji_picker_category_nature" = "Eläimet ja luonto"; +"emoji_picker_category_objects" = "Esineet"; +"emoji_picker_category_people" = "Hymiöt ja ihmiset"; +"emoji_picker_category_places" = "Matkustaminen ja paikat"; +"emoji_picker_category_symbols" = "Symbolit"; +"error_account_creation_not_possible" = "Kotipalvelimesi on päivitettävä tukemaan Matrix Authentication Serviceä ja tilin luomista."; +"error_failed_creating_the_permalink" = "Pysyvän linkin luominen epäonnistui"; +"error_failed_loading_map" = "%1$@ ei pystynyt lataamaan karttaa. Yritä myöhemmin uudelleen."; +"error_failed_loading_messages" = "Viestien lataaminen epäonnistui"; +"error_failed_locating_user" = "%1$@ ei päässyt käsiksi sijaintiisi. Yritä myöhemmin uudelleen."; +"error_failed_uploading_voice_message" = "Ääniviestin lähettäminen epäonnistui."; +"error_message_not_found" = "Viestiä ei löytynyt"; +"error_no_compatible_app_found" = "Yhteensopivaa sovellusta ei löytynyt käsittelemään tätä toimintoa."; +"error_some_messages_have_not_been_sent" = "Joitakin viestejä ei ole lähetetty"; +"error_unknown" = "Anteeksi, tapahtui virhe"; +"event_shield_reason_authenticity_not_guaranteed" = "Tämän salatun viestin aitoutta ei voida taata tällä laitteella."; +"event_shield_reason_previously_verified" = "Aiemmin vahvistetun käyttäjän salaama."; +"event_shield_reason_sent_in_clear" = "Ei salattu."; +"event_shield_reason_unknown_device" = "Tuntemattoman tai poistetun laitteen salaama."; +"event_shield_reason_unsigned_device" = "Salattu laitteella, jota sen omistaja ei ole vahvistanut."; +"event_shield_reason_unverified_identity" = "Vahvistamattoman käyttäjän salaama."; +"full_screen_intent_banner_message" = "Salli koko näytön ilmoitukset, kun laite on lukittu, jos et halua koskaan missata tärkeää puhelua."; +"full_screen_intent_banner_title" = "Paranna puhelukokemustasi"; +"invite_friends_rich_title" = "🔐️ Liity seuraani %1$@ -sovelluksessa"; +"invite_friends_text" = "Hei, keskustele kanssani %1$@ -sovelluksessa: %2$@"; +"leave_conversation_alert_subtitle" = "Haluatko varmasti poistua keskustelusta? Tämä keskustelu ei ole julkinen ja et voi liittyä takaisin ilman kutsua."; +"leave_room_alert_empty_subtitle" = "Haluatko varmasti poistua huoneesta? Olet huoneen ainoa jäsen. Jos poistut, kukaan ei voi liittyä takaisin, et edes sinä."; +"leave_room_alert_private_subtitle" = "Haluatko varmasti poistua huoneesta? Tämä huone ei ole julkinen ja et voi liittyä takaisin ilman kutsua."; +"leave_room_alert_subtitle" = "Haluatko varmasti poistua huoneesta?"; +"login_initial_device_name_ios" = "%1$@ iOS"; +"notification_channel_call" = "Puhelu"; +"notification_channel_listening_for_events" = "Tapahtumien kuuntelu"; +"notification_channel_noisy" = "Äänekkäät ilmoitukset"; +"notification_channel_ringing_calls" = "Soivat puhelut"; +"notification_channel_silent" = "Hiljaiset ilmoitukset"; +"notification_incoming_call" = "Saapuva puhelu"; +"notification_inline_reply_failed" = "** Lähetys epäonnistui - avaa huone"; +"notification_invite_body" = "Kutsui sinut keskustelemaan"; +"notification_invite_body_with_sender" = "%1$@ kutsui sinut keskustelemaan"; +"notification_mentioned_you_body" = "Mainitsi sinut: %1$@"; +"notification_new_messages" = "Uusia viestejä"; +"notification_reaction_body" = "Reagoi emojilla %1$@"; +"notification_room_invite_body" = "Kutsui sinut liittymään huoneeseen"; +"notification_room_invite_body_with_sender" = "%1$@ kutsui sinut liittymään huoneeseen"; +"notification_sender_me" = "Minä"; +"notification_sender_mention_reply" = "%1$@ mainitsi tai vastasi"; +"notification_test_push_notification_content" = "Katselet ilmoitusta! Klikkaa minua!"; +"notification_ticker_text_dm" = "%1$@: %2$@"; +"notification_ticker_text_group" = "%1$@: %2$@ %3$@"; +"notification_unread_notified_messages_and_invitation" = "%1$@ ja %2$@"; +"notification_unread_notified_messages_in_room" = "%1$@ %2$@"; +"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ %2$@ ja %3$@"; +"preference_rageshake" = "Raivostunut ravistaminen ilmoittaa virheestä"; +"rageshake_detection_dialog_content" = "Näytät ravistelevan puhelinta turhautuneena. Haluatko avata vikailmoitusnäytön?"; +"rich_text_editor_bullet_list" = "Numeroimaton luettelo päälle/pois"; +"rich_text_editor_close_formatting_options" = "Sulje muotoiluasetukset"; +"rich_text_editor_code_block" = "Koodilohko päälle/pois"; +"rich_text_editor_composer_placeholder" = "Viesti..."; +"rich_text_editor_create_link" = "Luo linkki"; +"rich_text_editor_edit_link" = "Muokkaa linkkiä"; +"rich_text_editor_format_bold" = "Käytä lihavoitua muotoa"; +"rich_text_editor_format_italic" = "Käytä kursiivimuotoa"; +"rich_text_editor_format_strikethrough" = "Käytä yliviivausmuotoa"; +"rich_text_editor_format_underline" = "Käytä alleviivausmuotoa"; +"rich_text_editor_full_screen_toggle" = "Koko näytön tila päälle/pois"; +"rich_text_editor_indent" = "Sisennä"; +"rich_text_editor_inline_code" = "Käytä rivinsisäistä koodimuotoa"; +"rich_text_editor_link" = "Aseta linkki"; +"rich_text_editor_numbered_list" = "Numeroitu luettelo päälle/pois"; +"rich_text_editor_open_compose_options" = "Avaa kirjoitusvaihtoehdot"; +"rich_text_editor_quote" = "Lainaus päälle/pois"; +"rich_text_editor_remove_link" = "Poista linkki"; +"rich_text_editor_unindent" = "Poista sisennys"; +"rich_text_editor_url_placeholder" = "Linkki"; +"rich_text_editor_a11y_add_attachment" = "Lisää liite"; +"rich_text_editor_composer_caption_placeholder" = "Valinnainen kuvateksti..."; +"screen_advanced_settings_element_call_base_url" = "Mukautettu Element Call URL-osoite"; +"screen_advanced_settings_element_call_base_url_description" = "Aseta mukautettu URL-osoite Element Callille."; +"screen_advanced_settings_element_call_base_url_validation_error" = "Virheellinen URL-osoite. Varmista, että sisällytät protokollan (http/https) ja oikean osoitteen."; +"screen_create_room_room_access_section_anyone_option_description" = "Kuka tahansa voi liittyä tähän huoneeseen"; +"screen_create_room_room_access_section_anyone_option_title" = "Kuka tahansa"; +"screen_create_room_room_access_section_header" = "Huoneeseen Pääsy"; +"screen_create_room_room_access_section_knocking_option_description" = "Kuka tahansa voi pyytää saada liittyä huoneeseen, mutta ylläpitäjän tai valvojan on hyväksyttävä pyyntö"; +"screen_create_room_room_access_section_knocking_option_title" = "Pyydä liittymistä"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Jotkin merkit eivät ole sallittuja. Vain kirjaimet, numerot ja seuraavat symbolit ovat tuettuja ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Tämä huoneen osoite on jo käytössä, yritä muokata huoneen osoitekenttää tai muuta huoneen nimeä"; +"screen_create_room_room_address_section_footer" = "Jotta tämä huone näkyisi julkisessa huonehakemistossa, tarvitset huoneen osoitteen."; +"screen_create_room_room_address_section_title" = "Huoneen osoite"; +"screen_create_room_room_visibility_section_title" = "Huoneen näkyvyys"; +"screen_join_room_cancel_knock_action" = "Peruuta pyyntö"; +"screen_join_room_cancel_knock_alert_confirmation" = "Kyllä, peruuta"; +"screen_join_room_cancel_knock_alert_description" = "Haluatko varmasti peruuttaa pyyntösi liittyä tähän huoneeseen?"; +"screen_join_room_cancel_knock_alert_title" = "Peruuta liittymispyyntö"; +"screen_join_room_knock_message_description" = "Viesti (valinnainen)"; +"screen_join_room_knock_sent_description" = "Saat kutsun liittyä huoneeseen, jos pyyntösi hyväksytään."; +"screen_join_room_knock_sent_title" = "Liittymispyyntö lähetetty"; +"screen_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Median käsittely epäonnistui, yritä uudelleen."; +"screen_media_upload_preview_error_failed_sending" = "Median lähettäminen epäonnistui, yritä uudelleen."; +"screen_pinned_timeline_empty_state_description" = "Paina viestiä ja valitse “%1$@” lisätäksesi sen tänne."; +"screen_pinned_timeline_empty_state_headline" = "Kiinnitä tärkeät viestit, jotta ne löytyvät helposti."; +"screen_reset_encryption_password_error" = "Tapahtui tuntematon virhe. Tarkista, että tilisi salasana on oikein ja yritä uudelleen."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Peruuta vahvistus ja lähetä"; +"screen_resolve_send_failure_changed_identity_subtitle" = "Voit peruuttaa vahvistuksen ja lähettää tämän viestin silti, tai voit peruuttaa viestin lähettämisen toistaiseksi ja yrittää uudelleen myöhemmin, kun olet vahvistanut käyttäjän %1$@ uudelleen."; +"screen_resolve_send_failure_changed_identity_title" = "Viestiäsi ei lähetetty, koska käyttäjän %1$@ vahvistettu identiteetti on muuttunut"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Lähetä viesti silti"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ käyttää yhtä tai useampaa vahvistamatonta laitetta. Voit lähettää viestin silti tai voit peruuttaa sen toistaiseksi ja yrittää myöhemmin uudelleen, kun %2$@ on vahvistanut kaikki laitteensa."; +"screen_resolve_send_failure_unsigned_device_title" = "Viestiäsi ei lähetetty, koska %1$@ ei ole vahvistanut kaikkia laitteitaan."; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Yksi tai useampi laitteistasi on vahvistamaton. Voit lähettää viestin silti tai peruuttaa sen toistaiseksi ja yrittää uudelleen myöhemmin, kun olet vahvistanut kaikki laitteesi."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Viestiäsi ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."; +"screen_room_mentions_at_room_subtitle" = "Ilmoita koko huoneelle"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; +"screen_room_pinned_banner_indicator" = "%1$@ / %2$@"; +"screen_room_pinned_banner_indicator_description" = "Kiinnitetty viesti %1$@"; +"screen_room_pinned_banner_loading_description" = "Viestiä ladataan..."; +"screen_room_pinned_banner_view_all_button_title" = "Näytä kaikki"; +"screen_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; +"screen_room_details_pinned_events_row_title" = "Kiinnitetyt viestit"; +"screen_room_details_requests_to_join_title" = "Requests to join"; +"screen_roomlist_knock_event_sent_description" = "Liittymispyyntö lähetetty"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Viestiä ei lähetetty, koska käyttäjän %1$@ vahvistettu identiteetti on muuttunut."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Viestiä ei lähetetty, koska %1$@ ei ole vahvistanut kaikkia laitteitaan."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Viestiä ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."; +"screen_account_provider_form_hint" = "Kotipalvelimen osoite"; +"screen_account_provider_form_notice" = "Kirjoita hakutermi tai osoite."; +"screen_account_provider_form_subtitle" = "Hae yritystä, yhteisöä tai yksityistä palvelinta."; +"screen_account_provider_form_title" = "Etsi palveluntarjoajaa"; +"screen_account_provider_signin_title" = "Olet kirjautumassa sisään %@-palvelimelle"; +"screen_account_provider_signup_title" = "Olet luomassa tiliä %@-palvelimelle"; +"screen_advanced_settings_developer_mode" = "Kehittäjätila"; +"screen_advanced_settings_developer_mode_description" = "Ottamalla käyttöön pääset käsiksi kehittäjille tarkoitettuihin ominaisuuksiin."; +"screen_advanced_settings_media_compression_description" = "Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä."; +"screen_advanced_settings_media_compression_title" = "Optimoi median laatu"; +"screen_advanced_settings_rich_text_editor_description" = "Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti."; +"screen_advanced_settings_send_read_receipts" = "Lukukuittaukset"; +"screen_advanced_settings_send_read_receipts_description" = "Jos tämä on poissa päältä, sinun lukukuittauksia ei lähetetä kenellekään. Vastaanotat silti lukukuittauksia muilta käyttäjiltä."; +"screen_advanced_settings_share_presence" = "Jaa läsnäolo"; +"screen_advanced_settings_share_presence_description" = "Jos tämä on poissa päältä, et lähetä tai vastaanota lukukuittauksia tai kirjoitusilmotuksia."; +"screen_advanced_settings_view_source_description" = "Ota käyttöön mahdollisuus tarkastella viestin lähdettä aikajanalla."; +"screen_analytics_prompt_data_usage" = "Emme tallenna tai profiloi henkilötietoja"; +"screen_analytics_prompt_help_us_improve" = "Jaa anonyymejä käyttötietoja auttaaksesi meitä tunnistamaan ongelmat."; +"screen_analytics_prompt_read_terms" = "Voit lukea kaikki ehtomme %1$@."; +"screen_analytics_prompt_read_terms_content_link" = "täällä"; +"screen_analytics_prompt_settings" = "Voit poistaa tämän käytöstä milloin tahansa"; +"screen_analytics_prompt_third_party_sharing" = "Emme jaa tietojasi kolmansien osapuolien kanssa"; +"screen_analytics_prompt_title" = "Auta parantamaan %1$@ -sovellusta"; +"screen_analytics_settings_share_data" = "Jaa analytiikkatietoja"; +"screen_app_lock_biometric_authentication" = "biometrinen tunnistus"; +"screen_app_lock_biometric_unlock" = "biometrinen tunnistus"; +"screen_app_lock_biometric_unlock_reason_ios" = "Sovelluksen käyttö edellyttää tunnistautumista"; +"screen_app_lock_forgot_pin" = "Unohtuiko PIN-koodi?"; +"screen_app_lock_settings_change_pin" = "Vaihda PIN-koodi"; +"screen_app_lock_settings_enable_biometric_unlock" = "Salli biometrinen tunnistus"; +"screen_app_lock_settings_enable_face_id_ios" = "Salli Face ID"; +"screen_app_lock_settings_enable_optic_id_ios" = "Salli Optic ID"; +"screen_app_lock_settings_enable_touch_id_ios" = "Salli Touch ID"; +"screen_app_lock_settings_remove_pin" = "Poista PIN-koodi"; +"screen_app_lock_settings_remove_pin_alert_message" = "Haluatko varmasti poistaa PIN-koodin?"; +"screen_app_lock_settings_remove_pin_alert_title" = "Poista PIN-koodi?"; +"screen_app_lock_setup_biometric_unlock_allow_title" = "Salli %1$@"; +"screen_app_lock_setup_biometric_unlock_skip" = "Käytän mieluummin PIN-koodia"; +"screen_app_lock_setup_biometric_unlock_subtitle" = "Säästä aikaa ja ota käyttöön %1$@"; +"screen_app_lock_setup_choose_pin" = "Valitse PIN-koodi"; +"screen_app_lock_setup_confirm_pin" = "Vahvista PIN-koodi"; +"screen_app_lock_setup_pin_context" = "Lukitse %1$@ -sovellus lisätäksesi turvaa keskusteluihisi.\n\nValitse PIN-koodi, jonka muistat. Jos unohdat sen, joudut kirjautumaan ulos."; +"screen_app_lock_setup_pin_forbidden_dialog_content" = "Et voi valita tätä PIN-koodia turvallisuussyistä"; +"screen_app_lock_setup_pin_forbidden_dialog_title" = "Valitse toinen PIN-koodi"; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "Anna sama PIN-koodi kahdesti"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "PIN-koodit eivät täsmää"; +"screen_app_lock_signout_alert_message" = "Sinun on kirjauduttava sisään uudelleen ja luotava uusi PIN-koodi jatkaaksesi"; +"screen_app_lock_signout_alert_title" = "Sinut kirjataan ulos"; +"screen_blocked_users_empty" = "Et ole estänyt ketään"; +"screen_blocked_users_unblocking" = "Poistetaan estoa..."; +"screen_bug_report_attach_screenshot" = "Liitä kuvakaappaus"; +"screen_bug_report_contact_me" = "Voitte ottaa minuun yhteyttä, jos teillä on lisäkysymyksiä."; +"screen_bug_report_contact_me_title" = "Ota minuun yhteyttä"; +"screen_bug_report_edit_screenshot" = "Muokkaa kuvakaappausta"; +"screen_bug_report_editor_description" = "Kuvaile ongelmaasi. Mitä teit? Mitä odotit tapahtuvan? Mitä oikeasti tapahtui? Kerro niin paljon kuin mahdollista."; +"screen_bug_report_editor_placeholder" = "Kuvaile ongelmaasi..."; +"screen_bug_report_editor_supporting" = "Jos mahdollista, kirjoita englanniksi."; +"screen_bug_report_error_description_too_short" = "Kuvaus on liian lyhyt. Kerro tarkemmin mitä tapahtui, kiitos!"; +"screen_bug_report_include_crash_logs" = "Lähetä kaatumislokit"; +"screen_bug_report_include_logs" = "Lähetä lokitiedostot"; +"screen_bug_report_include_screenshot" = "Lähetä kuvakaappaus"; +"screen_bug_report_logs_description" = "Lähetä lokitiedostot viestisi kanssa, jotta voimme varmistaa, että kaikki toimii oikein. Jos haluat lähettää viestisi ilman lokeja, jätä tämä asetus valitsematta."; +"screen_bug_report_view_logs" = "Näytä lokitiedostot"; +"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org on suuri, ilmainen palvelin julkisessa Matrix-verkossa turvalliseen, hajautettuun viestintään, jota ylläpitää Matrix.org-säätiö."; +"screen_change_account_provider_other" = "Muu"; +"screen_change_account_provider_subtitle" = "Käytä toista palveluntarjoajaa, kuten omaa yksityistä palvelintasi tai työpaikkaasi."; +"screen_change_account_provider_title" = "Vaihda palveluntarjoajaa"; +"screen_change_server_error_invalid_homeserver" = "Kotipalvelimeen ei saatu yhteyttä. Varmista, että olet syöttänyt osoitteen oikein. Jos osoite on oikein, ota yhteyttä palvelimesi ylläpitäjään."; +"screen_change_server_error_invalid_well_known" = "Sliding sync ei ole saatavilla well-known tiedostossa olevan ongelman vuoksi:\n%1$@"; +"screen_change_server_error_no_sliding_sync_message" = "Tämä palvelin ei tällä hetkellä tue sliding syncia."; +"screen_change_server_form_header" = "Kotipalvelimen osoite"; +"screen_change_server_form_notice" = "Voit yhdistää vain olemassa olevaan palvelimeen, joka tukee sliding syncia. Kotipalvelimesi ylläpitäjän on otettava se käyttöön. %1$@"; +"screen_change_server_subtitle" = "Mikä on palvelimesi osoite?"; +"screen_change_server_title" = "Valitse palvelimesi"; +"screen_chat_backup_key_backup_action_disable" = "Ota avainten säilytys pois käytöstä"; +"screen_chat_backup_key_backup_action_enable" = "Ota varmuuskopiointi käyttöön"; +"screen_chat_backup_key_backup_description" = "Säilytä kryptografinen identiteettisi ja viestien avaimet turvallisesti palvelimellasi. Tämän avulla pääset käsiksi viestihistoriaan uusillakin laitteilla. %1$@."; +"screen_chat_backup_key_backup_title" = "Avainten säilytys"; +"screen_chat_backup_key_storage_disabled_error" = "Avainten säilytys on oltava käytössä, jotta palautus voidaan ottaa käyttöön."; +"screen_chat_backup_key_storage_toggle_description" = "Lataa avaimet tästä laitteesta"; +"screen_chat_backup_key_storage_toggle_title" = "Salli avainten säilytys"; +"screen_chat_backup_recovery_action_change" = "Vaihda palautusavain"; +"screen_chat_backup_recovery_action_change_description" = "Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."; +"screen_chat_backup_recovery_action_confirm_description" = "Avainten säilytys ei ole tällä hetkellä synkronoitu."; +"screen_chat_backup_recovery_action_setup_description" = "Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$@ -sovelluksesta kaikkialla."; +"screen_create_account_title" = "Luo tili"; +"screen_create_new_recovery_key_list_item_1" = "Avaa %1$@ tietokoneella"; +"screen_create_new_recovery_key_list_item_2" = "Kirjaudu tilillesi uudelleen"; +"screen_create_new_recovery_key_list_item_3" = "Kun sinua pyydetään vahvistamaan laitteesi, valitse %1$@"; +"screen_create_new_recovery_key_list_item_3_reset_all" = "“Nollaa kaikki”"; +"screen_create_new_recovery_key_list_item_4" = "Seuraa ohjeita uuden palautusavaimen luomiseksi"; +"screen_create_new_recovery_key_list_item_5" = "Tallenna uusi palautusavaimesi salasanojen hallintaohjelmaan tai salattuun muistiinpanoon"; +"screen_create_new_recovery_key_title" = "Nollaa tilisi salaus toisella laitteella"; +"screen_create_poll_add_option_btn" = "Lisää vaihtoehto"; +"screen_create_poll_anonymous_desc" = "Näytä tulokset vasta kyselyn päätyttyä"; +"screen_create_poll_anonymous_headline" = "Piilota äänet"; +"screen_create_poll_answer_hint" = "Vaihtoehto %1$d"; +"screen_create_poll_cancel_confirmation_title_ios" = "Peruuta kysely"; +"screen_create_poll_question_desc" = "Kysymys tai aihe"; +"screen_create_poll_question_hint" = "Mistä kyselyssä on kyse?"; +"screen_create_poll_title" = "Luo kysely"; +"screen_create_room_action_create_room" = "Uusi huone"; +"screen_create_room_error_creating_room" = "Huoneen luomisessa tapahtui virhe"; +"screen_create_room_private_option_description" = "Vain kutsutut henkilöt pääsevät tähän huoneeseen. Kaikki viestit ovat päästä päähän salattuja."; +"screen_create_room_private_option_title" = "Yksityinen huone"; +"screen_create_room_public_option_description" = "Kuka tahansa voi löytää tämän huoneen.\nVoit muuttaa tämän milloin tahansa huoneen asetuksista."; +"screen_create_room_public_option_title" = "Julkinen huone"; +"screen_create_room_topic_label" = "Aihe (valinnainen)"; +"screen_deactivate_account_confirmation_dialog_content" = "Vahvista, että haluat deaktivoida tilisi. Tätä ei voi perua."; +"screen_deactivate_account_delete_all_messages" = "Poista kaikki viestini"; +"screen_deactivate_account_delete_all_messages_notice" = "Varoitus: Tulevaisuudessa muut voivat nähdä puutteellisia keskusteluja."; +"screen_deactivate_account_description" = "Tilisi deaktivointia %1$@. Jos teet sen:"; +"screen_deactivate_account_description_bold_part" = "ei voi peruuttaa"; +"screen_deactivate_account_list_item_1" = "Tilisi %1$@ (et voi kirjautua takaisin sisään, eikä tunnustasi voi käyttää uudelleen)."; +"screen_deactivate_account_list_item_1_bold_part" = "poistetaan käytöstä pysyvästi"; +"screen_deactivate_account_list_item_2" = "Sinut poistetaan kaikista keskusteluhuoneista."; +"screen_deactivate_account_list_item_3" = "Tilitietosi poistetaan identiteettipalvelimeltamme."; +"screen_deactivate_account_list_item_4" = "Viestisi näkyvät edelleen rekisteröityneille käyttäjille, mutta ne eivät ole uusien tai rekisteröimättömien käyttäjien saatavilla, jos päätät poistaa ne."; +"screen_deactivate_account_title" = "Deaktivoi tili"; +"screen_edit_poll_delete_confirmation" = "Haluatko varmasti poistaa tämän kyselyn?"; +"screen_edit_profile_display_name" = "Näyttönimi"; +"screen_edit_profile_display_name_placeholder" = "Näyttönimesi"; +"screen_edit_profile_error" = "Tuntematon virhe tapahtui, eikä tietoja voitu muuttaa."; +"screen_edit_profile_error_title" = "Profiilin muokkaaminen ei onnistunut"; +"screen_edit_profile_title" = "Muokkaa profiilia"; +"screen_edit_profile_updating_details" = "Muokataan profiilia..."; +"screen_encryption_reset_action_continue_reset" = "Jatka nollausta"; +"screen_encryption_reset_bullet_1" = "Tilitietosi, yhteystiedot, asetukset ja keskustelulista säilytetään"; +"screen_encryption_reset_bullet_2" = "Menetät kaiken viestihistorian, joka on tallella vain palvelimella"; +"screen_encryption_reset_bullet_3" = "Sinun on vahvistettava kaikki olemassa olevat laitteesi ja yhteystietosi uudelleen"; +"screen_encryption_reset_footer" = "Nollaa identiteettisi vain, jos et voi käyttää toista laitetta, johon olet kirjautunut, ja olet kadottanut palautusavaimesi."; +"screen_encryption_reset_title" = "Etkö voi vahvistaa? Sinun on nollattava identiteettisi."; +"screen_identity_confirmation_cannot_confirm" = "Etkö voi vahvistaa?"; +"screen_identity_confirmation_create_new_recovery_key" = "Luo uusi palautusavain"; +"screen_identity_confirmation_subtitle" = "Vahvista tämä laite suojattua viestintää varten."; +"screen_identity_confirmation_title" = "Vahvista identiteettisi"; +"screen_identity_confirmation_use_another_device" = "Käytä toista laitetta"; +"screen_identity_confirmation_use_recovery_key" = "Käytä palautusavainta"; +"screen_identity_confirmed_subtitle" = "Nyt voit lukea ja lähettää viestejä turvallisesti, ja kaikki, joiden kanssa keskustelet, voivat myös luottaa tähän laitteeseen."; +"screen_identity_confirmed_title" = "Laite vahvistettu"; +"screen_identity_waiting_on_other_device" = "Odotetaan toista laitetta…"; +"screen_invites_decline_chat_message" = "Haluatko varmasti hylätä kutsun liittyä %1$@ -huoneeseen?"; +"screen_invites_decline_chat_title" = "Hylkää kutsu"; +"screen_invites_decline_direct_chat_message" = "Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$@ kanssa?"; +"screen_invites_decline_direct_chat_title" = "Hylkää keskustelu"; +"screen_invites_empty_list" = "Ei kutsuja"; +"screen_invites_invited_you" = "%1$@ (%2$@) kutsui sinut"; +"screen_join_room_join_action" = "Liity huoneeseen"; +"screen_join_room_knock_action" = "Lähetä liittymispyyntö"; +"screen_join_room_space_not_supported_description" = "%1$@ ei tue vielä tiloja. Voit käyttää tiloja selainversiolla."; +"screen_join_room_space_not_supported_title" = "Tiloja ei vielä tueta"; +"screen_join_room_subtitle_knock" = "Paina alla olevaa nappia ja huoneen ylläpitäjä saa ilmoituksen. Voit liittyä keskusteluun kun pyyntösi on hyväksytty."; +"screen_join_room_subtitle_no_preview" = "Sinun on oltava tämän huoneen jäsen, jotta voit nähdä viestihistorian."; +"screen_join_room_title_knock" = "Haluatko liittyä tähän huoneeseen?"; +"screen_join_room_title_no_preview" = "Esikatselu ei ole saatavilla"; +"screen_key_backup_disable_confirmation_action_turn_off" = "Poista käytöstä"; +"screen_key_backup_disable_confirmation_description" = "Menetät salatut viestisi, jos kirjaudut ulos kaikista laitteista."; +"screen_key_backup_disable_confirmation_title" = "Haluatko varmasti poistaa varmuuskopioinnin käytöstä?"; +"screen_key_backup_disable_description" = "Avainten säilytyksen poistaminen poistaa sinun kryptografisen identiteetin ja viestien avaimet palvelimeltasi ja poistaa seuraavat suojausominausuudet käytöstä:"; +"screen_key_backup_disable_description_point_1" = "Et saa salattua viestihistoriaa uusilla laitteilla"; +"screen_key_backup_disable_description_point_2" = "Menetät pääsyn salattuihin viestihisi, jos kirjaudut ulos %1$@ -sovelluksesta kaikkialla."; +"screen_key_backup_disable_title" = "Haluatko varmasti ottaa avainten säilytyksen pois käytöstä ja poistaa sen?"; +"screen_login_error_deactivated_account" = "Tämä tili on deaktivoitu."; +"screen_login_error_invalid_credentials" = "Väärä käyttäjänimi ja/tai salasana"; +"screen_login_error_invalid_user_id" = "Tämä ei ole kelvollinen käyttäjätunnus. Odotettu muoto: '@käyttäjä:kotipalvelin.fi'"; +"screen_login_error_refresh_tokens" = "Tämä palvelin on määritetty käyttämään refresh tokeneja. Näitä ei tueta salasanapohjaisen kirjautumisen kanssa."; +"screen_login_error_unsupported_authentication" = "Valitsemasi kotipalvelin ei tue salasana- tai OIDC-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin."; +"screen_login_form_header" = "Syötä tietosi"; +"screen_login_title" = "Tervetuloa takaisin!"; +"screen_login_title_with_homeserver" = "Kirjaudu sisään %1$@ -palvelimelle"; +"screen_media_picker_error_failed_selection" = "Median valinta epäonnistui, yritä uudelleen."; +"screen_migration_message" = "Tämä on kertaluonteinen prosessi, kiitos odottamisesta."; +"screen_migration_title" = "Tiliä määritetään."; +"screen_notification_optin_subtitle" = "Voit muuttaa asetuksia myöhemmin."; +"screen_notification_optin_title" = "Salli ilmoitukset ja älä koskaan missaa viestejä"; +"screen_notification_settings_additional_settings_section_title" = "Lisäasetukset"; +"screen_notification_settings_calls_label" = "Ääni- ja videopuheluista"; +"screen_notification_settings_configuration_mismatch" = "Konfiguraatio ei täsmää"; +"screen_notification_settings_configuration_mismatch_description" = "Olemme yksinkertaistaneet ilmoitusasetuksia, jotta vaihtoehdot olisi helpompi löytää. Joitakin aiemmin valitsemiasi asetuksia ei näytetä tässä, mutta ne ovat edelleen voimassa.\n\nJos jatkat, jotkin asetukset saattavat muuttua."; +"screen_notification_settings_direct_chats" = "Yksityiskeskusteluissa"; +"screen_notification_settings_edit_custom_settings_section_title" = "Keskustelukohtaiset asetukset"; +"screen_notification_settings_edit_failed_updating_default_mode" = "Ilmoitusasetusten muokkaamisessa tapahtui virhe."; +"screen_notification_settings_edit_mode_all_messages" = "Kaikista viesteistä"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "Vain maininnoista ja avainsanoista"; +"screen_notification_settings_edit_screen_direct_section_header" = "Yksityiskeskusteluissa, ilmoita minulle"; +"screen_notification_settings_edit_screen_group_section_header" = "Ryhmäkeskusteluissa, ilmoita minulle"; +"screen_notification_settings_enable_notifications" = "Ota ilmoitukset käyttöön tällä laitteella"; +"screen_notification_settings_failed_fixing_configuration" = "Määritystä ei ole korjattu, yritä uudelleen."; +"screen_notification_settings_group_chats" = "Ryhmäkeskusteluissa"; +"screen_notification_settings_invite_for_me_label" = "Kutsut"; +"screen_notification_settings_mentions_only_disclaimer" = "Kotipalvelimesi ei tue tätä vaihtoehtoa salatuissa huoneissa, joten et ehkä saa ilmoitusta joissakin huoneissa."; +"screen_notification_settings_mode_all" = "Kaikki"; +"screen_notification_settings_mode_mentions" = "Maininnat"; +"screen_notification_settings_notification_section_title" = "Ilmoita minulle"; +"screen_notification_settings_room_mention_label" = "Ilmoita minulle @room-maininnoista"; +"screen_notification_settings_system_notifications_action_required" = "Jos haluat saada ilmoituksia, vaihda %1$@."; +"screen_notification_settings_system_notifications_action_required_content_link" = "järjestelmäsi asetuksia"; +"screen_notification_settings_system_notifications_turned_off" = "Järjestelmän ilmoitukset on poissa päältä"; +"screen_notification_settings_title" = "Ilmoitukset"; +"screen_onboarding_sign_in_manually" = "Kirjaudu sisään manuaalisesti"; +"screen_onboarding_sign_in_with_qr_code" = "Kirjaudu sisään QR-koodilla"; +"screen_onboarding_sign_up" = "Luo tili"; +"screen_onboarding_welcome_message" = "Tervetuloa kaikkien aikojen nopeimpaan %1$@ -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella."; +"screen_onboarding_welcome_subtitle" = "Tervetuloa %1$@ -sovellukseen. Ahdettu nopeudella ja yksinkertaisuudella."; +"screen_onboarding_welcome_title" = "Ole elementissäsi"; +"screen_polls_history_empty_ongoing" = "Meneillään olevia kyselyjä ei löytynyt."; +"screen_polls_history_empty_past" = "Aiempia kyselyjä ei löytynyt."; +"screen_polls_history_filter_ongoing" = "Meneillään olevat"; +"screen_polls_history_filter_past" = "Aiemmat"; +"screen_polls_history_title" = "Kyselyt"; +"screen_qr_code_login_connecting_subtitle" = "Muodostetaan turvallista yhteyttä"; +"screen_qr_code_login_connection_note_secure_state_description" = "Turvallista yhteyttä uuteen laitteeseen ei voitu muodostaa. Olemassa olevat laitteesi ovat edelleen turvassa, eikä sinun tarvitse huolehtia niistä."; +"screen_qr_code_login_connection_note_secure_state_list_header" = "Mitä nyt?"; +"screen_qr_code_login_connection_note_secure_state_list_item_1" = "Yritä kirjautua sisään uudelleen QR-koodilla, jos kyseessä oli verkko-ongelma"; +"screen_qr_code_login_connection_note_secure_state_list_item_2" = "Jos kohtaat saman ongelman, kokeile toista wifi-verkkoa tai käytä mobiilidataa wifi-yhteyden sijaan"; +"screen_qr_code_login_connection_note_secure_state_list_item_3" = "Jos tämä ei auta, kirjaudu sisään manuaalisesti"; +"screen_qr_code_login_connection_note_secure_state_title" = "Yhteys ei ole turvallinen"; +"screen_qr_code_login_device_code_subtitle" = "Sinua pyydetään antamaan tässä laitteessa näkyvät kaksi numeroa."; +"screen_qr_code_login_device_code_title" = "Kirjoita alla oleva numero toisella laitteellasi"; +"screen_qr_code_login_device_not_signed_in_scan_state_description" = "Kirjaudu sisään toisella laitteellasi ja yritä sitten uudelleen tai käytä toista laitetta, joka on jo kirjautunut sisään."; +"screen_qr_code_login_device_not_signed_in_scan_state_subtitle" = "Toinen laitteesi ei ole kirjautuneena"; +"screen_qr_code_login_error_cancelled_subtitle" = "Kirjautuminen peruutettiin toisella laitteella."; +"screen_qr_code_login_error_cancelled_title" = "Kirjautumispyyntö peruutettu"; +"screen_qr_code_login_error_declined_subtitle" = "Kirjautuminen hylättiin toisella laitteella."; +"screen_qr_code_login_error_declined_title" = "Kirjautuminen hylätty"; +"screen_qr_code_login_error_expired_subtitle" = "Kirjautuminen vanhentui. Yritä uudelleen."; +"screen_qr_code_login_error_expired_title" = "Kirjautumista ei suoritettu ajoissa"; +"screen_qr_code_login_error_linking_not_suported_subtitle" = "Toinen laitteesi ei tue kirjautumista %@ -sovellukseen QR-koodilla.\n\nYritä kirjautua sisään manuaalisesti tai skannaa QR-koodi toisella laitteella."; +"screen_qr_code_login_error_linking_not_suported_title" = "QR-koodia ei tueta"; +"screen_qr_code_login_error_sliding_sync_not_supported_subtitle" = "Palveluntarjoajasi ei tue %1$@ -sovellusta"; +"screen_qr_code_login_error_sliding_sync_not_supported_title" = "%1$@ -sovellusta ei tueta"; +"screen_qr_code_login_initial_state_button_title" = "Valmis skannaamaan"; +"screen_qr_code_login_initial_state_item_1" = "Avaa %1$@ tietokoneella"; +"screen_qr_code_login_initial_state_item_2" = "Napsauta avatariasi"; +"screen_qr_code_login_initial_state_item_3" = "Valitse %1$@"; +"screen_qr_code_login_initial_state_item_3_action" = "“Yhdistä uusi laite”"; +"screen_qr_code_login_initial_state_item_4" = "Skannaa QR-koodi tällä laitteella"; +"screen_qr_code_login_initial_state_subtitle" = "Saatavilla vain, jos palveluntarjoajasi tukee sitä."; +"screen_qr_code_login_initial_state_title" = "Avaa %1$@ toisella laitteella saadaksesi QR-koodin"; +"screen_qr_code_login_invalid_scan_state_description" = "Käytä toisessa laitteessa näkyvää QR-koodia."; +"screen_qr_code_login_invalid_scan_state_subtitle" = "Väärä QR-koodi"; +"screen_qr_code_login_no_camera_permission_button" = "Siirry kameran asetuksiin"; +"screen_qr_code_login_no_camera_permission_state_description" = "Jatkaaksesi sinun on annettava lupa %1$@ -sovellukselle käyttää laitteesi kameraa."; +"screen_qr_code_login_no_camera_permission_state_title" = "Salli lupa kameraan QR-koodin skannaamiseksi"; +"screen_qr_code_login_scanning_state_title" = "Skannaa QR-koodi"; +"screen_qr_code_login_start_over_button" = "Aloita alusta"; +"screen_qr_code_login_unknown_error_description" = "Tapahtui odottamaton virhe. Yritä uudelleen."; +"screen_qr_code_login_verify_code_loading" = "Odotetaan toista laitettasi"; +"screen_qr_code_login_verify_code_subtitle" = "Palveluntarjoajasi saattaa kysyä seuraavaa koodia kirjautumisen vahvistamiseksi."; +"screen_qr_code_login_verify_code_title" = "Vahvistuskoodisi"; +"screen_recovery_key_change_description" = "Hanki uusi palautusavain, jos olet kadottanut nykyisen avaimen. Palautusavaimen vaihtamisen jälkeen vanha avaimesi ei enää toimi."; +"screen_recovery_key_change_generate_key" = "Luo uusi palautusavain"; +"screen_recovery_key_change_success" = "Palautusavain vaihdettu"; +"screen_recovery_key_change_title" = "Vaihda palautusavain?"; +"screen_recovery_key_confirm_create_new_recovery_key" = "Luo uusi palautusavain"; +"screen_recovery_key_confirm_description" = "Varmista, ettei kukaan näe tätä ruutua!"; +"screen_recovery_key_confirm_error_content" = "Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen."; +"screen_recovery_key_confirm_error_title" = "Väärä palautusavain"; +"screen_recovery_key_confirm_key_description" = "Jos sinulla on turva-avain tai turvalause, sekin toimii."; +"screen_recovery_key_confirm_key_placeholder" = "Syötä..."; +"screen_recovery_key_confirm_lost_recovery_key" = "Hukkasitko palautusavaimesi?"; +"screen_recovery_key_confirm_success" = "Palautusavain vahvistettu"; +"screen_recovery_key_copied_to_clipboard" = "Palautusavain kopioitu"; +"screen_recovery_key_generating_key" = "Luodaan..."; +"screen_recovery_key_save_action" = "Tallenna palautusavain"; +"screen_recovery_key_save_description" = "Kirjoita tämä palautusavain turvalliseen paikkaan, kuten salasanojen hallintaohjelmaan, salattuun muistiinpanoon tai fyysiseen kassakaappiin."; +"screen_recovery_key_save_key_description" = "Kopioi palautusavain napauttamalla"; +"screen_recovery_key_save_title" = "Tallenna palautusavain turvalliseen paikkaan"; +"screen_recovery_key_setup_confirmation_description" = "Et voi palata katsomaan uutta palautusavaintasi uudelleen tämän vaiheen jälkeen."; +"screen_recovery_key_setup_confirmation_title" = "Oletko tallentanut palautusavaimesi?"; +"screen_recovery_key_setup_description" = "Avainten säilytys on suojattu palautusavaimella. Jos tarvitset uuden palautusavaimen tämän jälkeen, voit luoda uuden valitsemalla ‘Vaihda palautusavain’."; +"screen_recovery_key_setup_generate_key" = "Luo palautusavaimesi"; +"screen_recovery_key_setup_generate_key_description" = "Älä jaa tätä kenenkään kanssa!"; +"screen_recovery_key_setup_success" = "Palautuksen käyttöönotto onnistui"; +"screen_recovery_key_setup_title" = "Ota palautus käyttöön"; +"screen_report_content_block_user_hint" = "Valitse tämä, jos haluat piilottaa kaikki nykyiset ja tulevat viestit tältä käyttäjältä"; +"screen_report_content_explanation" = "Tämä viesti ilmoitetaan kotipalvelimesi ylläpitäjälle. Ylläpitäjä ei pysty lukemaan salattuja viestejä."; +"screen_report_content_hint" = "Syy tämän sisällön ilmoittamiseen"; +"screen_reset_encryption_confirmation_alert_action" = "Kyllä, nollaa nyt"; +"screen_reset_encryption_confirmation_alert_subtitle" = "Tätä prosessia ei voi peruuttaa."; +"screen_reset_encryption_confirmation_alert_title" = "Haluatko varmasti nollata identiteettisi?"; +"screen_reset_encryption_password_subtitle" = "Vahvista, että haluat nollata identiteettisi."; +"screen_reset_encryption_password_title" = "Kirjoita tilisi salasana jatkaaksesi"; +"screen_reset_identity_confirmation_subtitle" = "Olet siirtymässä %1$@ -tilillesi nollaamaan identiteettisi. Tämän jälkeen sinut ohjataan takaisin sovellukseen."; +"screen_reset_identity_confirmation_title" = "Etkö voi vahvistaa? Siirry tilillesi ja nollaa identiteettisi."; +"screen_room_alias_resolver_resolve_alias_failure" = "Huoneen aliaksen ratkaiseminen epäonnistui."; +"screen_room_attachment_source_camera" = "Kamera"; +"screen_room_attachment_source_camera_video" = "Nauhoita video"; +"screen_room_attachment_source_files" = "Liite"; +"screen_room_attachment_source_gallery" = "Kuva- ja videokirjasto"; +"screen_room_attachment_source_location" = "Sijainti"; +"screen_room_attachment_source_poll" = "Kysely"; +"screen_room_attachment_text_formatting" = "Tekstin muotoilu"; +"screen_room_change_permissions_administrators" = "Vain ylläpitäjät"; +"screen_room_change_permissions_ban_people" = "Porttikieltojen antaminen"; +"screen_room_change_permissions_delete_messages" = "Viestien poistaminen"; +"screen_room_change_permissions_invite_people" = "Kutsujen antaminen"; +"screen_room_change_permissions_moderators" = "Ylläpitäjät ja valvojat"; +"screen_room_change_permissions_remove_people" = "Henkilöiden poistaminen"; +"screen_room_change_permissions_room_avatar" = "Huoneen avatarin vaihtaminen"; +"screen_room_change_permissions_room_name" = "Huoneen nimen vaihtaminen"; +"screen_room_change_permissions_room_topic" = "Huoneen aiheen vaihtaminen"; +"screen_room_change_permissions_send_messages" = "Viestien lähettäminen"; +"screen_room_change_role_administrators_title" = "Muokkaa ylläpitäjiä"; +"screen_room_change_role_confirm_add_admin_description" = "Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."; +"screen_room_change_role_confirm_add_admin_title" = "Lisää ylläpitäjä?"; +"screen_room_change_role_confirm_demote_self_action" = "Alenna"; +"screen_room_change_role_confirm_demote_self_description" = "Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."; +"screen_room_change_role_confirm_demote_self_title" = "Alenna itsesi?"; +"screen_room_change_role_invited_member_name" = "%1$@ (Kutsuttu)"; +"screen_room_change_role_moderators_admin_section_footer" = "Ylläpitäjillä on automaattisesti valvojan oikeudet"; +"screen_room_change_role_moderators_title" = "Muokkaa valvojia"; +"screen_room_change_role_unsaved_changes_description" = "Sinulla on tallentamattomia muutoksia"; +"screen_room_details_add_topic_title" = "Lisää aihe"; +"screen_room_details_already_a_member" = "On jo jäsen"; +"screen_room_details_already_invited" = "On jo kutsuttu"; +"screen_room_details_badge_encrypted" = "Salattu"; +"screen_room_details_badge_not_encrypted" = "Ei salattu"; +"screen_room_details_badge_public" = "Julkinen huone"; +"screen_room_details_edit_room_title" = "Muokkaa huonetta"; +"screen_room_details_edition_error" = "Tuntematon virhe tapahtui, eikä tietoja voitu muuttaa."; +"screen_room_details_edition_error_title" = "Huoneen muokkaaminen ei onnistunut"; +"screen_room_details_encryption_enabled_subtitle" = "Viestisi suojataan lukoilla. Vain sinulla ja viesiten vastaanottajilla on uniikit avaimet niiden avaamiseen."; +"screen_room_details_encryption_enabled_title" = "Viestien salaus käytössä"; +"screen_room_details_error_loading_notification_settings" = "Ilmoitusasetuksia ladattaessa tapahtui virhe."; +"screen_room_details_error_muting" = "Tämän huoneen mykistäminen epäonnistui, yritä uudelleen."; +"screen_room_details_error_unmuting" = "Tämän huoneen mykistyksen poistaminen epäonnistui, yritä uudelleen."; +"screen_room_details_notification_mode_custom" = "Mukautettu"; +"screen_room_details_notification_mode_default" = "Oletus"; +"screen_room_details_share_room_title" = "Jaa huone"; +"screen_room_details_title" = "Huoneen tiedot"; +"screen_room_details_updating_room" = "Muokataan huonetta..."; +"screen_room_directory_search_loading_error" = "Lataus epäonnistui"; +"screen_room_directory_search_title" = "Huoneluettelo"; +"screen_room_encrypted_history_banner" = "Viestihistoria ei ole tällä hetkellä saatavilla"; +"screen_room_encrypted_history_banner_unverified" = "Viestihistoria ei ole käytettävissä tässä huoneessa. Vahvista tämä laite nähdäksesi viestihistoriasi."; +"screen_room_error_failed_retrieving_user_details" = "Käyttäjän tietojen hakeminen epäonnistui"; +"screen_room_invite_again_alert_message" = "Haluatko kutsua heidät takaisin?"; +"screen_room_invite_again_alert_title" = "Olet yksin tässä keskustelussa"; +"screen_room_member_details_block_alert_action" = "Estä"; +"screen_room_member_details_block_alert_description" = "Estetyt käyttäjät eivät voi lähettää sinulle viestejä ja kaikki heidän viestit piilotetaan. Voit poistaa eston milloin tahansa."; +"screen_room_member_details_block_user" = "Estä käyttäjä"; +"screen_room_member_details_title" = "Profiili"; +"screen_room_member_details_unblock_alert_action" = "Poista esto"; +"screen_room_member_details_unblock_alert_description" = "Näet jälleen kaikki heidän lähettämänsä viestit."; +"screen_room_member_details_unblock_user" = "Poista käyttäjän esto"; +"screen_room_member_details_verify_button_subtitle" = "Vahvista tämä käyttäjä verkkosovelluksen avulla."; +"screen_room_member_details_verify_button_title" = "Vahvista %1$@"; +"screen_room_member_list_ban_member_confirmation_action" = "Anna porttikielto"; +"screen_room_member_list_ban_member_confirmation_description" = "He eivät voi enää liittyä tähän huoneeseen, jos heidät kutsutaan."; +"screen_room_member_list_ban_member_confirmation_title" = "Haluatko varmasti antaa tälle jäsenelle porttikiellon?"; +"screen_room_member_list_banned_empty" = "Tässä huoneessa ei ole porttikieltoja"; +"screen_room_member_list_banning_user" = "Annetaan porttikieltoa käyttäjälle %1$@"; +"screen_room_member_list_manage_member_ban" = "Poista jäsen huoneesta ja anna porttikielto"; +"screen_room_member_list_manage_member_remove" = "Poista huoneesta"; +"screen_room_member_list_manage_member_remove_confirmation_kick" = "Poista vain jäsen huoneesta"; +"screen_room_member_list_manage_member_remove_confirmation_title" = "Poistetaanko jäsen huoneesta ja kielletäänkö heitä liittymästä tulevaisuudessa?"; +"screen_room_member_list_manage_member_unban_action" = "Poista porttikielto"; +"screen_room_member_list_manage_member_unban_message" = "He voivat liittyä tähän huoneeseen uudelleen, jos heidät kutsutaan."; +"screen_room_member_list_manage_member_unban_title" = "Poista käyttäjän porttikielto"; +"screen_room_member_list_manage_member_user_info" = "Näytä profiili"; +"screen_room_member_list_mode_banned" = "Porttikiellot"; +"screen_room_member_list_mode_members" = "Jäsenet"; +"screen_room_member_list_pending_header_title" = "Kutsutut"; +"screen_room_member_list_removing_user" = "Poistetaan käyttäjää %1$@ huoneesta..."; +"screen_room_member_list_role_administrator" = "Ylläpitäjä"; +"screen_room_member_list_role_moderator" = "Valvoja"; +"screen_room_member_list_room_members_header_title" = "Huoneen jäsenet"; +"screen_room_member_list_unbanning_user" = "Poistetaan käyttäjän %1$@ porttikieltoa"; +"screen_room_notification_settings_allow_custom" = "Salli mukautettu asetus"; +"screen_room_notification_settings_allow_custom_footnote" = "Tämän ottaminen käyttöön ohittaa oletusasetuksesi"; +"screen_room_notification_settings_custom_settings_title" = "Ilmoita minulle tässä keskustelussa"; +"screen_room_notification_settings_default_setting_footnote" = "Voit muuttaa sen %1$@."; +"screen_room_notification_settings_default_setting_footnote_content_link" = "yleisissä asetuksissa"; +"screen_room_notification_settings_default_setting_title" = "Oletusasetus"; +"screen_room_notification_settings_edit_remove_setting" = "Poista mukautettu asetus"; +"screen_room_notification_settings_error_loading_settings" = "Ilmoitusasetusten lataamisessa tapahtui virhe."; +"screen_room_notification_settings_error_restoring_default" = "Oletustilan palauttaminen epäonnistui, yritä uudelleen."; +"screen_room_notification_settings_error_setting_mode" = "Tilan asettaminen epäonnistui, yritä uudelleen."; +"screen_room_notification_settings_mentions_only_disclaimer" = "Kotipalvelimesi ei tue tätä vaihtoehtoa salatuissa huoneissa, joten et saa ilmoituksia tästä huoneesta."; +"screen_room_notification_settings_mode_all_messages" = "Kaikista viesteistä"; +"screen_room_notification_settings_room_custom_settings_title" = "Ilmoita minulle tässä huoneessa"; +"screen_room_retry_send_menu_send_again_action" = "Lähetä uudelleen"; +"screen_room_retry_send_menu_title" = "Viestisi lähettäminen epäonnistui"; +"screen_room_roles_and_permissions_admins" = "Ylläpitäjät"; +"screen_room_roles_and_permissions_change_my_role" = "Vaihda rooliani"; +"screen_room_roles_and_permissions_change_role_demote_to_member" = "Alenna jäseneksi"; +"screen_room_roles_and_permissions_change_role_demote_to_moderator" = "Alenna valvojaksi"; +"screen_room_roles_and_permissions_member_moderation" = "Jäsenten valvonta"; +"screen_room_roles_and_permissions_messages_and_content" = "Viestit ja sisältö"; +"screen_room_roles_and_permissions_moderators" = "Valvojat"; +"screen_room_roles_and_permissions_permissions_header" = "Oikeudet"; +"screen_room_roles_and_permissions_reset" = "Nollaa oikeudet"; +"screen_room_roles_and_permissions_reset_confirm_description" = "Kun nollaat käyttöoikeudet, menetät nykyiset asetukset."; +"screen_room_roles_and_permissions_reset_confirm_title" = "Nollataanko oikeudet?"; +"screen_room_roles_and_permissions_roles_header" = "Roolit"; +"screen_room_roles_and_permissions_room_details" = "Huoneen tiedot"; +"screen_room_roles_and_permissions_title" = "Roolit ja oikeudet"; +"screen_room_timeline_add_reaction" = "Lisää emoji"; +"screen_room_timeline_beginning_of_room" = "Tämä on huoneen %1$@ alku."; +"screen_room_timeline_beginning_of_room_no_name" = "Tämä on tämän keskustelun alku."; +"screen_room_timeline_legacy_call" = "Puhelu, jota ei tueta. Kysy, voiko soittaja käyttää uutta Element X -sovellusta."; +"screen_room_timeline_less_reactions" = "Näytä vähemmän"; +"screen_room_timeline_message_copied" = "Viesti kopioitu"; +"screen_room_timeline_no_permission_to_post" = "Sinulla ei ole oikeutta kirjoittaa tässä huoneessa"; +"screen_room_timeline_reactions_show_more" = "Näytä lisää"; +"screen_room_timeline_read_marker_title" = "Uusi"; +"screen_room_title" = "Keskustelu"; +"screen_room_typing_many_members_first_component_ios" = "%1$@, %2$@ ja "; +"screen_room_typing_notification_plural_ios" = " kirjoittavat..."; +"screen_room_typing_notification_singular_ios" = " kirjoittaa..."; +"screen_room_typing_two_members" = "%1$@ ja %2$@"; +"screen_room_voice_message_tooltip" = "Pidä pohjassa nauhoittaaksesi"; +"screen_roomlist_a11y_create_message" = "Luo uusi keskustelu tai huone"; +"screen_roomlist_empty_message" = "Aloita lähettämällä viesti jollekin."; +"screen_roomlist_empty_title" = "Sinulla ei ole vielä keskusteluja."; +"screen_roomlist_filter_favourites" = "Suosikit"; +"screen_roomlist_filter_favourites_empty_state_subtitle" = "Voit lisätä keskustelun suosikkeihisi keskustelun asetuksissa.\nToistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."; +"screen_roomlist_filter_favourites_empty_state_title" = "Sinulla ei ole vielä suosikkikeskusteluja"; +"screen_roomlist_filter_invites" = "Kutsut"; +"screen_roomlist_filter_invites_empty_state_title" = "Sinulla ei ole yhtään odottavaa kutsua."; +"screen_roomlist_filter_low_priority" = "Matala prioriteetti"; +"screen_roomlist_filter_mixed_empty_state_subtitle" = "Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi."; +"screen_roomlist_filter_mixed_empty_state_title" = "Sinulla ei ole sopivia keskusteluja tähän valintaan"; +"screen_roomlist_filter_people_empty_state_title" = "Sinulla ei ole vielä yhtään yksityisviestiä"; +"screen_roomlist_filter_rooms" = "Huoneet"; +"screen_roomlist_filter_rooms_empty_state_title" = "Et ole vielä missään huoneessa"; +"screen_roomlist_filter_unreads" = "Lukemattomat"; +"screen_roomlist_filter_unreads_empty_state_title" = "Onnittelut!\nSinulla ei ole lukemattomia viestejä!"; +"screen_roomlist_main_space_title" = "Keskustelut"; +"screen_roomlist_mark_as_read" = "Merkitse luetuksi"; +"screen_roomlist_mark_as_unread" = "Merkitse lukemattomaksi"; +"screen_roomlist_room_directory_button_title" = "Selaa kaikkia huoneita"; +"screen_server_confirmation_message_login_element_dot_io" = "Yksityinen palvelin Elementin työntekijöille."; +"screen_server_confirmation_message_login_matrix_dot_org" = "Matrix on avoin verkko turvallista, hajautettua viestintää varten."; +"screen_server_confirmation_message_register" = "Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."; +"screen_server_confirmation_title_login" = "Olet kirjautumassa sisään %1$@-palvelimelle"; +"screen_server_confirmation_title_register" = "Olet luomassa tiliä %1$@-palvelimelle"; +"screen_session_verification_cancelled_subtitle" = "Jokin ei vaikuta oikealta. Joko pyyntö aikakatkaistiin tai hylättiin."; +"screen_session_verification_compare_emojis_subtitle" = "Vahvista, että alla olevat hymiöt vastaavat toisella laitteella näkyviä hymiöitä."; +"screen_session_verification_compare_emojis_title" = "Vertaa hymiöitä"; +"screen_session_verification_compare_numbers_subtitle" = "Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."; +"screen_session_verification_compare_numbers_title" = "Vertaa numeroita"; +"screen_session_verification_complete_subtitle" = "Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."; +"screen_session_verification_enter_recovery_key" = "Käytä palautusavainta"; +"screen_session_verification_failed_subtitle" = "Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."; +"screen_session_verification_open_existing_session_subtitle" = "Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."; +"screen_session_verification_open_existing_session_title" = "Avaa laite, jossa olet jo kirjautuneena"; +"screen_session_verification_positive_button_canceled" = "Yritä vahvistusta uudelleen"; +"screen_session_verification_positive_button_initial" = "Olen valmis"; +"screen_session_verification_positive_button_verifying_ongoing" = "Odotetaan vahvistusta..."; +"screen_session_verification_ready_subtitle" = "Vertaa emojisarjaa."; +"screen_session_verification_request_accepted_subtitle" = "Vertaa hymiöitä, varmistaen että ne ovat samassa järjestyksessä."; +"screen_session_verification_request_details_timestamp" = "Kirjautui sisään"; +"screen_session_verification_request_failure_title" = "Vahvistus epäonnistui"; +"screen_session_verification_request_footer" = "Jatka vain, jos sinä aloitit tämän vahvistuksen."; +"screen_session_verification_request_subtitle" = "Vahvista toinen laite pitääksesi viestihistoriasi turvassa."; +"screen_session_verification_request_success_subtitle" = "Nyt voit lukea tai lähettää viestejä turvallisesti toisella laitteellasi."; +"screen_session_verification_request_success_title" = "Laite vahvistettu"; +"screen_session_verification_request_title" = "Vahvistus pyydetty"; +"screen_session_verification_they_dont_match" = "Ne eivät täsmää"; +"screen_session_verification_they_match" = "Ne täsmäävät"; +"screen_session_verification_use_another_device_subtitle" = "Varmista, että sovellus on avoinna toisessa laitteessa, ennen kuin aloitat vahvistuksen tästä."; +"screen_session_verification_use_another_device_title" = "Avaa sovellus toisella vahvistetulla laitteella"; +"screen_session_verification_waiting_another_device_subtitle" = "Sinun pitäisi nähdä ponnahdusikkuna toisessa laitteessa. Aloita vahvistus nyt sieltä."; +"screen_session_verification_waiting_another_device_title" = "Aloita vahvistus toisella laitteella"; +"screen_session_verification_waiting_to_accept_subtitle" = "Hyväksy vahvistuspyyntö toisella laitteella jatkaaksesi."; +"screen_session_verification_waiting_to_accept_title" = "Odotetaan pyynnön hyväksymistä"; +"screen_share_location_title" = "Jaa sijainti"; +"screen_share_my_location_action" = "Jaa sijaintini"; +"screen_share_open_apple_maps" = "Avaa Apple Mapsissa"; +"screen_share_open_google_maps" = "Avaa Google Mapsissa"; +"screen_share_open_osm_maps" = "Avaa OpenStreetMapissa"; +"screen_share_this_location_action" = "Jaa tämä sijainti"; +"screen_signed_out_reason_1" = "Olet vaihtanut salasanasi toisessa istunnossa"; +"screen_signed_out_reason_2" = "Olet poistanut istunnon toisesta istunnosta"; +"screen_signed_out_reason_3" = "Palvelimesi ylläpitäjä on mitätöinyt käyttöoikeutesi"; +"screen_signed_out_subtitle" = "Sinut on saatettu kirjata ulos jostakin alla luetellusta syystä. Kirjaudu uudelleen sisään jatkaaksesi %@ -sovelluksen käyttöä."; +"screen_signed_out_title" = "Sinut on kirjattu ulos"; +"screen_signout_confirmation_dialog_content" = "Haluatko varmasti kirjautua ulos?"; +"screen_signout_in_progress_dialog_content" = "Kirjaudutaan ulos..."; +"screen_signout_key_backup_disabled_subtitle" = "Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."; +"screen_signout_key_backup_disabled_title" = "Olet poistanut varmuuskopioinnin käytöstä"; +"screen_signout_key_backup_offline_subtitle" = "Avaimiasi varmuuskopioitiin vielä, kun menit offline-tilaan. Muodosta yhteys uudelleen, jotta avaimesi voidaan varmuuskopioida ennen uloskirjautumista."; +"screen_signout_key_backup_ongoing_subtitle" = "Odota, että tämä on valmis ennen uloskirjautumista."; +"screen_signout_key_backup_ongoing_title" = "Avaimiasi varmuuskopioidaan vielä"; +"screen_signout_recovery_disabled_subtitle" = "Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, menetät pääsyn salattuihin viesteihisi."; +"screen_signout_recovery_disabled_title" = "Palautus ei ole käytössä"; +"screen_signout_save_recovery_key_subtitle" = "Olet kirjautumassa ulos viimeisestä istunnostasi. Jos kirjaudut ulos nyt, saatat menettää pääsyn salattuihin viesteihisi."; +"screen_start_chat_error_starting_chat" = "Keskustelun aloituksessa tapahtui virhe"; +"screen_view_location_title" = "Sijainti"; +"screen_welcome_bullet_1" = "Puhelut, kyselyt, haku ja paljon muuta lisätään myöhemmin tänä vuonna."; +"screen_welcome_bullet_2" = "Salattujen huoneiden viestihistoria ei ole vielä käytettävissä."; +"screen_welcome_bullet_3" = "Haluaisimme kuulla mielipiteesi, kerro mitä mieltä olet asetuksien kautta."; +"screen_welcome_button" = "Mennään!"; +"screen_welcome_subtitle" = "Tässä on mitä sinun tarvitsee tietää:"; +"screen_welcome_title" = "Tervetuloa %1$@ -sovellukseen!"; +"session_verification_banner_message" = "Vaikuttaisi siltä, että käytät uutta laitetta. Vahvista toisella laitteella nähdäksesi salatut viestit."; +"session_verification_banner_title" = "Vahvista, että se olet sinä"; +"settings_rageshake" = "Raivostunut ravistaminen"; +"settings_rageshake_detection_threshold" = "Havaitsemiskynnys"; +"settings_version_number" = "Versio: %1$@ (%2$@)"; +"state_event_avatar_changed_too" = "(myös avatar vaihdettiin)"; +"state_event_avatar_url_changed" = "%1$@ vaihtoi avatarinsa"; +"state_event_avatar_url_changed_by_you" = "Vaihdoit avatarisi"; +"state_event_demoted_to_member" = "%1$@ alennettiin jäseneksi"; +"state_event_demoted_to_moderator" = "%1$@ alennettiin valvojaksi"; +"state_event_display_name_changed_from" = "%1$@ vaihtoi näyttönimekseen %3$@ (se oli %2$@)"; +"state_event_display_name_changed_from_by_you" = "Vaihdoit näyttönimeksesi %2$@ (se oli %1$@)"; +"state_event_display_name_removed" = "%1$@ poisti näyttönimensä (se oli %2$@)"; +"state_event_display_name_removed_by_you" = "Poistit näyttönimesi (se oli %1$@)"; +"state_event_display_name_set" = "%1$@ asetti näyttönimekseen %2$@"; +"state_event_display_name_set_by_you" = "Asetit näyttönimeksesi %1$@"; +"state_event_promoted_to_administrator" = "%1$@ ylennettiin ylläpitäjäksi"; +"state_event_promoted_to_moderator" = "%1$@ ylennettiin valvojaksi"; +"state_event_room_avatar_changed" = "%1$@ vaihtoi huoneen avatarin"; +"state_event_room_avatar_changed_by_you" = "Vaihdoit huoneen avatarin"; +"state_event_room_avatar_removed" = "%1$@ poisti huoneen avatarin"; +"state_event_room_avatar_removed_by_you" = "Poistit huoneen avatarin"; +"state_event_room_ban" = "%1$@ antoi porttikiellon käyttäjälle %2$@"; +"state_event_room_ban_by_you" = "Annoit porttikiellon käyttäjälle %1$@"; +"state_event_room_created" = "%1$@ loi huoneen"; +"state_event_room_created_by_you" = "Loit huoneen"; +"state_event_room_invite" = "%1$@ kutsui käyttäjän %2$@"; +"state_event_room_invite_accepted" = "%1$@ hyväksyi kutsun"; +"state_event_room_invite_accepted_by_you" = "Hyväksyit kutsun"; +"state_event_room_invite_by_you" = "Kutsuit käyttäjän %1$@"; +"state_event_room_invite_you" = "%1$@ kutsui sinut"; +"state_event_room_join" = "%1$@ liittyi huoneeseen"; +"state_event_room_join_by_you" = "Liityit huoneeseen"; +"state_event_room_knock" = "%1$@ pyytää liittymistä"; +"state_event_room_knock_accepted" = "%1$@ myönsi pääsyn käyttäjälle %2$@"; +"state_event_room_knock_accepted_by_you" = "Sallit käyttäjän %1$@ liittyä"; +"state_event_room_knock_by_you" = "Pyysit liittymistä"; +"state_event_room_knock_denied" = "%1$@ hylkäsi käyttäjän %2$@ liittymispyynnön"; +"state_event_room_knock_denied_by_you" = "Hylkäsit käyttäjän %1$@ liittymispyynnön"; +"state_event_room_knock_denied_you" = "%1$@ hylkäsi liittymispyyntösi"; +"state_event_room_knock_retracted" = "%1$@ ei halua enää liittyä"; +"state_event_room_knock_retracted_by_you" = "Peruutit liittymispyyntösi"; +"state_event_room_leave" = "%1$@ poistui huoneesta"; +"state_event_room_leave_by_you" = "Poistuit huoneesta"; +"state_event_room_name_changed" = "%1$@ vaihtoi huoneen nimeksi: %2$@"; +"state_event_room_name_changed_by_you" = "Vaihdoit huoneen nimeksi: %1$@"; +"state_event_room_name_removed" = "%1$@ poisti huoneen nimen"; +"state_event_room_name_removed_by_you" = "Poistit huoneen nimen"; +"state_event_room_none" = "%1$@ ei tehnyt muutoksia"; +"state_event_room_none_by_you" = "Et tehnyt muutoksia"; +"state_event_room_pinned_events_changed" = "%1$@ muutti kiinnitettyjä viestejä"; +"state_event_room_pinned_events_changed_by_you" = "Muutit kiinnitettyjä viestejä"; +"state_event_room_pinned_events_pinned" = "%1$@ kiinnitti viestin"; +"state_event_room_pinned_events_pinned_by_you" = "Kiinnitit viestin"; +"state_event_room_pinned_events_unpinned" = "%1$@ poisti viestin kiinnityksen"; +"state_event_room_pinned_events_unpinned_by_you" = "Poistit viestin kiinnityksen"; +"state_event_room_reject" = "%1$@ hylkäsi kutsun"; +"state_event_room_reject_by_you" = "Hylkäsit kutsun"; +"state_event_room_remove" = "%1$@ poisti käyttäjän %2$@"; +"state_event_room_remove_by_you" = "Poistit käyttäjän %1$@"; +"state_event_room_third_party_invite" = "%1$@ kutsui käyttäjän %2$@ huoneeseen"; +"state_event_room_third_party_invite_by_you" = "Kutsuit käyttäjän %1$@ huoneeseen"; +"state_event_room_third_party_revoked_invite" = "%1$@ peruutti käyttäjän %2$@ kutsun huoneeseen"; +"state_event_room_third_party_revoked_invite_by_you" = "Peruutit käyttäjän %1$@ kutsun huoneeseen"; +"state_event_room_topic_changed" = "%1$@ vaihtoi aiheeksi: %2$@"; +"state_event_room_topic_changed_by_you" = "Vaihdoit aiheeksi: %1$@"; +"state_event_room_topic_removed" = "%1$@ poisti huoneen aiheen"; +"state_event_room_topic_removed_by_you" = "Poistit huoneen aiheen"; +"state_event_room_unban" = "%1$@ poisti käyttäjän %2$@ porttikiellon"; +"state_event_room_unban_by_you" = "Poistit käyttäjän %1$@ porttikiellon"; +"state_event_room_unknown_membership_change" = "%1$@ teki tuntemattoman muutoksen jäsenyyteensä"; +"test_language_identifier" = "fi"; +"test_untranslated_default_language_identifier" = "en"; +"troubleshoot_notifications_entry_point_section" = "Vianmääritys"; +"troubleshoot_notifications_screen_action" = "Suorita testit"; +"troubleshoot_notifications_screen_action_again" = "Suorita testit uudelleen"; +"troubleshoot_notifications_screen_failure" = "Osa testeistä epäonnistui. Tarkista tiedot."; +"troubleshoot_notifications_screen_notice" = "Suorita testit havaitaksesi konfiguraatiossasi olevat ongelmat, joiden vuoksi ilmoitukset eivät ehkä toimi odotetulla tavalla."; +"troubleshoot_notifications_screen_quick_fix_action" = "Yritä korjata"; +"troubleshoot_notifications_screen_success" = "Kaikki testit läpäistiin onnistuneesti."; +"troubleshoot_notifications_screen_title" = "Ilmoitusten vianmääritys"; +"troubleshoot_notifications_screen_waiting" = "Jotkin testit vaativat huomiotasi. Tarkista tiedot."; +"troubleshoot_notifications_test_check_permission_description" = "Tarkistaa, että sovellus voi näyttää ilmoituksia."; +"troubleshoot_notifications_test_check_permission_title" = "Lupien tarkistus"; +"troubleshoot_notifications_test_current_push_provider_description" = "Hakee nykyisen palveluntarjoajan nimen."; +"troubleshoot_notifications_test_current_push_provider_failure" = "Push-palveluntarjoajia ei ole valittu."; +"troubleshoot_notifications_test_current_push_provider_success" = "Nykyinen push-palveluntarjoaja: %1$@."; +"troubleshoot_notifications_test_current_push_provider_title" = "Nykyinen push-palveluntarjoaja"; +"troubleshoot_notifications_test_detect_push_provider_description" = "Varmistaa, että sovelluksella on vähintään yksi push-palveluntarjoaja."; +"troubleshoot_notifications_test_detect_push_provider_failure" = "Push-palveluntarjoajia ei löytynyt."; +"troubleshoot_notifications_test_detect_push_provider_title" = "Push-palveluntarjoajien havaitseminen"; +"troubleshoot_notifications_test_display_notification_description" = "Tarkistaa, että sovellus voi näyttää ilmoituksen."; +"troubleshoot_notifications_test_display_notification_failure" = "Ilmoitusta ei ole klikattu."; +"troubleshoot_notifications_test_display_notification_permission_failure" = "Ilmoitusta ei voida näyttää."; +"troubleshoot_notifications_test_display_notification_success" = "Ilmoitusta on klikattu!"; +"troubleshoot_notifications_test_display_notification_title" = "Ilmoituksen näyttäminen"; +"troubleshoot_notifications_test_display_notification_waiting" = "Klikkaa ilmoitusta jatkaaksesi testiä."; +"troubleshoot_notifications_test_firebase_availability_description" = "Varmistaa, että Firebase on käytettävissä."; +"troubleshoot_notifications_test_firebase_availability_failure" = "Firebase ei ole saatavilla."; +"troubleshoot_notifications_test_firebase_availability_success" = "Firebase on saatavilla."; +"troubleshoot_notifications_test_firebase_availability_title" = "Firebasen tarkistus"; +"troubleshoot_notifications_test_firebase_token_description" = "Varmistaa, että Firebase token on käytettävissä."; +"troubleshoot_notifications_test_firebase_token_failure" = "Firebase token ei ole tiedossa."; +"troubleshoot_notifications_test_firebase_token_success" = "Firebase token: %1$@."; +"troubleshoot_notifications_test_firebase_token_title" = "Firebase tokenin tarkistus"; +"troubleshoot_notifications_test_push_loop_back_description" = "Varmistaa, että sovellus vastaanottaa push-ilmoituksen."; +"troubleshoot_notifications_test_push_loop_back_failure_1" = "Virhe: pusher on hylännyt pyynnön."; +"troubleshoot_notifications_test_push_loop_back_failure_2" = "Virhe: %1$@."; +"troubleshoot_notifications_test_push_loop_back_failure_3" = "Virhe, push-ilmoitusta ei voi testata."; +"troubleshoot_notifications_test_push_loop_back_failure_4" = "Virhe, aikakatkaisu push-ilmoitusta odotellessa."; +"troubleshoot_notifications_test_push_loop_back_success" = "Push-ilmoituksella kesti %1$d ms palata takaisin."; +"troubleshoot_notifications_test_push_loop_back_title" = "Testaa push-ilmoituksen paluu"; +"troubleshoot_notifications_test_unified_push_description" = "Varmistas, että UnifiedPush-jakelijat ovat käytettävissä."; +"troubleshoot_notifications_test_unified_push_failure" = "Push-jakelijoita ei löytynyt."; +"troubleshoot_notifications_test_unified_push_title" = "UnifiedPushin tarkistus"; +"a11y_poll" = "Kysely"; +"banner_set_up_recovery_submit" = "Ota palautus käyttöön"; +"dialog_title_error" = "Virhe"; +"dialog_title_success" = "Onnistui"; +"notification_fallback_content" = "Ilmoitus"; +"notification_invitation_action_join" = "Liity"; +"notification_invitation_action_reject" = "Hylkää"; +"notification_room_action_mark_as_read" = "Merkitse luetuksi"; +"notification_room_action_quick_reply" = "Pikavastaus"; +"screen_pinned_timeline_screen_title_empty" = "Kiinnitetyt viestit"; +"screen_room_mentions_at_room_title" = "Kaikki"; +"screen_account_provider_change" = "Vaihda palveluntarjoajaa"; +"screen_account_provider_signin_subtitle" = "Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."; +"screen_account_provider_signup_subtitle" = "Keskustelusi asuvat täällä — aivan kuten aivan kuten käyttäisit sähköpostipalveluntarjoajaa sähköpostiesi säilyttämiseen."; +"screen_analytics_settings_help_us_improve" = "Jaa anonyymejä käyttötietoja auttaaksesi meitä tunnistamaan ongelmat."; +"screen_analytics_settings_read_terms" = "Voit lukea kaikki ehtomme %1$@."; +"screen_analytics_settings_read_terms_content_link" = "täällä"; +"screen_blocked_users_unblock_alert_action" = "Poista esto"; +"screen_blocked_users_unblock_alert_description" = "Näet jälleen kaikki heidän lähettämänsä viestit."; +"screen_blocked_users_unblock_alert_title" = "Poista käyttäjän esto"; +"screen_bug_report_rash_logs_alert_title" = "%1$@ kaatui edellisellä käyttökerralla. Haluatko jakaa virheraportin kanssamme?"; +"screen_chat_backup_recovery_action_confirm" = "Käytä palautusavainta"; +"screen_chat_backup_recovery_action_setup" = "Ota palautus käyttöön"; +"screen_create_poll_cancel_confirmation_content_ios" = "Muutoksiasi ei tallenneta"; +"screen_create_room_add_people_title" = "Kutsu ihmisiä"; +"screen_create_room_room_name_label" = "Huoneen nimi"; +"screen_create_room_title" = "Luo huone"; +"screen_dm_details_block_alert_action" = "Estä"; +"screen_dm_details_block_alert_description" = "Estetyt käyttäjät eivät voi lähettää sinulle viestejä ja kaikki heidän viestit piilotetaan. Voit poistaa eston milloin tahansa."; +"screen_dm_details_block_user" = "Estä käyttäjä"; +"screen_dm_details_unblock_alert_action" = "Poista esto"; +"screen_dm_details_unblock_alert_description" = "Näet jälleen kaikki heidän lähettämänsä viestit."; +"screen_dm_details_unblock_user" = "Poista käyttäjän esto"; +"screen_edit_poll_delete_confirmation_title" = "Poista kysely"; +"screen_edit_poll_title" = "Muokkaa kyselyä"; +"screen_identity_use_another_device" = "Käytä toista laitetta"; +"screen_login_subtitle" = "Matrix on avoin verkko turvallista, hajautettua viestintää varten."; +"screen_notification_settings_mentions_section_title" = "Maininnat"; +"screen_qr_code_login_invalid_scan_state_retry_button" = "Yritä uudelleen"; +"screen_recovery_key_change_generate_key_description" = "Älä jaa tätä kenenkään kanssa!"; +"screen_recovery_key_confirm_title" = "Syötä palautusavaimesi"; +"screen_report_content_block_user" = "Estä käyttäjä"; +"screen_reset_encryption_password_placeholder" = "Syötä..."; +"screen_room_attachment_source_camera_photo" = "Ota kuva"; +"screen_room_change_permissions_everyone" = "Kaikki"; +"screen_room_change_permissions_member_moderation" = "Jäsenten valvonta"; +"screen_room_change_permissions_messages_and_content" = "Viestit ja sisältö"; +"screen_room_change_permissions_room_details" = "Huoneen tiedot"; +"screen_room_change_role_section_administrators" = "Ylläpitäjät"; +"screen_room_change_role_section_moderators" = "Valvojat"; +"screen_room_change_role_section_users" = "Jäsenet"; +"screen_room_change_role_unsaved_changes_title" = "Tallenna muutokset?"; +"screen_room_details_invite_people_title" = "Kutsu ihmisiä"; +"screen_room_details_leave_conversation_title" = "Poistu keskustelusta"; +"screen_room_details_leave_room_title" = "Poistu huoneesta"; +"screen_room_details_notification_title" = "Ilmoitukset"; +"screen_room_details_roles_and_permissions" = "Roolit ja oikeudet"; +"screen_room_details_room_name_label" = "Huoneen nimi"; +"screen_room_details_security_title" = "Turvallisuus"; +"screen_room_details_topic_title" = "Aihe"; +"screen_room_error_failed_processing_media" = "Median käsittely epäonnistui, yritä uudelleen."; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Poista jäsen huoneesta ja anna porttikielto"; +"screen_room_notification_settings_mode_mentions_and_keywords" = "Vain maininnoista ja avainsanoista"; +"screen_room_timeline_reactions_show_less" = "Näytä vähemmän"; +"screen_roomlist_filter_people" = "Ihmiset"; +"screen_server_confirmation_change_server" = "Vaihda palveluntarjoajaa"; +"screen_session_verification_request_failure_subtitle" = "Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."; +"screen_signout_confirmation_dialog_submit" = "Kirjaudu ulos"; +"screen_signout_confirmation_dialog_title" = "Kirjaudu ulos"; +"screen_signout_key_backup_offline_title" = "Avaimiasi varmuuskopioidaan vielä"; +"screen_signout_preference_item" = "Kirjaudu ulos"; +"screen_signout_save_recovery_key_title" = "Oletko tallentanut palautusavaimesi?"; +"troubleshoot_notifications_entry_point_title" = "Ilmoitusten vianmääritys"; diff --git a/ElementX/Resources/Localizations/fi.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/fi.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..2b6e31e2ea --- /dev/null +++ b/ElementX/Resources/Localizations/fi.lproj/Localizable.stringsdict @@ -0,0 +1,342 @@ + + + + + a11y_digits_entered + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d numero syötetty + other + %1$d numeroa syötetty + + + a11y_read_receipts_multiple_with_others + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ ja %2$d muu on lukenut viestin + other + %1$@ ja %2$d muuta on lukenut viestin + + + common_member_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d jäsen + other + %1$d jäsentä + + + common_poll_votes_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d ääni + other + %d ääntä + + + notification_compat_summary_line_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@: %2$d viesti + other + %1$@: %2$d viestiä + + + notification_compat_summary_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d ilmoitus + other + %d ilmoitusta + + + notification_invitations + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d kutsu + other + %d kutsua + + + notification_new_messages_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d uusi viesti + other + %d uutta viestiä + + + notification_unread_notified_messages + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d lukematon viesti + other + %d lukematonta viestiä + + + notification_unread_notified_messages_in_room_rooms + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d huoneessa + other + %d huoneessa + + + screen_app_lock_subtitle + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Sinulla on %1$d yritys + other + Sinulla on %1$d yritystä + + + screen_app_lock_subtitle_wrong_pin + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Väärä PIN-koodi. Sinulla on %1$d yritys jäljellä + other + Väärä PIN-koodi. Sinulla on %1$d yritystä jäljellä + + + screen_pinned_timeline_screen_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d kiinnitetty viesti + other + %1$d kiinnitettyä viestiä + + + screen_room_member_list_header_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d henkilö + other + %1$d henkilöä + + + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + + screen_room_timeline_state_changes + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d muutos huoneeseen + other + %1$d muutosta huoneeseen + + + screen_room_typing_many_members + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@, %2$@ ja %3$d muu + other + %1$@, %2$@ ja %3$d muuta + + + screen_room_typing_many_members_second_component_ios + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d muu + other + %d muuta + + + screen_room_typing_notification + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ kirjoittaa + other + %1$@ kirjoittavat + + + troubleshoot_notifications_test_detect_push_provider_success + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Löytyi %1$d push-palveluntarjoaja: %2$@ + other + Löytyi %1$d push-palveluntarjoajaa: %2$@ + + + troubleshoot_notifications_test_unified_push_success + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d jakelija löytyi: %2$@. + other + %1$d jakelijaa löytyi: %2$@. + + + + \ No newline at end of file diff --git a/ElementX/Resources/Localizations/fi.lproj/SAS.strings b/ElementX/Resources/Localizations/fi.lproj/SAS.strings new file mode 100644 index 0000000000..572c9eaf97 --- /dev/null +++ b/ElementX/Resources/Localizations/fi.lproj/SAS.strings @@ -0,0 +1,64 @@ +"aeroplane" = "Lentokone"; +"anchor" = "Ankkuri"; +"apple" = "Omena"; +"ball" = "Pallo"; +"banana" = "Banaani"; +"bell" = "Soittokello"; +"bicycle" = "Polkupyörä"; +"book" = "Kirja"; +"butterfly" = "Perhonen"; +"cactus" = "Kaktus"; +"cake" = "Kakku"; +"cat" = "Kissa"; +"clock" = "Pöytäkello"; +"cloud" = "Pilvi"; +"corn" = "Maissi"; +"dog" = "Koira"; +"elephant" = "Norsu"; +"fire" = "Tuli"; +"fish" = "Kala"; +"flag" = "Lippu"; +"flower" = "Kukka"; +"folder" = "Kansio"; +"gift" = "Lahja"; +"glasses" = "Silmälasit"; +"globe" = "Maapallo"; +"guitar" = "Kitara"; +"hammer" = "Vasara"; +"hat" = "Hattu"; +"headphones" = "Kuulokkeet"; +"heart" = "Sydän"; +"horse" = "Hevonen"; +"hourglass" = "Tiimalasi"; +"key" = "Avain"; +"light_bulb" = "Hehkulamppu"; +"lion" = "Leijona"; +"lock" = "Lukko"; +"moon" = "Kuu"; +"mushroom" = "Sieni"; +"octopus" = "Tursas"; +"panda" = "Panda"; +"paperclip" = "Paperiliitin"; +"pencil" = "Lyijykynä"; +"penguin" = "Pingviini"; +"pig" = "Sika"; +"pin" = "Nuppineula"; +"pizza" = "Pizza"; +"rabbit" = "Kani"; +"robot" = "Robotti"; +"rocket" = "Raketti"; +"rooster" = "Kukko"; +"santa" = "Joulupukki"; +"scissors" = "Sakset"; +"smiley" = "Hymynaama"; +"spanner" = "Kiintoavain"; +"strawberry" = "Mansikka"; +"telephone" = "Puhelin"; +"thumbs_up" = "Peukalo ylös"; +"train" = "Juna"; +"tree" = "Puu"; +"trophy" = "Palkinto"; +"trumpet" = "Trumpetti"; +"turtle" = "Kilpikonna"; +"umbrella" = "Sateenvarjo"; +"unicorn" = "Yksisarvinen"; \ 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 5103afcbd0..2a9b6b1165 100644 --- a/ElementX/Resources/Localizations/fr.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/fr.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Enregistrer un message vocal."; "a11y_voice_message_stop_recording" = "Arrêter l'enregistrement"; "action_accept" = "Accepter"; +"action_add_caption" = "Ajouter une légende"; "action_add_to_timeline" = "Ajouter à la discussion"; "action_back" = "Retour"; "action_call" = "Appel"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirmez le mot de passe"; "action_continue" = "Continuer"; "action_copy" = "Copier"; +"action_copy_caption" = "Copier la légende"; "action_copy_link" = "Copier le lien"; "action_copy_link_to_message" = "Copier le lien vers le message"; +"action_copy_text" = "Copier le texte"; "action_create" = "Créer"; "action_create_a_room" = "Créer un salon"; "action_deactivate" = "Désactiver"; @@ -47,6 +50,7 @@ "action_discard" = "Annuler"; "action_done" = "Terminé"; "action_edit" = "Modifier"; +"action_edit_caption" = "Modifier la légende"; "action_edit_poll" = "Modifier le sondage"; "action_enable" = "Activer"; "action_end_poll" = "Terminer le sondage"; @@ -81,6 +85,8 @@ "action_react" = "Réagissez"; "action_reject" = "Rejeter"; "action_remove" = "Supprimer"; +"action_remove_caption" = "Supprimer la légende"; +"action_remove_message" = "Supprimer le message"; "action_reply" = "Répondre"; "action_reply_in_thread" = "Répondre dans le fil de discussion"; "action_report_bug" = "Signaler un problème"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Configurer la récupération"; "common_about" = "À propos"; "common_acceptable_use_policy" = "Politique d’utilisation acceptable"; +"common_adding_caption" = "Ajout d'une légende"; "common_advanced_settings" = "Paramètres avancés"; "common_analytics" = "Statistiques d’utilisation"; "common_appearance" = "Apparence"; "common_audio" = "Audio"; "common_blocked_users" = "Utilisateurs bloqués"; "common_bubbles" = "Bulles"; -"common_call_invite" = "Appel en cours (non supporté)"; "common_call_started" = "Appel démarré"; "common_chat_backup" = "Sauvegarde des discussions"; "common_copyright" = "Droits d’auteur"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Discussion à deux"; "common_edited_suffix" = "(modifié)"; "common_editing" = "Édition"; +"common_editing_caption" = "Modification de la légende"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Chiffrement"; "common_encryption_enabled" = "Chiffrement activé"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Impossible d’envoyer une ou plusieurs invitations"; "common_unlock" = "Déverrouillage"; "common_unmute" = "Retirer la sourdine"; +"common_unsupported_call" = "Appel non pris en charge"; "common_unsupported_event" = "Événement non pris en charge"; "common_username" = "Nom d’utilisateur"; "common_verification_cancelled" = "Vérification annulée"; @@ -233,7 +241,7 @@ "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_verify_identity" = "Vérifier l'identité"; "common_video" = "Vidéo"; "common_voice_message" = "Message vocal"; "common_waiting" = "En attente..."; @@ -246,10 +254,10 @@ "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"; +"confirm_recovery_key_banner_message" = "Confirmez votre clé de récupération pour conserver l’accès à votre stockage de clés et à l’historique des messages."; +"confirm_recovery_key_banner_primary_button_title" = "Saisissez votre clé de récupération"; +"confirm_recovery_key_banner_secondary_button_title" = "Clé de récupération oubliée?"; +"confirm_recovery_key_banner_title" = "Le stockage de vos clés n'est pas synchronisé"; "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$@"; @@ -344,18 +352,20 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Légende facultative..."; "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_access_section_anyone_option_description" = "Tout le monde peut rejoindre ce salon"; +"screen_create_room_room_access_section_anyone_option_title" = "Tout le monde"; +"screen_create_room_room_access_section_header" = "Accès au salon"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Demander à rejoindre"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Certains caractères ne sont pas autorisés. Seuls les lettres, les chiffres et les symboles suivants sont utilisables ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Cette adresse de salon existe déjà, veuillez essayer de modifier le champ d'adresse de salon ou de modifier le nom du salon"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Oui, tout accepter"; +"screen_knock_requests_list_accept_all_alert_description" = "Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"; +"screen_knock_requests_list_accept_all_alert_title" = "Tout accepter"; +"screen_knock_requests_list_accept_all_button_title" = "Tout accepter"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Oui, rejeter et bannir"; +"screen_knock_requests_list_ban_alert_description" = "Êtes-vous sûr de vouloir rejeter la demande et bannir %1$@? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."; +"screen_knock_requests_list_ban_alert_title" = "Refuser et interdire l'accès"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Oui, refuser"; +"screen_knock_requests_list_decline_alert_description" = "Êtes-vous sûr de vouloir refuser la demande de %1$@ à rejoindre le salon?"; +"screen_knock_requests_list_decline_alert_title" = "Refuser l'accès"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Refuser et bannir"; +"screen_knock_requests_list_empty_state_description" = "Lorsque quelqu'un demandera à rejoindre le salon, vous pourrez voir sa demande ici."; +"screen_knock_requests_list_empty_state_title" = "Personne ne demande à rejoindre le salon"; +"screen_knock_requests_list_title" = "Demandes en attente"; +"screen_media_upload_preview_caption_warning" = "Les légendes peuvent ne pas être visibles pour les utilisateurs d'anciennes applications."; +"screen_media_upload_preview_error_failed_processing" = "Échec du traitement des médias à télécharger, veuillez réessayer."; +"screen_media_upload_preview_error_failed_sending" = "Échec du téléchargement du média, veuillez réessayer."; "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_reset_encryption_password_error" = "Une erreur s'est produite. Vérifiez que le mot de passe de votre compte est correct et réessayez."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "Tout afficher"; "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_single_knock_request_accept_button_title" = "Accepter"; +"screen_room_single_knock_request_title" = "%1$@ souhaite rejoindre ce salon"; +"screen_room_single_knock_request_view_button_title" = "Voir"; "screen_room_details_pinned_events_row_title" = "Messages épinglés"; +"screen_room_details_requests_to_join_title" = "Demandes en attente"; "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."; @@ -392,8 +424,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_media_compression_description" = "Téléchargez des photos et des vidéos plus rapidement et réduisez la consommation de données"; +"screen_advanced_settings_media_compression_title" = "Optimisez la qualité des médias"; "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."; @@ -462,12 +494,12 @@ "screen_chat_backup_key_backup_action_enable" = "Activer la 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_disabled_error" = "Le stockage des clés doit être activé pour configurer la restauration."; "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_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_confirm_description" = "Le stockage de vos clés est actuellement désynchronisé."; "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"; @@ -555,8 +587,6 @@ "screen_login_title" = "Content de vous revoir !"; "screen_login_title_with_homeserver" = "Connectez-vous à %1$@"; "screen_media_picker_error_failed_selection" = "Échec de la sélection du média, veuillez réessayer."; -"screen_media_upload_preview_error_failed_processing" = "Échec du traitement des médias à télécharger, veuillez réessayer."; -"screen_media_upload_preview_error_failed_sending" = "Échec du téléchargement du média, veuillez réessayer."; "screen_migration_message" = "Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants."; "screen_migration_title" = "Configuration de votre compte."; "screen_notification_optin_subtitle" = "Vous pourrez modifier vos paramètres ultérieurement."; @@ -642,7 +672,7 @@ "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"; "screen_recovery_key_confirm_description" = "Assurez vous que personne d'autre ne regarde votre écran!"; -"screen_recovery_key_confirm_error_content" = "Veuillez réessayer afin de pouvoir accéder à vos anciens messages."; +"screen_recovery_key_confirm_error_content" = "Veuillez réessayer pour confirmer l'accès à votre stockage de clés."; "screen_recovery_key_confirm_error_title" = "Clé de récupération incorrecte"; "screen_recovery_key_confirm_key_description" = "Si vous avez une clé de sécurité ou une phrase de sécurité, cela fonctionnera également."; "screen_recovery_key_confirm_key_placeholder" = "Saisissez la clé ici…"; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Ajouter un émoji"; "screen_room_timeline_beginning_of_room" = "Ceci est le début de %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Ceci est le début de cette conversation."; +"screen_room_timeline_legacy_call" = "Appel non pris en charge. Demandez à l'appelant s'il peut utiliser la nouvelle application Element X pour vous appeler."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Assurez-vous que l'application est ouverte sur un autre appareil avant de commencer la vérification."; +"screen_session_verification_use_another_device_title" = "Ouvrez l'application sur un autre appareil vérifié"; +"screen_session_verification_waiting_another_device_subtitle" = "Vous devriez voir une alerte sur l'autre appareil. Démarrez la vérification à partir de là dès maintenant."; +"screen_session_verification_waiting_another_device_title" = "Démarrer la vérification sur l’autre appareil"; "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."; "screen_session_verification_waiting_to_accept_title" = "En attente d’acceptation de la demande"; "screen_share_location_title" = "Partage de position"; @@ -911,7 +946,7 @@ "state_event_room_invite_you" = "%1$@ vous a invité(e)"; "state_event_room_join" = "%1$@ a rejoint le salon"; "state_event_room_join_by_you" = "Vous avez rejoint le salon"; -"state_event_room_knock" = "%1$@ a demandé à rejoindre"; +"state_event_room_knock" = "%1$@ demande à rejoindre le salon"; "state_event_room_knock_accepted" = "%1$@ a autorisé %2$@ à rejoindre"; "state_event_room_knock_accepted_by_you" = "Vous avez autorisé %1$@ à joindre le salon"; "state_event_room_knock_by_you" = "Vous avez demandé à rejoindre"; @@ -1033,7 +1068,7 @@ "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_recovery_key_confirm_title" = "Saisissez votre clé de récupération"; "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"; diff --git a/ElementX/Resources/Localizations/fr.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/fr.lproj/Localizable.stringsdict index e456503941..e12dce7a4d 100644 --- a/ElementX/Resources/Localizations/fr.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/fr.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d personnes + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ et %2$d autre personne souhaitent rejoindre ce salon + other + %1$@ et %2$d autres personnes souhaitent rejoindre ce salon + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/hu.lproj/Localizable.strings b/ElementX/Resources/Localizations/hu.lproj/Localizable.strings index 829d85237a..f6f5b11a1f 100644 --- a/ElementX/Resources/Localizations/hu.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/hu.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Hangüzenet felvétele."; "a11y_voice_message_stop_recording" = "Rögzítés leállítása"; "action_accept" = "Elfogadás"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Hozzáadás az idővonalhoz"; "action_back" = "Vissza"; "action_call" = "Hívás"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Jelszó megerősítése"; "action_continue" = "Folytatás"; "action_copy" = "Másolás"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Hivatkozás másolása"; "action_copy_link_to_message" = "Üzenetre mutató hivatkozás másolása"; +"action_copy_text" = "Copy text"; "action_create" = "Létrehozás"; "action_create_a_room" = "Szoba létrehozása"; "action_deactivate" = "Deaktiválás"; @@ -47,6 +50,7 @@ "action_discard" = "Elvetés"; "action_done" = "Kész"; "action_edit" = "Szerkesztés"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Szavazás szerkesztése"; "action_enable" = "Engedélyezés"; "action_end_poll" = "Szavazás lezárása"; @@ -81,6 +85,8 @@ "action_react" = "Reakció"; "action_reject" = "Elutasítás"; "action_remove" = "Eltávolítás"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Válasz"; "action_reply_in_thread" = "Válasz az üzenetszálban"; "action_report_bug" = "Hiba jelentése"; @@ -119,13 +125,13 @@ "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_adding_caption" = "Adding caption"; "common_advanced_settings" = "Speciális beállítások"; "common_analytics" = "Elemzések"; "common_appearance" = "Megjelenítés"; "common_audio" = "Hang"; "common_blocked_users" = "Letiltott felhasználók"; "common_bubbles" = "Buborékok"; -"common_call_invite" = "Folyamatban lévő hívás (nem támogatott)"; "common_call_started" = "A hívás elindult"; "common_chat_backup" = "Csevegés biztonsági mentése"; "common_copyright" = "Szerzői jogok"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Közvetlen csevegés"; "common_edited_suffix" = "(szerkesztve)"; "common_editing" = "Szerkesztés"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Titkosítás"; "common_encryption_enabled" = "Titkosítás engedélyezve"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Nem sikerült elküldeni a meghívót (meghívókat)"; "common_unlock" = "Feloldás"; "common_unmute" = "Némítás feloldása"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Nem támogatott esemény"; "common_username" = "Felhasználónév"; "common_verification_cancelled" = "Az ellenőrzés megszakítva"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Bárki csatlakozhat ehhez a szobához"; +"screen_create_room_room_access_section_anyone_option_title" = "Bárki"; +"screen_create_room_room_access_section_header" = "Szobahozzáférés"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Csatlakozás kérése"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."; +"screen_media_upload_preview_error_failed_sending" = "Nem sikerült a média feltöltése, próbálja újra."; "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_reset_encryption_password_error" = "Ismeretlen hiba történt. Ellenőrizze, hogy a fiókja jelszava helyes-e, és próbálja meg újra."; @@ -375,11 +402,16 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Egy vagy több eszköze nincs ellenőrizve. Így is elküldheti az üzenetet, vagy egyelőre megszakíthatja, és később, az összes eszköz ellenőrzése után újrapróbálkozhat."; "screen_resolve_send_failure_you_unsigned_device_title" = "Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte"; "screen_room_mentions_at_room_subtitle" = "Az egész szoba értesítése"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "screen_room_pinned_banner_indicator" = "%1$@ / %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ kitűzött üzenet"; "screen_room_pinned_banner_loading_description" = "Üzenet betöltése…"; "screen_room_pinned_banner_view_all_button_title" = "Összes megtekintése"; +"screen_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Kitűzött üzenetek"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Örülünk, hogy visszatért!"; "screen_login_title_with_homeserver" = "Bejelentkezés ide: %1$@"; "screen_media_picker_error_failed_selection" = "Nem sikerült kiválasztani a médiát, próbálja újra."; -"screen_media_upload_preview_error_failed_processing" = "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."; -"screen_media_upload_preview_error_failed_sending" = "Nem sikerült a média feltöltése, próbálja újra."; "screen_migration_message" = "Ez egy egyszeri folyamat, köszönjük a türelmét."; "screen_migration_title" = "A fiók beállítása."; "screen_notification_optin_subtitle" = "A beállításokat később is módosíthatja."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Emodzsi hozzáadása"; "screen_room_timeline_beginning_of_room" = "Ez a(z) %1$@ kezdete."; "screen_room_timeline_beginning_of_room_no_name" = "Ez a beszélgetés kezdete."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "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."; "screen_session_verification_waiting_to_accept_title" = "Várakozás a kérés elfogadására"; "screen_share_location_title" = "Hely megosztása"; diff --git a/ElementX/Resources/Localizations/hu.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/hu.lproj/Localizable.stringsdict index 8830225081..beebf83762 100644 --- a/ElementX/Resources/Localizations/hu.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/hu.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d személy + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/id.lproj/Localizable.strings b/ElementX/Resources/Localizations/id.lproj/Localizable.strings index b217ee3984..7f654be975 100644 --- a/ElementX/Resources/Localizations/id.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/id.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Rekam pesan suara."; "a11y_voice_message_stop_recording" = "Berhenti merekam"; "action_accept" = "Terima"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Tambahkan ke lini masa"; "action_back" = "Kembali"; "action_call" = "Panggil"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Konfirmasi kata sandi"; "action_continue" = "Lanjutkan"; "action_copy" = "Salin"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Salin tautan"; "action_copy_link_to_message" = "Salin tautan ke pesan"; +"action_copy_text" = "Copy text"; "action_create" = "Buat"; "action_create_a_room" = "Buat ruangan"; "action_deactivate" = "Nonaktifkan"; @@ -47,6 +50,7 @@ "action_discard" = "Abaikan"; "action_done" = "Selesai"; "action_edit" = "Sunting"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Sunting pemungutan suara"; "action_enable" = "Aktifkan"; "action_end_poll" = "Akhiri pemungutan suara"; @@ -81,6 +85,8 @@ "action_react" = "Bereaksi"; "action_reject" = "Tolak"; "action_remove" = "Hapus"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Balas"; "action_reply_in_thread" = "Balas dalam utas"; "action_report_bug" = "Laporkan kutu"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Siapkan pemulihan"; "common_about" = "Tentang"; "common_acceptable_use_policy" = "Kebijakan penggunaan wajar"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Pengaturan tingkat lanjut"; "common_analytics" = "Analitik"; "common_appearance" = "Penampilan"; "common_audio" = "Audio"; "common_blocked_users" = "Pengguna yang diblokir"; "common_bubbles" = "Gelembung"; -"common_call_invite" = "Panggilan sedang berjalan (tidak didukung)"; "common_call_started" = "Panggilan dimulai"; "common_chat_backup" = "Pencadangan percakapan"; "common_copyright" = "Hak cipta"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Obrolan langsung"; "common_edited_suffix" = "(disunting)"; "common_editing" = "Penyuntingan"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Enkripsi"; "common_encryption_enabled" = "Enkripsi diaktifkan"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Tidak dapat mengirim undangan"; "common_unlock" = "Buka kunci"; "common_unmute" = "Bunyikan"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Peristiwa tidak didukung"; "common_username" = "Nama pengguna"; "common_verification_cancelled" = "Verifikasi dibatalkan"; @@ -348,14 +356,16 @@ "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_create_room_room_access_section_anyone_option_description" = "Siapa pun dapat bergabung dengan ruangan ini"; +"screen_create_room_room_access_section_anyone_option_title" = "Siapa pun"; +"screen_create_room_room_access_section_header" = "Akses Ruangan"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Minta untuk bergabung"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Beberapa karakter tidak diperbolehkan. Hanya huruf, angka, dan simbol berikut didukung ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Alamat ruangan sudah ada, silakan coba sunting kolom alamat ruangan atau ubah nama ruangan"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Gagal memproses media untuk diunggah, silakan coba lagi."; +"screen_media_upload_preview_error_failed_sending" = "Gagal mengunggah media, silakan coba lagi."; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pesan yang disematkan"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Selamat datang kembali!"; "screen_login_title_with_homeserver" = "Masuk ke %1$@"; "screen_media_picker_error_failed_selection" = "Gagal memilih media, silakan coba lagi."; -"screen_media_upload_preview_error_failed_processing" = "Gagal memproses media untuk diunggah, silakan coba lagi."; -"screen_media_upload_preview_error_failed_sending" = "Gagal mengunggah media, silakan coba lagi."; "screen_migration_message" = "Ini adalah proses satu kali, terima kasih telah menunggu."; "screen_migration_title" = "Menyiapkan akun Anda."; "screen_notification_optin_subtitle" = "Anda dapat mengubah pengaturan Anda nanti."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Tambahkan emoji"; "screen_room_timeline_beginning_of_room" = "Ini adalah awal dari %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Ini adalah awal dari percakapan ini."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Pastikan Anda membuka aplikasi di perangkat lain sebelum memulai verifikasi dari sini."; +"screen_session_verification_use_another_device_title" = "Buka aplikasi di perangkat terverifikasi lain"; +"screen_session_verification_waiting_another_device_subtitle" = "Anda akan melihat popup di perangkat lain. Mulai verifikasi dari sana sekarang."; +"screen_session_verification_waiting_another_device_title" = "Mulai verifikasi di perangkat lain"; "screen_session_verification_waiting_to_accept_subtitle" = "Terima permintaan untuk memulai proses verifikasi di sesi Anda yang lain untuk melanjutkan."; "screen_session_verification_waiting_to_accept_title" = "Menunggu untuk menerima permintaan"; "screen_share_location_title" = "Bagikan lokasi"; @@ -912,7 +947,7 @@ "state_event_room_join" = "%1$@ bergabung ke ruangan"; "state_event_room_join_by_you" = "Anda bergabung ke ruangan"; "state_event_room_knock" = "%1$@ meminta untuk bergabung"; -"state_event_room_knock_accepted" = "%1$@ memperbolehkan %2$@ untuk bergabung"; +"state_event_room_knock_accepted" = "%1$@ memberikan akses kepada %2$@"; "state_event_room_knock_accepted_by_you" = "Anda memperbolehkan %1$@ untuk bergabung"; "state_event_room_knock_by_you" = "Anda meminta untuk bergabung"; "state_event_room_knock_denied" = "%1$@ menolak permintaan %2$@ untuk bergabung"; diff --git a/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict index 2787aa3503..cfe76bd928 100644 --- a/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/id.lproj/Localizable.stringsdict @@ -198,6 +198,22 @@ %1$d orang + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/it.lproj/Localizable.strings b/ElementX/Resources/Localizations/it.lproj/Localizable.strings index ef7bddaf9f..33309394d1 100644 --- a/ElementX/Resources/Localizations/it.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/it.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Registra un messaggio vocale."; "a11y_voice_message_stop_recording" = "Ferma la registrazione"; "action_accept" = "Accetta"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Aggiungi alla conversazione"; "action_back" = "Indietro"; "action_call" = "Chiama"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Conferma password"; "action_continue" = "Continua"; "action_copy" = "Copia"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copia collegamento"; "action_copy_link_to_message" = "Copia collegamento al messaggio"; +"action_copy_text" = "Copy text"; "action_create" = "Crea"; "action_create_a_room" = "Crea una stanza"; "action_deactivate" = "Disattiva"; @@ -47,6 +50,7 @@ "action_discard" = "Annulla"; "action_done" = "Fine"; "action_edit" = "Modifica"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Modifica sondaggio"; "action_enable" = "Attiva"; "action_end_poll" = "Termina sondaggio"; @@ -81,6 +85,8 @@ "action_react" = "Reagisci"; "action_reject" = "Rifiuta"; "action_remove" = "Rimuovi"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Rispondi"; "action_reply_in_thread" = "Rispondi nella discussione"; "action_report_bug" = "Segnala un problema"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Configura il ripristino"; "common_about" = "Informazioni"; "common_acceptable_use_policy" = "Regole sull'utilizzo consentito"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Impostazioni avanzate"; "common_analytics" = "Statistiche di utilizzo"; "common_appearance" = "Aspetto"; "common_audio" = "Audio"; "common_blocked_users" = "Utenti bloccati"; "common_bubbles" = "Fumetti"; -"common_call_invite" = "Chiamata in corso (non supportata)"; "common_call_started" = "Chiamata avviata"; "common_chat_backup" = "Backup della chat"; "common_copyright" = "Copyright"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Conversazione diretta"; "common_edited_suffix" = "(modificato)"; "common_editing" = "Modifica in corso"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Crittografia abilitata"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Impossibile inviare inviti"; "common_unlock" = "Sblocca"; "common_unmute" = "Annulla silenzioso"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Evento non supportato"; "common_username" = "Nome utente"; "common_verification_cancelled" = "Verifica annullata"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Elaborazione del file multimediale da caricare fallita, riprova."; +"screen_media_upload_preview_error_failed_sending" = "Caricamento del file multimediale fallito, riprova."; "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_reset_encryption_password_error" = "Si è verificato un errore sconosciuto. Controlla che la password del tuo account sia corretta e riprova."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Messaggi fissati"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Bentornato!"; "screen_login_title_with_homeserver" = "Accedi a %1$@"; "screen_media_picker_error_failed_selection" = "Selezione del file multimediale fallita, riprova."; -"screen_media_upload_preview_error_failed_processing" = "Elaborazione del file multimediale da caricare fallita, riprova."; -"screen_media_upload_preview_error_failed_sending" = "Caricamento del file multimediale fallito, riprova."; "screen_migration_message" = "Si tratta di una procedura che si effettua una sola volta, grazie per l'attesa."; "screen_migration_title" = "Configurazione del tuo account."; "screen_notification_optin_subtitle" = "Potrai modificare le tue impostazioni in seguito."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Aggiungi emoji"; "screen_room_timeline_beginning_of_room" = "Questo è l'inizio di %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Questo è l'inizio della conversazione."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "Non corrispondono"; "screen_session_verification_they_match" = "Corrispondono"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Accetta la richiesta di avviare il processo di verifica nell'altra sessione per continuare."; "screen_session_verification_waiting_to_accept_title" = "In attesa di accettare la richiesta"; "screen_share_location_title" = "Condividi posizione"; diff --git a/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict index f144b6b3be..f84b37ef64 100644 --- a/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d persone + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/ka.lproj/Localizable.strings b/ElementX/Resources/Localizations/ka.lproj/Localizable.strings index 33314f91c3..bfd0efcef3 100644 --- a/ElementX/Resources/Localizations/ka.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ka.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "ხმოვანი შეტყობინების ჩაწერა."; "a11y_voice_message_stop_recording" = "ჩაწერის შეწყვეტა"; "action_accept" = "მიღება"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "დამატება ქრონოლოგიაში"; "action_back" = "უკან"; "action_call" = "Call"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "გაგრძელება"; "action_copy" = "კოპირება"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "ბმულის კოპირება"; "action_copy_link_to_message" = "დააკოპირეთ შეტყობინების ბმული"; +"action_copy_text" = "Copy text"; "action_create" = "შექმნა"; "action_create_a_room" = "ოთახის შექმნა"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Discard"; "action_done" = "მზადაა"; "action_edit" = "რედაქტირება"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "გამოკითხვის რედაქტირება"; "action_enable" = "ჩართვა"; "action_end_poll" = "გამოკითხვის დასრულება"; @@ -81,6 +85,8 @@ "action_react" = "რეაგირება"; "action_reject" = "Reject"; "action_remove" = "წაშლა"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "პასუხი"; "action_reply_in_thread" = "პასუხი თემაში"; "action_report_bug" = "ხარვეზის შეტყობინება"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "შესახებ"; "common_acceptable_use_policy" = "მისაღები გამოყენების პოლიტიკა"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "გაფართოებული პარამეტრები"; "common_analytics" = "ანალიტიკა"; "common_appearance" = "გარეგნობა"; "common_audio" = "აუდიო"; "common_blocked_users" = "Blocked users"; "common_bubbles" = "ბუშტები"; -"common_call_invite" = "Call in progress (unsupported)"; "common_call_started" = "Call started"; "common_chat_backup" = "ჩატის სარეზერვო ასლი"; "common_copyright" = "საავტორო უფლება"; @@ -138,6 +144,7 @@ "common_direct_chat" = "პირდაპირი ჩატი"; "common_edited_suffix" = "(რედაქტირებულია)"; "common_editing" = "რედაქტირება"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "დაშიფვრა ჩართულია"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "მოწვევის (ების) გაგზავნა შეუძლებელია"; "common_unlock" = "განბლოკვა"; "common_unmute" = "დადუმების გაუქმება"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "მხარდაუჭერელი მოვლენა"; "common_username" = "მომხმარებლის სახელი"; "common_verification_cancelled" = "დადასტურება გაუქმდა"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."; +"screen_media_upload_preview_error_failed_sending" = "მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "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" = "თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "ემოჯის დამატება"; "screen_room_timeline_beginning_of_room" = "ეს არის %1$@-ს დასაწყისი."; "screen_room_timeline_beginning_of_room_no_name" = "ეს არის ამ საუბრის დასაწყისი."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "ნაკლების ჩვენება"; "screen_room_timeline_message_copied" = "შეტყობინება დაკოპირდა"; "screen_room_timeline_no_permission_to_post" = "თქვენ არ გაქვთ ამ ოთახში გამოქვეყნების ნებართვა"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "ისინი არ ემთხვევიან ერთმანეთს"; "screen_session_verification_they_match" = "ისინი ემთხვევიან ერთმანეთს"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "მიიღეთ დადასტურების მოთხოვნა თქვენს სხვა სესიაში ამ პროცესის გასაგრძელებლად."; "screen_session_verification_waiting_to_accept_title" = "მოთხოვნის მიღებას ველოდებით"; "screen_share_location_title" = "მდებარეობის გაზიარება"; diff --git a/ElementX/Resources/Localizations/ka.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/ka.lproj/Localizable.stringsdict index e7051408c9..020466b968 100644 --- a/ElementX/Resources/Localizations/ka.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/ka.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d ადამიანი + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/nl.lproj/Localizable.strings b/ElementX/Resources/Localizations/nl.lproj/Localizable.strings index a149e82a7b..4c633456bb 100644 --- a/ElementX/Resources/Localizations/nl.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/nl.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Spraakbericht opnemen."; "a11y_voice_message_stop_recording" = "Opnemen stoppen"; "action_accept" = "Accepteren"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Toevoegen aan tijdlijn"; "action_back" = "Terug"; "action_call" = "Bellen"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Bevestig wachtwoord"; "action_continue" = "Voortzetten"; "action_copy" = "Kopiëren"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Kopieer link"; "action_copy_link_to_message" = "Kopieer link naar bericht"; +"action_copy_text" = "Copy text"; "action_create" = "Aanmaken"; "action_create_a_room" = "Creëer een kamer"; "action_deactivate" = "Sluiten"; @@ -47,6 +50,7 @@ "action_discard" = "Verwerpen"; "action_done" = "Gereed"; "action_edit" = "Bewerken"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Peiling wijzigen"; "action_enable" = "Activeren"; "action_end_poll" = "Peiling beëindigen"; @@ -81,6 +85,8 @@ "action_react" = "Reageren"; "action_reject" = "Weiger"; "action_remove" = "Verwijderen"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Antwoorden"; "action_reply_in_thread" = "Antwoord in subchat"; "action_report_bug" = "Probleem melden"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Over"; "common_acceptable_use_policy" = "Beleid inzake redelijk gebruik"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Geavanceerde instellingen"; "common_analytics" = "Gebruiksgegevens"; "common_appearance" = "Weergave"; "common_audio" = "Geluid"; "common_blocked_users" = "Geblokkeerde gebruikers"; "common_bubbles" = "Bubbels"; -"common_call_invite" = "Gesprek bezig (niet ondersteund)"; "common_call_started" = "Oproep gestart"; "common_chat_backup" = "Chat back-up"; "common_copyright" = "Copyright"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Directe chat"; "common_edited_suffix" = "(bewerkt)"; "common_editing" = "Bewerken"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryptie ingeschakeld"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Kan uitnodiging(en) niet verzenden"; "common_unlock" = "Ontgrendelen"; "common_unmute" = "Dempen opheffen"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Niet-ondersteunde gebeurtenis"; "common_username" = "Gebruikersnaam"; "common_verification_cancelled" = "Verificatie geannuleerd"; @@ -290,7 +298,7 @@ "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_message" = "Pas je instellingen aan om meldingen op het volledige scherm toe te staan wanneer de telefoon is vergrendeld. Zo mis je nooit een belangrijk gesprek."; "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$@"; @@ -348,14 +356,16 @@ "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_create_room_room_access_section_anyone_option_description" = "Iedereen kan toetreden tot deze kamer"; +"screen_create_room_room_access_section_anyone_option_title" = "Iedereen"; +"screen_create_room_room_access_section_header" = "Toegang tot de kamer"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Vraag om toe te treden"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."; +"screen_media_upload_preview_error_failed_sending" = "Het uploaden van media is mislukt. Probeer het opnieuw."; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Vastgezette berichten"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Welkom terug!"; "screen_login_title_with_homeserver" = "Inloggen bij %1$@"; "screen_media_picker_error_failed_selection" = "Het selecteren van media is mislukt. Probeer het opnieuw."; -"screen_media_upload_preview_error_failed_processing" = "Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."; -"screen_media_upload_preview_error_failed_sending" = "Het uploaden van media is mislukt. Probeer het opnieuw."; "screen_migration_message" = "Dit is een eenmalig proces, bedankt voor het wachten."; "screen_migration_title" = "Je account instellen."; "screen_notification_optin_subtitle" = "Je kunt je instellingen later wijzigen."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Emoji toevoegen"; "screen_room_timeline_beginning_of_room" = "Dit is het begin van %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Dit is het begin van dit gesprek."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Accepteer het verzoek tot verificatie in je andere sessie om door te gaan."; "screen_session_verification_waiting_to_accept_title" = "Wachten om verzoek te accepteren"; "screen_share_location_title" = "Locatie delen"; diff --git a/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict index a4b7f469a6..564de20b4f 100644 --- a/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/nl.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d personen + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/pl.lproj/Localizable.strings b/ElementX/Resources/Localizations/pl.lproj/Localizable.strings index 56841a16e6..37b1f45f85 100644 --- a/ElementX/Resources/Localizations/pl.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pl.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Nagraj wiadomość głosową."; "a11y_voice_message_stop_recording" = "Zatrzymaj nagrywanie"; "action_accept" = "Akceptuj"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Dodaj do osi czasu"; "action_back" = "Wróć"; "action_call" = "Zadzwoń"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Potwierdź hasło"; "action_continue" = "Kontynuuj"; "action_copy" = "Kopiuj"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Kopiuj link"; "action_copy_link_to_message" = "Kopiuj link do wiadomości"; +"action_copy_text" = "Copy text"; "action_create" = "Utwórz"; "action_create_a_room" = "Utwórz pokój"; "action_deactivate" = "Dezaktywuj"; @@ -47,6 +50,7 @@ "action_discard" = "Odrzuć"; "action_done" = "Gotowe"; "action_edit" = "Edytuj"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Edytuj ankietę"; "action_enable" = "Włącz"; "action_end_poll" = "Zakończ ankietę"; @@ -81,6 +85,8 @@ "action_react" = "Dodaj reakcję"; "action_reject" = "Odrzuć"; "action_remove" = "Usuń"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Odpowiedz"; "action_reply_in_thread" = "Odpowiedz w wątku"; "action_report_bug" = "Zgłoś błąd"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Skonfiguruj przywracanie"; "common_about" = "O programie"; "common_acceptable_use_policy" = "Polityka użytkowania"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Ustawienia zaawansowane"; "common_analytics" = "Analityka"; "common_appearance" = "Wygląd"; "common_audio" = "Dźwięk"; "common_blocked_users" = "Zablokowani użytkownicy"; "common_bubbles" = "Bąbelki"; -"common_call_invite" = "Rozmowa w trakcie (niewspierane)"; "common_call_started" = "Rozpoczęto rozmowę"; "common_chat_backup" = "Backup czatu"; "common_copyright" = "Prawa autorskie"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Czat prywatny"; "common_edited_suffix" = "(edytowane)"; "common_editing" = "Edytowanie"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Szyfrowanie"; "common_encryption_enabled" = "Szyfrowanie włączone"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Nie można wysłać zaproszeń"; "common_unlock" = "Odblokuj"; "common_unmute" = "Wyłącz wyciszenie"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Nieobsługiwane zdarzenie"; "common_username" = "Nazwa użytkownika"; "common_verification_cancelled" = "Weryfikacja anulowana"; @@ -348,14 +356,16 @@ "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_create_room_room_access_section_anyone_option_description" = "Każdy może dołączyć do tego pokoju"; +"screen_create_room_room_access_section_anyone_option_title" = "Wszyscy"; +"screen_create_room_room_access_section_header" = "Dostęp do pokoju"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Poproś o dołączenie"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."; +"screen_media_upload_preview_error_failed_sending" = "Przesyłanie multimediów nie powiodło się, spróbuj ponownie."; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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" = "Wczytywanie wiadomości..."; "screen_room_pinned_banner_view_all_button_title" = "Wyświetl wszystkie"; +"screen_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Przypięte wiadomości"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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ń."; @@ -555,8 +587,6 @@ "screen_login_title" = "Witaj ponownie!"; "screen_login_title_with_homeserver" = "Zaloguj się do %1$@"; "screen_media_picker_error_failed_selection" = "Nie udało się wybrać multimediów. Spróbuj ponownie."; -"screen_media_upload_preview_error_failed_processing" = "Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."; -"screen_media_upload_preview_error_failed_sending" = "Przesyłanie multimediów nie powiodło się, spróbuj ponownie."; "screen_migration_message" = "Jest to jednorazowy proces, dziękujemy za czekanie."; "screen_migration_title" = "Konfigurowanie Twojego konta."; "screen_notification_optin_subtitle" = "Możesz zmienić ustawienia później."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Dodaj emoji"; "screen_room_timeline_beginning_of_room" = "To jest początek %1$@"; "screen_room_timeline_beginning_of_room_no_name" = "To jest początek tej konwersacji"; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -830,7 +861,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_failed_subtitle" = "Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."; "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ę"; @@ -842,11 +873,15 @@ "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_subtitle" = "Już możesz bezpiecznie czytać lub wysyłać wiadomości na drugim urządzeniu."; "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować."; "screen_session_verification_waiting_to_accept_title" = "Oczekiwanie na zaakceptowanie żądania"; "screen_share_location_title" = "Udostępnij lokalizację"; @@ -1059,7 +1094,7 @@ "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_session_verification_request_failure_subtitle" = "Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."; "screen_signout_confirmation_dialog_submit" = "Wyloguj"; "screen_signout_confirmation_dialog_title" = "Wyloguj"; "screen_signout_key_backup_offline_title" = "Twoje klucze są nadal archiwizowane"; diff --git a/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict index 794f787cf5..c600ba88c6 100644 --- a/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/pl.lproj/Localizable.stringsdict @@ -254,6 +254,22 @@ %1$d osób + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings index 659c5bf139..ea5cd6ded4 100644 --- a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Gravar mensagem de voz."; "a11y_voice_message_stop_recording" = "Parar gravação"; "action_accept" = "Aceitar"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Adicionar à linha do tempo"; "action_back" = "Voltar"; "action_call" = "Chamar"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Continuar"; "action_copy" = "Copiar"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copiar link"; "action_copy_link_to_message" = "Copiar link para a mensagem"; +"action_copy_text" = "Copy text"; "action_create" = "Criar"; "action_create_a_room" = "Criar uma sala"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Descartar"; "action_done" = "Concluído"; "action_edit" = "Editar"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Editar enquete"; "action_enable" = "Habilitar"; "action_end_poll" = "Encerrar enquete"; @@ -81,6 +85,8 @@ "action_react" = "Reagir"; "action_reject" = "Recusar"; "action_remove" = "Remover"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Responder"; "action_reply_in_thread" = "Responder no tópico"; "action_report_bug" = "Reportar erro"; @@ -119,13 +125,13 @@ "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_adding_caption" = "Adding caption"; "common_advanced_settings" = "Configurações avançadas"; "common_analytics" = "Telemetria"; "common_appearance" = "Aparência"; "common_audio" = "Áudio"; "common_blocked_users" = "Usuários bloqueados"; "common_bubbles" = "Bolhas"; -"common_call_invite" = "Chamada em progresso (sem suporte)"; "common_call_started" = "Chamada iniciada"; "common_chat_backup" = "Backup de conversas"; "common_copyright" = "Direitos autorais"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Conversa privada"; "common_edited_suffix" = "(editado)"; "common_editing" = "Editando"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Criptografia ativada"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Não foi possível enviar o(s) convite(s)"; "common_unlock" = "Desbloquear"; "common_unmute" = "Desmutar"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Evento não suportado"; "common_username" = "Nome do usuário"; "common_verification_cancelled" = "Verificação cancelada"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Falha ao processar mídia para upload. Tente novamente."; +"screen_media_upload_preview_error_failed_sending" = "Falha ao enviar mídia. Tente novamente."; "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."; @@ -375,11 +402,16 @@ "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" = "Notificar a sala inteira"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -461,7 +493,7 @@ "screen_chat_backup_key_backup_action_disable" = "Desativar o backup"; "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_backup_title" = "Armazenamento de chaves"; "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"; @@ -487,9 +519,9 @@ "screen_create_poll_title" = "Criar enquete"; "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 nesta sala serão criptografadas. A criptografia não pode ser desativada posteriormente."; +"screen_create_room_private_option_description" = "Apenas as pessoas convidadas podem aceder a esta sala. Todas as mensagens são criptografadas de ponta a ponta."; "screen_create_room_private_option_title" = "Sala privativa (somente por convite)"; -"screen_create_room_public_option_description" = "As mensagens não serão criptografadas e qualquer pessoa pode lê-las. Você pode ativar a criptografia posteriormente."; +"screen_create_room_public_option_description" = "Qualquer um pode encontrar esta sala.\nVocê pode mudar isso a qualquer momento nas configurações da sala."; "screen_create_room_public_option_title" = "Sala pública (qualquer pessoa)"; "screen_create_room_topic_label" = "Tópico (opcional)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; @@ -555,8 +587,6 @@ "screen_login_title" = "Bem-vindo de volta!"; "screen_login_title_with_homeserver" = "Iniciar sessão em %1$@"; "screen_media_picker_error_failed_selection" = "Falha ao selecionar a mídia, tente novamente."; -"screen_media_upload_preview_error_failed_processing" = "Falha ao processar mídia para upload. Tente novamente."; -"screen_media_upload_preview_error_failed_sending" = "Falha ao enviar mídia. Tente novamente."; "screen_migration_message" = "Este é um processo único, obrigado por esperar."; "screen_migration_title" = "Configurando sua conta."; "screen_notification_optin_subtitle" = "Você pode alterar suas configurações mais tarde."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Adicionar emoji"; "screen_room_timeline_beginning_of_room" = "Este é o início do %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Este é o início desta conversa."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "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."; "screen_session_verification_waiting_to_accept_title" = "Aguardando para aceitar a solicitação"; "screen_share_location_title" = "Compartilhar localização"; diff --git a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.stringsdict index 7c41ae03e3..18b1f01b16 100644 --- a/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/pt-BR.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d pessoas + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/pt.lproj/Localizable.strings b/ElementX/Resources/Localizations/pt.lproj/Localizable.strings index b4a06c13f7..e0453d96d1 100644 --- a/ElementX/Resources/Localizations/pt.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/pt.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Gravar mensagem de voz."; "a11y_voice_message_stop_recording" = "Parar gravação"; "action_accept" = "Aceitar"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Adicionar à cronologia"; "action_back" = "Voltar"; "action_call" = "Chamar"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirmar palavra-passe"; "action_continue" = "Continuar"; "action_copy" = "Copiar"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copiar ligação"; "action_copy_link_to_message" = "Copiar ligação da mensagem"; +"action_copy_text" = "Copy text"; "action_create" = "Criar"; "action_create_a_room" = "Criar uma sala"; "action_deactivate" = "Desativar"; @@ -47,6 +50,7 @@ "action_discard" = "Descartar"; "action_done" = "Feito"; "action_edit" = "Editar"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Editar sondagem"; "action_enable" = "Ativar"; "action_end_poll" = "Fim da sondagem"; @@ -81,6 +85,8 @@ "action_react" = "Reagir"; "action_reject" = "Rejeitar"; "action_remove" = "Remover"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Responder"; "action_reply_in_thread" = "Responder ao tópico"; "action_report_bug" = "Comunicar problema"; @@ -115,17 +121,17 @@ "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_content" = "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes."; "banner_set_up_recovery_title" = "Configurar a recuperação"; "common_about" = "Sobre"; "common_acceptable_use_policy" = "Política de utilização aceitável"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Configurações avançadas"; "common_analytics" = "Recolha e análise de dados"; "common_appearance" = "Aparência"; "common_audio" = "Áudio"; "common_blocked_users" = "Utilizadores bloqueados"; "common_bubbles" = "Bolhas"; -"common_call_invite" = "Chamada em curso (não suportada)"; "common_call_started" = "Chamada iniciada"; "common_chat_backup" = "Cópia de segurança das conversas"; "common_copyright" = "Direitos de autor"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Conversa direta"; "common_edited_suffix" = "(editada)"; "common_editing" = "A editar"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encriptação"; "common_encryption_enabled" = "Cifragem ativada"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Não foi possível enviar convite(s)"; "common_unlock" = "Desbloquear"; "common_unmute" = "Dessilenciar"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Evento não suportado"; "common_username" = "Nome de utilizador"; "common_verification_cancelled" = "Verificação cancelada"; @@ -243,7 +251,7 @@ "common.open_source_licenses" = "Licenças de código aberto"; "common.pinned" = "Afixado"; "common.send_to" = "Enviar para"; -"common.you" = "Você"; +"common.you" = "Tu"; "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."; @@ -348,14 +356,16 @@ "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_create_room_room_access_section_anyone_option_description" = "Qualquer pessoa pode entrar nesta sala"; +"screen_create_room_room_access_section_anyone_option_title" = "Qualquer pessoa"; +"screen_create_room_room_access_section_header" = "Acesso à sala"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Pedir para participar"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Alguns caracteres não são permitidos. Apenas letras, dígitos e os seguintes símbolos são suportados! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Este endereço de sala já existe, tente editar o campo de endereço da sala ou altere o nome da sala"; "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?"; @@ -363,23 +373,45 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Falha ao processar multimédia para carregamento, por favor tente novamente."; +"screen_media_upload_preview_error_failed_sending" = "Falhar ao carregar multimédia, por favor tente novamente."; "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_subtitle" = "Podes retirar a tua verificação e enviar esta mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde depois de reverificares %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_subtitle" = "%1$@ está a utilizar um ou mais dispositivos não verificados. Podes enviar a mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde, depois de %2$@ ter 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_subtitle" = "Um ou mais dos teus dispositivos não foram verificados. Podes enviar a mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde, depois de teres verificado todos os teus 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_multiple_knock_requests_view_all_button_title" = "View all"; "screen_room_pinned_banner_indicator" = "%1$@ de %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ mensagens afixadas"; "screen_room_pinned_banner_loading_description" = "A carregar mensagem..."; "screen_room_pinned_banner_view_all_button_title" = "Ver todas"; +"screen_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Mensagens afixadas"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -501,7 +533,7 @@ "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_list_item_4" = "As tuas mensagens continuarão a ser visíveis para os utilizadores registados, mas não estarão disponíveis para os utilizadores novos ou não registados se optares por as apagar."; "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"; @@ -555,8 +587,6 @@ "screen_login_title" = "Bem-vindo(a) de volta!"; "screen_login_title_with_homeserver" = "Iniciar sessão em %1$@"; "screen_media_picker_error_failed_selection" = "Falha ao selecionar multimédia, por favor tente novamente."; -"screen_media_upload_preview_error_failed_processing" = "Falha ao processar multimédia para carregamento, por favor tente novamente."; -"screen_media_upload_preview_error_failed_sending" = "Falhar ao carregar multimédia, por favor tente novamente."; "screen_migration_message" = "Este processo só acontece uma única vez, obrigado por esperares."; "screen_migration_title" = "A configurar a tua conta…"; "screen_notification_optin_subtitle" = "Podes alterar as tuas definições mais tarde."; @@ -669,7 +699,7 @@ "screen_reset_encryption_confirmation_alert_title" = "Tens a certeza que pretendes repor a tua cifra?"; "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" = "Está prestes a aceder à sua conta %1$@ para repor a sua identidade. Depois, você será levado de volta ao aplicativo."; +"screen_reset_identity_confirmation_subtitle" = "Estás prestes a aceder à tua conta %1$@ para redefinir a tua identidade. Depois disso, serás levado de volta à aplicação."; "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"; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Adicionar emoji"; "screen_room_timeline_beginning_of_room" = "%1$@ começou aqui."; "screen_room_timeline_beginning_of_room_no_name" = "Esta conversa começou aqui."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Para continuar, aceita o pedido de verificação na tua outra sessão."; "screen_session_verification_waiting_to_accept_title" = "À aguardar a aceitação do pedido"; "screen_share_location_title" = "Partilhar localização"; diff --git a/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict index eb8934967a..b3c5171d73 100644 --- a/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/pt.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d pessoas + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/ro.lproj/Localizable.strings b/ElementX/Resources/Localizations/ro.lproj/Localizable.strings index fdaff848fe..dbbeb1b50a 100644 --- a/ElementX/Resources/Localizations/ro.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ro.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Înregistrați un mesaj vocal"; "a11y_voice_message_stop_recording" = "Opriți înregistrarea"; "action_accept" = "Acceptați"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Adăugați conversației"; "action_back" = "Înapoi"; "action_call" = "Apel"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Continuați"; "action_copy" = "Copiați"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Copiați linkul"; "action_copy_link_to_message" = "Copiați linkul către mesaj"; +"action_copy_text" = "Copy text"; "action_create" = "Creați"; "action_create_a_room" = "Creați o cameră"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Renunţare"; "action_done" = "Efectuat"; "action_edit" = "Editați"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Editați sondajul"; "action_enable" = "Activați"; "action_end_poll" = "Închideți sondajul"; @@ -81,6 +85,8 @@ "action_react" = "Reacționați"; "action_reject" = "Respinge"; "action_remove" = "Ștergeți"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Răspundeți"; "action_reply_in_thread" = "Răspundeți în fir"; "action_report_bug" = "Raportați o eroare"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Despre"; "common_acceptable_use_policy" = "Politică de utilizare rezonabilă"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Setări avansate"; "common_analytics" = "Analitice"; "common_appearance" = "Aspect"; "common_audio" = "Audio"; "common_blocked_users" = "Utilizatori blocați"; "common_bubbles" = "Baloane"; -"common_call_invite" = "Apel în curs (nesuportat)"; "common_call_started" = "A început un apel"; "common_chat_backup" = "Backup conversații"; "common_copyright" = "Drepturi de autor"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Chat direct"; "common_edited_suffix" = "(editat)"; "common_editing" = "Editare"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Criptare activată"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Nu s-a putut trimite invitația (invitațiile)"; "common_unlock" = "Deblocare"; "common_unmute" = "Activați sunetul"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Eveniment neacceptat"; "common_username" = "Utilizator"; "common_verification_cancelled" = "Verificare anulată"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Procesarea datelor media a eșuat, vă rugăm să încercați din nou."; +"screen_media_upload_preview_error_failed_sending" = "Încărcarea fișierelor media a eșuat, încercați din nou."; "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."; @@ -375,11 +402,16 @@ "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" = "Notificați întreaga cameră"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Bine ați revenit!"; "screen_login_title_with_homeserver" = "Conectați-vă la %1$@"; "screen_media_picker_error_failed_selection" = "Selectarea fișierelor media a eșuat, încercați din nou."; -"screen_media_upload_preview_error_failed_processing" = "Procesarea datelor media a eșuat, vă rugăm să încercați din nou."; -"screen_media_upload_preview_error_failed_sending" = "Încărcarea fișierelor media a eșuat, încercați din nou."; "screen_migration_message" = "Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare."; "screen_migration_title" = "Contul dumneavoastră se configurează"; "screen_notification_optin_subtitle" = "Puteți modifica setările mai târziu."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Adăugați emoji"; "screen_room_timeline_beginning_of_room" = "Acesta este începutul conversației %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Acesta este începutul acestei conversații."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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ă"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua."; "screen_session_verification_waiting_to_accept_title" = "Se așteptă acceptarea cererii"; "screen_share_location_title" = "Partajați locația"; diff --git a/ElementX/Resources/Localizations/ro.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/ro.lproj/Localizable.stringsdict index 18f7fe5b53..09720f2314 100644 --- a/ElementX/Resources/Localizations/ro.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/ro.lproj/Localizable.stringsdict @@ -238,6 +238,22 @@ %1$d persoane + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/ru.lproj/Localizable.strings b/ElementX/Resources/Localizations/ru.lproj/Localizable.strings index c4b1205b07..1e29541697 100644 --- a/ElementX/Resources/Localizations/ru.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/ru.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Записать голосовое сообщение."; "a11y_voice_message_stop_recording" = "Остановить запись"; "action_accept" = "Разрешить"; +"action_add_caption" = "Добавить подпись"; "action_add_to_timeline" = "Добавить в хронологию"; "action_back" = "Назад"; "action_call" = "Позвонить"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Подтвердите пароль"; "action_continue" = "Продолжить"; "action_copy" = "Копировать"; +"action_copy_caption" = "Скопировать подпись"; "action_copy_link" = "Скопировать ссылку"; "action_copy_link_to_message" = "Скопировать ссылку в сообщение"; +"action_copy_text" = "Копировать текст"; "action_create" = "Создать"; "action_create_a_room" = "Создать комнату"; "action_deactivate" = "Отключить"; @@ -47,6 +50,7 @@ "action_discard" = "Отменить"; "action_done" = "Готово"; "action_edit" = "Редактировать"; +"action_edit_caption" = "Изменить подпись"; "action_edit_poll" = "Редактировать опрос"; "action_enable" = "Включить"; "action_end_poll" = "Завершить опрос"; @@ -81,6 +85,8 @@ "action_react" = "Реакция"; "action_reject" = "Отклонить"; "action_remove" = "Удалить"; +"action_remove_caption" = "Удалить подпись"; +"action_remove_message" = "Удалить сообщение"; "action_reply" = "Ответить"; "action_reply_in_thread" = "Ответить в теме"; "action_report_bug" = "Сообщить об ошибке"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Для защиты вашего аккаунта рекомендуется настроить восстановление"; "common_about" = "О приложении"; "common_acceptable_use_policy" = "Политика допустимого использования"; +"common_adding_caption" = "Добавление подписи"; "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" = "Авторское право"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Личный чат"; "common_edited_suffix" = "(изменено)"; "common_editing" = "Редактирование"; +"common_editing_caption" = "Редактирование подписи"; "common_emote" = "%1$@%2$@"; "common_encryption" = "Шифрование"; "common_encryption_enabled" = "Шифрование включено"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Не удалось отправить приглашение(я)"; "common_unlock" = "Разблокировать"; "common_unmute" = "Вкл. звук"; +"common_unsupported_call" = "Неподдерживаемый вызов"; "common_unsupported_event" = "Неподдерживаемое событие"; "common_username" = "Имя пользователя"; "common_verification_cancelled" = "Подтверждение отменено"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Любой желающий может присоединиться к этой комнате"; +"screen_create_room_room_access_section_anyone_option_title" = "Любой"; +"screen_create_room_room_access_section_header" = "Доступ в комнату"; +"screen_create_room_room_access_section_knocking_option_description" = "Любой желающий может подать заявку на присоединение к комнате, но администратор или модератор должен будет принять запрос."; +"screen_create_room_room_access_section_knocking_option_title" = "Попросить присоединиться"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Некоторые символы не допускаются. Поддерживаются только буквы, цифры и следующие символы! $ & '() * +/; =? @ [] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Такой адрес комнаты уже существует, попробуйте отредактировать поле адреса комнаты или изменить название комнаты"; "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" = "Вы действительно хотите отменить заявку на вступление в эту комнату?"; @@ -363,6 +373,23 @@ "screen_join_room_knock_message_description" = "Сообщение (опционально)"; "screen_join_room_knock_sent_description" = "Вы получите приглашение присоединиться к комнате, как только ваш запрос будет принят."; "screen_join_room_knock_sent_title" = "Запрос на присоединение отправлен"; +"screen_knock_requests_list_accept_all_alert_confirm_button_title" = "Да, принять все"; +"screen_knock_requests_list_accept_all_alert_description" = "Вы действительно хотите принять все заявки на присоединение?"; +"screen_knock_requests_list_accept_all_alert_title" = "Принять все запросы"; +"screen_knock_requests_list_accept_all_button_title" = "Принять всё"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Да, отклонить и запретить"; +"screen_knock_requests_list_ban_alert_description" = "Вы уверен, что хочешь отклонить и запретить %1$@? Этот пользователь больше не сможет запросить доступ к этой комнате."; +"screen_knock_requests_list_ban_alert_title" = "Отклонить и запретить доступ"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Да, отклонить"; +"screen_knock_requests_list_decline_alert_description" = "Вы уверены, что хотите отклонить %1$@ запрос на присоединение к этой комнате?"; +"screen_knock_requests_list_decline_alert_title" = "Отклонить доступ"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Отклонить и запретить"; +"screen_knock_requests_list_empty_state_description" = "Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате."; +"screen_knock_requests_list_empty_state_title" = "Нет ожидающих запросов на присоединение"; +"screen_knock_requests_list_title" = "Запросы на присоединение"; +"screen_media_upload_preview_caption_warning" = "Подпись может быть не видна пользователям старых приложений."; +"screen_media_upload_preview_error_failed_processing" = "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."; +"screen_media_upload_preview_error_failed_sending" = "Не удалось загрузить медиафайлы, попробуйте еще раз."; "screen_pinned_timeline_empty_state_description" = "Нажмите на сообщение и выберите “%1$@”, чтобы добавить его сюда."; "screen_pinned_timeline_empty_state_headline" = "Закрепите важные сообщения, чтобы их можно было легко найти"; "screen_reset_encryption_password_error" = "Произошла неизвестная ошибка. Проверьте правильность пароля учетной записи и повторите попытку."; @@ -375,11 +402,16 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Одно или несколько ваших устройств не проверены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, проверив все свои устройства."; "screen_resolve_send_failure_you_unsigned_device_title" = "Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств."; "screen_room_mentions_at_room_subtitle" = "Уведомить всю комнату"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Показать все"; "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_single_knock_request_accept_button_title" = "Принять"; +"screen_room_single_knock_request_title" = "%1$@ хочет присоединиться к этой комнате"; +"screen_room_single_knock_request_view_button_title" = "Просмотр"; "screen_room_details_pinned_events_row_title" = "Закрепленные сообщения"; +"screen_room_details_requests_to_join_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$@ не проверил одно или несколько устройств."; @@ -498,7 +530,7 @@ "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_1_bold_part" = "отключена навсегда"; "screen_deactivate_account_list_item_2" = "Вы будете удалены из всех чатов."; "screen_deactivate_account_list_item_3" = "Данные вашей учётной записи будут удалены с нашего сервера идентификации."; "screen_deactivate_account_list_item_4" = "Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."; @@ -555,8 +587,6 @@ "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" = "Вы можете изменить настройки позже."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Добавить эмодзи"; "screen_room_timeline_beginning_of_room" = "Это начало %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Это начало разговора."; +"screen_room_timeline_legacy_call" = "Неподдерживаемый вызов. уточните, может ли звонящий использовать новое приложение Element X."; "screen_room_timeline_less_reactions" = "Показать меньше"; "screen_room_timeline_message_copied" = "Сообщение скопировано"; "screen_room_timeline_no_permission_to_post" = "У вас нет разрешения публиковать сообщения в этой комнате"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Запрошено подтверждение"; "screen_session_verification_they_dont_match" = "Они не совпадают"; "screen_session_verification_they_match" = "Они совпадают"; +"screen_session_verification_use_another_device_subtitle" = "Прежде чем начать проверку, убедитесь, что приложение открыто на другом устройстве."; +"screen_session_verification_use_another_device_title" = "Откройте приложение на другом проверенном устройстве"; +"screen_session_verification_waiting_another_device_subtitle" = "Вы должны увидеть всплывающее окно на другом устройстве. Начните проверку оттуда прямо сейчас."; +"screen_session_verification_waiting_another_device_title" = "Начать проверку на другом устройстве"; "screen_session_verification_waiting_to_accept_subtitle" = "Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе."; "screen_session_verification_waiting_to_accept_title" = "Ожидание принятия запроса"; "screen_share_location_title" = "Поделиться местоположением"; diff --git a/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict index 71a17080b1..b56aa9e5b9 100644 --- a/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/ru.lproj/Localizable.stringsdict @@ -254,6 +254,24 @@ %1$d пользователей + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d хочет присоединиться к этой комнате + few + %1$@ +%2$d хотят присоединиться к этой комнате + many + %1$@ +%2$d хотят присоединиться к этой комнате + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/sk.lproj/Localizable.strings b/ElementX/Resources/Localizations/sk.lproj/Localizable.strings index 1008f9615c..367a13aa32 100644 --- a/ElementX/Resources/Localizations/sk.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/sk.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Nahrať hlasovú správu."; "a11y_voice_message_stop_recording" = "Zastaviť nahrávanie"; "action_accept" = "Prijať"; +"action_add_caption" = "Pridať titulok"; "action_add_to_timeline" = "Pridať na časovú os"; "action_back" = "Späť"; "action_call" = "Zavolať"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Potvrdiť heslo"; "action_continue" = "Pokračovať"; "action_copy" = "Kopírovať"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Kopírovať odkaz"; "action_copy_link_to_message" = "Kopírovať odkaz do správy"; +"action_copy_text" = "Copy text"; "action_create" = "Vytvoriť"; "action_create_a_room" = "Vytvoriť miestnosť"; "action_deactivate" = "Deaktivovať"; @@ -47,6 +50,7 @@ "action_discard" = "Zahodiť"; "action_done" = "Hotovo"; "action_edit" = "Upraviť"; +"action_edit_caption" = "Upraviť titulok"; "action_edit_poll" = "Upraviť anketu"; "action_enable" = "Povoliť"; "action_end_poll" = "Ukončiť anketu"; @@ -81,6 +85,8 @@ "action_react" = "Reagovať"; "action_reject" = "Odmietnuť"; "action_remove" = "Odstrániť"; +"action_remove_caption" = "Odstrániť titulok"; +"action_remove_message" = "Remove message"; "action_reply" = "Odpovedať"; "action_reply_in_thread" = "Odpovedať vo vlákne"; "action_report_bug" = "Nahlásiť chybu"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Nastaviť obnovenie"; "common_about" = "O aplikácii"; "common_acceptable_use_policy" = "Zásady prijateľného používania"; +"common_adding_caption" = "Pridáva sa titulok"; "common_advanced_settings" = "Pokročilé nastavenia"; "common_analytics" = "Analytika"; "common_appearance" = "Vzhľad"; "common_audio" = "Zvuk"; "common_blocked_users" = "Blokovaní používatelia"; "common_bubbles" = "Bubliny"; -"common_call_invite" = "Prebieha hovor (nepodporované)"; "common_call_started" = "Hovor sa začal"; "common_chat_backup" = "Záloha konverzácie"; "common_copyright" = "Autorské práva"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Priama konverzácia"; "common_edited_suffix" = "(upravené)"; "common_editing" = "Upravuje sa"; +"common_editing_caption" = "Úprava titulku"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Šifrovanie"; "common_encryption_enabled" = "Šifrovanie zapnuté"; @@ -150,7 +157,7 @@ "common_favourited" = "Obľúbené"; "common_file" = "Súbor"; "common_forward_message" = "Preposlať správu"; -"common_frequently_used" = "Frequently used"; +"common_frequently_used" = "Často používané"; "common_gif" = "GIF"; "common_image" = "Obrázok"; "common_in_reply_to" = "V odpovedi na %1$@"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Nie je možné odoslať pozvánku/ky"; "common_unlock" = "Odomknúť"; "common_unmute" = "Zrušiť stlmenie zvuku"; +"common_unsupported_call" = "Nepodporovaný hovor"; "common_unsupported_event" = "Nepodporovaná udalosť"; "common_username" = "Používateľské meno"; "common_verification_cancelled" = "Overovanie zrušené"; @@ -233,7 +241,7 @@ "common_verification_failed" = "Overenie zlyhalo"; "common_verified" = "Overené"; "common_verify_device" = "Overiť zariadenie"; -"common_verify_identity" = "Verify identity"; +"common_verify_identity" = "Overiť totožnosť"; "common_video" = "Video"; "common_voice_message" = "Hlasová správa"; "common_waiting" = "Čaká sa…"; @@ -246,10 +254,10 @@ "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"; +"confirm_recovery_key_banner_message" = "Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ."; +"confirm_recovery_key_banner_primary_button_title" = "Zadajte kľúč na obnovenie"; +"confirm_recovery_key_banner_secondary_button_title" = "Zabudli ste svoj kľúč na obnovenie?"; +"confirm_recovery_key_banner_title" = "Vaše úložisko kľúčov nie je synchronizované"; "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$@"; @@ -344,18 +352,20 @@ "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…"; +"rich_text_editor_composer_caption_placeholder" = "Voliteľný titulok…"; "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_access_section_anyone_option_description" = "Do tejto miestnosti sa môže pripojiť ktokoľvek"; +"screen_create_room_room_access_section_anyone_option_title" = "Ktokoľvek"; +"screen_create_room_room_access_section_header" = "Prístup do miestnosti"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Požiadať o pripojenie"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Niektoré znaky nie sú povolené. Podporované sú iba písmená, číslice a nasledujúce symboly ! $ & '() * +/; =? @ [] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Táto adresa miestnosti už existuje, skúste upraviť pole adresy miestnosti alebo zmeňte názov miestnosti"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Prijať všetky"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Odmietnuť a zakázať"; +"screen_knock_requests_list_empty_state_description" = "Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."; +"screen_knock_requests_list_empty_state_title" = "Žiadna čakajúca žiadosť o pripojenie"; +"screen_knock_requests_list_title" = "Žiadosti o pripojenie"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."; +"screen_media_upload_preview_error_failed_sending" = "Nepodarilo sa nahrať médiá, skúste to prosím znova."; "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_reset_encryption_password_error" = "Nastala neznáma chyba. Skontrolujte, či je heslo vášho účtu správne a skúste to znova."; @@ -375,11 +402,16 @@ "screen_resolve_send_failure_you_unsigned_device_subtitle" = "Jedno alebo viac vašich zariadení nie je overených. Správu môžete odoslať aj tak, alebo môžete zatiaľ zrušiť a skúsiť to znova neskôr po overení všetkých svojich zariadení."; "screen_resolve_send_failure_you_unsigned_device_title" = "Vaša správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení"; "screen_room_mentions_at_room_subtitle" = "Informovať celú miestnosť"; +"screen_room_multiple_knock_requests_view_all_button_title" = "Zobraziť všetko"; "screen_room_pinned_banner_indicator" = "%1$@ z %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Pripnutých správ"; "screen_room_pinned_banner_loading_description" = "Načítava sa správa..."; "screen_room_pinned_banner_view_all_button_title" = "Zobraziť všetko"; +"screen_room_single_knock_request_accept_button_title" = "Prijať"; +"screen_room_single_knock_request_title" = "%1$@ chce vstúpiť do tejto miestnosti"; +"screen_room_single_knock_request_view_button_title" = "Zobraziť"; "screen_room_details_pinned_events_row_title" = "Pripnuté správy"; +"screen_room_details_requests_to_join_title" = "Žiadosti o vstup"; "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."; @@ -392,8 +424,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_media_compression_description" = "Nahrávajte fotografie a videá rýchlejšie a znížte spotrebu dát"; +"screen_advanced_settings_media_compression_title" = "Optimalizovať kvalitu médií"; "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."; @@ -462,12 +494,12 @@ "screen_chat_backup_key_backup_action_enable" = "Zapnúť 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_disabled_error" = "Úložisko kľúčov musí byť zapnuté, aby bolo možné nastaviť obnovenie."; "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_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_confirm_description" = "Vaše úložisko kľúčov nie je momentálne synchronizované."; "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"; @@ -555,8 +587,6 @@ "screen_login_title" = "Vitajte späť!"; "screen_login_title_with_homeserver" = "Prihlásiť sa do %1$@"; "screen_media_picker_error_failed_selection" = "Nepodarilo sa vybrať médium, skúste to prosím znova."; -"screen_media_upload_preview_error_failed_processing" = "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."; -"screen_media_upload_preview_error_failed_sending" = "Nepodarilo sa nahrať médiá, skúste to prosím znova."; "screen_migration_message" = "Ide o jednorazový proces, ďakujeme za trpezlivosť."; "screen_migration_title" = "Nastavenie vášho účtu."; "screen_notification_optin_subtitle" = "Svoje nastavenia môžete neskôr zmeniť."; @@ -642,7 +672,7 @@ "screen_recovery_key_change_title" = "Zmeniť kľúč na obnovenie?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Vytvoriť nový kľúč na obnovenie"; "screen_recovery_key_confirm_description" = "Uistite sa, že túto obrazovku nikto nevidí!"; -"screen_recovery_key_confirm_error_content" = "Skúste prosím znova potvrdiť prístup k vašej zálohe konverzácie."; +"screen_recovery_key_confirm_error_content" = "Skúste prosím znova potvrdiť prístup k úložisku kľúčov."; "screen_recovery_key_confirm_error_title" = "Nesprávny kľúč na obnovenie"; "screen_recovery_key_confirm_key_description" = "Ak máte bezpečnostný kľúč alebo bezpečnostnú frázu, bude to fungovať tiež."; "screen_recovery_key_confirm_key_placeholder" = "Zadať..."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Pridať emoji"; "screen_room_timeline_beginning_of_room" = "Toto je začiatok %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Toto je začiatok tejto konverzácie."; +"screen_room_timeline_legacy_call" = "Nepodporovaný hovor. Opýtajte sa, či volajúci môže použiť novú aplikáciu Element X."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Pred začatím overovania odtiaľto sa uistite, že máte aplikáciu otvorenú v inom zariadení."; +"screen_session_verification_use_another_device_title" = "Otvorte aplikáciu na inom overenom zariadení"; +"screen_session_verification_waiting_another_device_subtitle" = "Na druhom zariadení by sa malo zobraziť vyskakovacie okno. Začnite teraz overovanie odtiaľ."; +"screen_session_verification_waiting_another_device_title" = "Spustiť overovanie na druhom zariadení"; "screen_session_verification_waiting_to_accept_subtitle" = "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii."; "screen_session_verification_waiting_to_accept_title" = "Čaká sa na prijatie žiadosti"; "screen_share_location_title" = "Zdieľať polohu"; @@ -911,8 +946,8 @@ "state_event_room_invite_you" = "%1$@ vás pozval/a"; "state_event_room_join" = "%1$@ sa pripojil/a do miestnosti"; "state_event_room_join_by_you" = "Vstúpili ste do miestnosti"; -"state_event_room_knock" = "%1$@ požiadal o pripojenie"; -"state_event_room_knock_accepted" = "%1$@ umožnil/a používateľovi %2$@ pripojiť sa"; +"state_event_room_knock" = "%1$@ žiada o vstup"; +"state_event_room_knock_accepted" = "%1$@ umožnil/a vstup používateľovi %2$@"; "state_event_room_knock_accepted_by_you" = "Povolili ste používateľovi %1$@, aby sa pripojil"; "state_event_room_knock_by_you" = "Požiadali ste o pripojenie"; "state_event_room_knock_denied" = "%1$@ odmietol/a žiadosť používateľa %2$@ o vstup"; @@ -1033,7 +1068,7 @@ "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_recovery_key_confirm_title" = "Zadajte kľúč na obnovenie"; "screen_report_content_block_user" = "Zablokovať používateľa"; "screen_reset_encryption_password_placeholder" = "Zadať..."; "screen_room_attachment_source_camera_photo" = "Urobiť fotku"; diff --git a/ElementX/Resources/Localizations/sk.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/sk.lproj/Localizable.stringsdict index acd716ce57..3cf16f0d90 100644 --- a/ElementX/Resources/Localizations/sk.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/sk.lproj/Localizable.stringsdict @@ -254,6 +254,24 @@ %1$d osôb + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d ďalší chcú vstúpiť do tejto miestnosti + few + %1$@ +%2$d ďalší chcú vstúpiť do tejto miestnosti + other + %1$@ +%2$d ďalších chce vstúpiť do tejto miestnosti + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/sv.lproj/Localizable.strings b/ElementX/Resources/Localizations/sv.lproj/Localizable.strings index 579f9bff91..12ccf743e0 100644 --- a/ElementX/Resources/Localizations/sv.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/sv.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Spela in röstmeddelande."; "a11y_voice_message_stop_recording" = "Stoppa inspelning"; "action_accept" = "Godkänn"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Lägg till i tidslinjen"; "action_back" = "Tillbaka"; "action_call" = "Ring"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Fortsätt"; "action_copy" = "Kopiera"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Kopiera länk"; "action_copy_link_to_message" = "Kopiera länk till meddelande"; +"action_copy_text" = "Copy text"; "action_create" = "Skapa"; "action_create_a_room" = "Skapa ett rum"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Kassera"; "action_done" = "Klar"; "action_edit" = "Redigera"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Redigera omröstning"; "action_enable" = "Aktivera"; "action_end_poll" = "Avsluta omröstning"; @@ -81,6 +85,8 @@ "action_react" = "Reagera"; "action_reject" = "Avvisa"; "action_remove" = "Ta bort"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Svara"; "action_reply_in_thread" = "Svara i tråd"; "action_report_bug" = "Rapportera bugg"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Ställ in återställning"; "common_about" = "Om"; "common_acceptable_use_policy" = "Policy för godtagbar användning"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Avancerade inställningar"; "common_analytics" = "Analysdata"; "common_appearance" = "Utseende"; "common_audio" = "Ljud"; "common_blocked_users" = "Blockerade användare"; "common_bubbles" = "Bubblor"; -"common_call_invite" = "Samtal pågår (stöds inte)"; "common_call_started" = "Samtal startat"; "common_chat_backup" = "Chattsäkerhetskopia"; "common_copyright" = "Upphovsrätt"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Direktchatt"; "common_edited_suffix" = "(redigerad)"; "common_editing" = "Redigerar"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Kryptering aktiverad"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Kunde inte skicka inbjudningar"; "common_unlock" = "Lås upp"; "common_unmute" = "Avtysta"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Händelse som inte stöds"; "common_username" = "Användarnamn"; "common_verification_cancelled" = "Verifiering avbruten"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."; +"screen_media_upload_preview_error_failed_sending" = "Misslyckades att ladda upp media, vänligen pröva igen."; "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_reset_encryption_password_error" = "Ett okänt fel inträffade. Kontrollera att ditt kontolösenord är korrekt och försök igen."; @@ -375,11 +402,16 @@ "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" = "Meddela hela rummet"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "screen_room_pinned_banner_indicator" = "%1$@ av %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Fästa meddelanden"; "screen_room_pinned_banner_loading_description" = "Laddar meddelande …"; "screen_room_pinned_banner_view_all_button_title" = "Visa alla"; +"screen_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Fästa meddelanden"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -460,14 +492,14 @@ "screen_change_server_title" = "Välj din server"; "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_description" = "Lagra din kryptografiska identitet och dina meddelandenycklar säkert på servern. Detta gör att du kan se din meddelandehistorik på alla nya enheter. %1$@."; "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_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_confirm_description" = "Din nyckellagring är för närvarande osynkroniserad."; "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"; @@ -555,8 +587,6 @@ "screen_login_title" = "Välkommen tillbaka!"; "screen_login_title_with_homeserver" = "Logga in på %1$@"; "screen_media_picker_error_failed_selection" = "Misslyckades att välja media, vänligen pröva igen."; -"screen_media_upload_preview_error_failed_processing" = "Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."; -"screen_media_upload_preview_error_failed_sending" = "Misslyckades att ladda upp media, vänligen pröva igen."; "screen_migration_message" = "Detta är en engångsprocess, tack för att du väntar."; "screen_migration_title" = "Konfigurerar ditt konto"; "screen_notification_optin_subtitle" = "Du kan ändra dina inställningar senare."; @@ -658,7 +688,7 @@ "screen_recovery_key_setup_confirmation_title" = "Har du sparat din återställningsnyckel?"; "screen_recovery_key_setup_description" = "Din chattsäkerhetskopia skyddas av en återställningsnyckel. Om du behöver en ny återställningsnyckel efter installationen kan du återskapa genom att välja ”Byt återställningsnyckel”."; "screen_recovery_key_setup_generate_key" = "Generera din återställningsnyckel"; -"screen_recovery_key_setup_generate_key_description" = "Se till att du kan lagra din återställningsnyckel någonstans säkert"; +"screen_recovery_key_setup_generate_key_description" = "Dela inte detta med någon!"; "screen_recovery_key_setup_success" = "Konfiguration av återställning lyckades"; "screen_recovery_key_setup_title" = "Ställ in återställning"; "screen_report_content_block_user_hint" = "Markera om du vill dölja alla nuvarande och framtida meddelanden från denna användare"; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Lägg till emoji"; "screen_room_timeline_beginning_of_room" = "Det här är början på %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "Detta är början på det här samtalet."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta."; "screen_session_verification_waiting_to_accept_title" = "Väntar på att acceptera begäran"; "screen_share_location_title" = "Dela plats"; @@ -911,7 +946,7 @@ "state_event_room_invite_you" = "%1$@ bjöd in dig"; "state_event_room_join" = "%1$@ gick med i rummet"; "state_event_room_join_by_you" = "Du gick med i rummet"; -"state_event_room_knock" = "%1$@ begärde att gå med"; +"state_event_room_knock" = "%1$@ begär att gå med"; "state_event_room_knock_accepted" = "%1$@ tillät %2$@ att gå med"; "state_event_room_knock_accepted_by_you" = "Du lät %1$@ att gå med"; "state_event_room_knock_by_you" = "Du begärde att gå med"; @@ -1032,7 +1067,7 @@ "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_change_generate_key_description" = "Dela inte detta med någon!"; "screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Blockera användare"; "screen_reset_encryption_password_placeholder" = "Ange …"; diff --git a/ElementX/Resources/Localizations/sv.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/sv.lproj/Localizable.stringsdict index fe8c8bdc09..e23f92a5bc 100644 --- a/ElementX/Resources/Localizations/sv.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/sv.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$d personer + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/uk.lproj/InfoPlist.strings b/ElementX/Resources/Localizations/uk.lproj/InfoPlist.strings index bfb3c8a41b..6825b0489e 100644 --- a/ElementX/Resources/Localizations/uk.lproj/InfoPlist.strings +++ b/ElementX/Resources/Localizations/uk.lproj/InfoPlist.strings @@ -1,5 +1,5 @@ "NSCameraUsageDescription" = "Щоб зробити фото або відео і відправити їх як повідомлення, Element X потрібен доступ до камери."; "NSFaceIDUsageDescription" = "Face ID використовується для доступу до вашого застосунку."; -"NSLocationWhenInUseUsageDescription" = "Надайте доступ до місцезнаходження, щоб Element X міг поділитися вашим місцезнаходженням."; -"NSMicrophoneUsageDescription" = "Щоб записувати та надсилати повідомлення зі звуком, Element X потрібен доступ до мікрофону."; -"NSPhotoLibraryUsageDescription" = "Дозволяє зберігати фотографії та відео у вашій бібліотеці."; +"NSLocationWhenInUseUsageDescription" = "Надайте доступ до місцеперебування, щоб Element X міг поділитися ним."; +"NSMicrophoneUsageDescription" = "Щоб записувати та надсилати повідомлення зі звуком, Element X потрібен доступ до мікрофона."; +"NSPhotoLibraryUsageDescription" = "Дозволяє зберігати світлини та відео у вашій бібліотеці."; diff --git a/ElementX/Resources/Localizations/uk.lproj/Localizable.strings b/ElementX/Resources/Localizations/uk.lproj/Localizable.strings index 6e5b02cfdd..b2b8377d92 100644 --- a/ElementX/Resources/Localizations/uk.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/uk.lproj/Localizable.strings @@ -1,9 +1,9 @@ "Notification" = "Сповіщення"; "a11y_delete" = "Видалити"; -"a11y_hide_password" = "Приховати пароль"; -"a11y_jump_to_bottom" = "Перейти до низу"; +"a11y_hide_password" = "Cховати пароль"; +"a11y_jump_to_bottom" = "Перейти вниз"; "a11y_notifications_mentions_only" = "Тільки згадки"; -"a11y_notifications_muted" = "Приглушений"; +"a11y_notifications_muted" = "Звук вимкнено"; "a11y_page_n" = "Сторінка %1$d"; "a11y_pause" = "Пауза"; "a11y_pin_field" = "Поле PIN-коду"; @@ -17,36 +17,40 @@ "a11y_remove_reaction_with" = "Видалити реакцію з %1$@"; "a11y_send_files" = "Надіслати файли"; "a11y_show_password" = "Показати пароль"; -"a11y_start_call" = "Розпочати дзвінок"; +"a11y_start_call" = "Розпочати виклик"; "a11y_user_menu" = "Меню користувача"; "a11y_voice_message_record" = "Записати голосове повідомлення."; "a11y_voice_message_stop_recording" = "Припинити запис"; "action_accept" = "Прийняти"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Додати до стрічки"; "action_back" = "Назад"; "action_call" = "Зателефонувати"; "action_cancel" = "Скасувати"; -"action_cancel_for_now" = "Cancel for now"; +"action_cancel_for_now" = "Скасувати наразі"; "action_choose_photo" = "Вибрати фото"; "action_clear" = "Очистити"; "action_close" = "Закрити"; "action_complete_verification" = "Верифікація завершена"; "action_confirm" = "Підтвердити"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "Підтвердіть пароль"; "action_continue" = "Продовжити"; "action_copy" = "Скопіювати"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Скопіювати посилання"; "action_copy_link_to_message" = "Скопіювати посилання на повідомлення"; +"action_copy_text" = "Copy text"; "action_create" = "Створити"; "action_create_a_room" = "Створити кімнату"; -"action_deactivate" = "Deactivate"; -"action_deactivate_account" = "Deactivate account"; +"action_deactivate" = "Деактивувати"; +"action_deactivate_account" = "Деактивувати обліковий запис"; "action_decline" = "Відхилити"; "action_delete_poll" = "Видалити опитування"; "action_disable" = "Вимкнути"; "action_discard" = "Відкинути"; "action_done" = "Готово"; "action_edit" = "Редагувати"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Редагувати опитування"; "action_enable" = "Увімкнути"; "action_end_poll" = "Завершити опитування"; @@ -54,14 +58,14 @@ "action_forgot_password" = "Забули пароль?"; "action_forward" = "Переслати"; "action_go_back" = "Повернутися"; -"action_ignore" = "Ignore"; +"action_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_learn_more" = "Докладніше"; "action_leave" = "Вийти"; "action_leave_conversation" = "Залишити розмову"; "action_leave_room" = "Вийти з кімнати"; @@ -81,6 +85,8 @@ "action_react" = "Реакція"; "action_reject" = "Відхилити"; "action_remove" = "Вилучити"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Відповісти"; "action_reply_in_thread" = "Відповісти в гілці"; "action_report_bug" = "Повідомити про помилку"; @@ -94,63 +100,64 @@ "action_send" = "Надіслати"; "action_send_message" = "Надіслати повідомлення"; "action_share" = "Поділитися"; -"action_share_link" = "Поширити посилання"; -"action_show" = "Show"; +"action_share_link" = "Поділитися посиланням"; +"action_show" = "Показати"; "action_sign_in_again" = "Увійдіть знову"; "action_signout" = "Вийти"; "action_signout_anyway" = "Все одно вийти"; "action_skip" = "Пропустити"; "action_start" = "Розпочати"; -"action_start_chat" = "Почати чат"; +"action_start_chat" = "Розпочати бесіду"; "action_start_verification" = "Почати верифікацію"; "action_static_map_load" = "Натисніть, щоб завантажити мапу"; "action_take_photo" = "Зробити фото"; -"action_tap_for_options" = "Натисніть, щоб переглянути параметри"; +"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" = "Так"; -"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" = "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"; +"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_adding_caption" = "Adding caption"; "common_advanced_settings" = "Додаткові налаштування"; "common_analytics" = "Аналітика"; "common_appearance" = "Тема"; "common_audio" = "Аудіо"; "common_blocked_users" = "Заблоковані користувачі"; "common_bubbles" = "Бульбашки"; -"common_call_invite" = "Триває виклик (не підтримується)"; -"common_call_started" = "Дзвінок розпочато"; -"common_chat_backup" = "Резервне копіювання чату"; +"common_call_started" = "Виклик розпочато"; +"common_chat_backup" = "Резервне копіювання бесіди"; "common_copyright" = "Авторське право"; "common_creating_room" = "Створення кімнати..."; -"common_current_user_left_room" = "Вийшов (-ла) з кімнати"; +"common_current_user_left_room" = "Виходить з кімнати"; "common_dark" = "Темна"; -"common_decryption_error" = "Помилка розшифровки"; +"common_decryption_error" = "Помилка розшифрування"; "common_developer_options" = "Налаштування розробника"; -"common_device_id" = "Device ID"; -"common_direct_chat" = "Особистий чат"; +"common_device_id" = "Ідентифікатор пристрою"; +"common_direct_chat" = "Особиста бесіда"; "common_edited_suffix" = "(відредаговано)"; "common_editing" = "Редагування"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; -"common_encryption" = "Encryption"; +"common_encryption" = "Шифрування"; "common_encryption_enabled" = "Шифрування ввімкнено"; "common_enter_your_pin" = "Введіть свій PIN-код"; "common_error" = "Помилка"; "common_everyone" = "Усі"; "common_face_id_ios" = "Face ID"; -"common_failed" = "Невдало"; -"common_favourite" = "Улюблений"; -"common_favourited" = "Вибране"; +"common_failed" = "Помилка"; +"common_favourite" = "Обране"; +"common_favourited" = "Обране"; "common_file" = "Файл"; "common_forward_message" = "Переслати повідомлення"; -"common_frequently_used" = "Frequently used"; +"common_frequently_used" = "Частовживані"; "common_gif" = "GIF"; "common_image" = "Зображення"; "common_in_reply_to" = "У відповідь на %1$@"; @@ -195,12 +202,12 @@ "common_saved_changes" = "Збережені зміни"; "common_saving" = "Збереження"; "common_screen_lock" = "Блокування екрану"; -"common_search_for_someone" = "Пошук когось"; +"common_search_for_someone" = "Шукати когось"; "common_search_results" = "Результати пошуку"; "common_security" = "Безпека"; -"common_seen_by" = "Побачили"; +"common_seen_by" = "Переглянули"; "common_sending" = "Надсилання…"; -"common_sending_failed" = "Не вдалося відправити"; +"common_sending_failed" = "Не вдалося надіслати"; "common_sent" = "Надіслано"; "common_server_not_supported" = "Сервер не підтримується"; "common_server_url" = "URL-адреса сервера"; @@ -208,7 +215,7 @@ "common_shared_location" = "Поширене розташування"; "common_signing_out" = "Вихід"; "common_something_went_wrong" = "Щось пішло не так"; -"common_starting_chat" = "Початок чату..."; +"common_starting_chat" = "Початок бесіди..."; "common_sticker" = "Наліпка"; "common_success" = "Успіх"; "common_suggestions" = "Пропозиції"; @@ -226,42 +233,43 @@ "common_unable_to_invite_title" = "Не вдалося надіслати запрошення"; "common_unlock" = "Розблокувати"; "common_unmute" = "Увімкнути звук"; +"common_unsupported_call" = "Unsupported call"; "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_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" = "Copied to clipboard"; +"common.copied_to_clipboard" = "Скопійовано до буферу обміну"; "common.do_not_show_this_again" = "Не показувати це знову"; "common.open_source_licenses" = "Ліцензії відкритого коду"; -"common.pinned" = "Pinned"; +"common.pinned" = "Закріплено"; "common.send_to" = "Надіслати до"; -"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$@"; +"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_camera" = "Щоб дозволити застосунку використовувати камеру, надайте дозвіл у системних налаштуваннях."; +"dialog_permission_generic" = "Надайте дозвіл в системних налаштуваннях."; "dialog_permission_location_description_ios" = "Надайте доступ в Налаштуваннях -> Місцезнаходження."; -"dialog_permission_location_title_ios" = "%1$@ не має доступу до вашого місцезнаходження."; -"dialog_permission_microphone" = "Для того, щоб дозволити програмі використовувати мікрофон, надайте дозвіл у налаштуваннях системи."; +"dialog_permission_location_title_ios" = "%1$@ не має доступу до вашого розташування."; +"dialog_permission_microphone" = "Щоб дозволити застосунку використовувати мікрофон, надайте дозвіл у налаштуваннях системи."; "dialog_permission_microphone_description_ios" = "Надайте доступ, щоб ви могли записувати та надсилати повідомлення з аудіо."; "dialog_permission_microphone_title_ios" = "%1$@ потрібен дозвіл на доступ до мікрофона."; -"dialog_permission_notification" = "Для того, щоб програма відображала сповіщення, надайте дозвіл у налаштуваннях системи."; +"dialog_permission_notification" = "Щоб застосунок показував сповіщення, надайте дозвіл у налаштуваннях системи."; "dialog_title_confirmation" = "Підтвердження"; "dialog_title_warning" = "Попередження"; "dialog_unsaved_changes_description_ios" = "Внесені зміни не буде збережено"; @@ -274,47 +282,47 @@ "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 і створення облікових записів."; "error_failed_creating_the_permalink" = "Не вдалося створити постійне посилання"; -"error_failed_loading_map" = "%1$@ Не вдалося завантажити карту. Будь ласка, спробуйте ще раз пізніше."; +"error_failed_loading_map" = "%1$@ не може завантажити мапу. Повторіть спробу пізніше."; "error_failed_loading_messages" = "Не вдалося завантажити повідомлення"; -"error_failed_locating_user" = "%1$@ не вдалося отримати доступ до вашого місцезнаходження. Будь ласка, спробуйте ще раз пізніше."; +"error_failed_locating_user" = "%1$@ не вдалося отримати доступ до вашого розташування. Повторіть спробу пізніше."; "error_failed_uploading_voice_message" = "Не вдалося завантажити голосове повідомлення."; "error_message_not_found" = "Повідомлення не знайдено"; "error_no_compatible_app_found" = "Не знайдено сумісного застосунку для виконання цієї дії."; "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_sent_in_clear" = "Not encrypted."; -"event_shield_reason_unknown_device" = "Зашифрований невідомим або видаленим пристроєм."; -"event_shield_reason_unsigned_device" = "Зашифровано пристроєм, який не підтверджено його власником."; -"event_shield_reason_unverified_identity" = "Зашифровано неперевіреним користувачем."; -"full_screen_intent_banner_message" = "Щоб ніколи не пропустити важливий дзвінок, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."; -"full_screen_intent_banner_title" = "Enhance your call experience"; +"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" = "Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."; +"full_screen_intent_banner_title" = "Покращуйте досвід дзвінків"; "invite_friends_rich_title" = "🔐️ Приєднуйтеся до мене в %1$@"; -"invite_friends_text" = "Привіт, пишіть мені за адресою %1$@: %2$@"; -"leave_conversation_alert_subtitle" = "Ви впевнені, що хочете залишити цю розмову? Ця розмова не є загальнодоступною, і ви не зможете знову приєднатися без запрошення."; -"leave_room_alert_empty_subtitle" = "Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо Ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі і Ви."; -"leave_room_alert_private_subtitle" = "Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не є публічною, і ви не зможете повернутися до неї без запрошення."; +"invite_friends_text" = "Вітаю, поспілкуйтеся зі мною в %1$@: %2$@"; +"leave_conversation_alert_subtitle" = "Ви впевнені, що хочете залишити цю розмову? Ця розмова не загальнодоступна, і ви не зможете знову приєднатися без запрошення."; +"leave_room_alert_empty_subtitle" = "Ви впевнені, що хочете вийти з цієї кімнати? Ви тут єдина людина. Якщо ви вийдете, ніхто в майбутньому не зможе приєднатися, у тому числі й ви."; +"leave_room_alert_private_subtitle" = "Ви впевнені, що хочете вийти з цієї кімнати? Ця кімната не загальнодоступна, і ви не зможете повернутися до неї без запрошення."; "leave_room_alert_subtitle" = "Ви впевнені, що хочете вийти з кімнати?"; "login_initial_device_name_ios" = "%1$@ iOS"; "notification_channel_call" = "Виклик"; "notification_channel_listening_for_events" = "Прослуховування подій"; "notification_channel_noisy" = "Гучні сповіщення"; -"notification_channel_ringing_calls" = "Дзвінки"; +"notification_channel_ringing_calls" = "Виклики"; "notification_channel_silent" = "Тихі сповіщення"; -"notification_incoming_call" = "Вхідний дзвінок"; -"notification_inline_reply_failed" = "** Не вдалося надіслати - будь ласка, відкрийте кімнату"; -"notification_invite_body" = "Запросив (-ла) Вас до чату"; -"notification_invite_body_with_sender" = "%1$@ invited you to chat"; -"notification_mentioned_you_body" = "Згадав(-ла) вас: %1$@"; +"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$@ invited you to join the room"; +"notification_reaction_body" = "Реагує з %1$@"; +"notification_room_invite_body" = "Запрошує вас приєднатися до кімнати"; +"notification_room_invite_body_with_sender" = "%1$@ запросив вас приєднатися до кімнати"; "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$@"; @@ -344,46 +352,70 @@ "rich_text_editor_unindent" = "Без відступу"; "rich_text_editor_url_placeholder" = "Посилання"; "rich_text_editor_a11y_add_attachment" = "Додати вкладення"; -"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; +"rich_text_editor_composer_caption_placeholder" = "Необов'язковий підпис..."; "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_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_room_mentions_at_room_subtitle" = "Сповістіть усю кімнату"; +"screen_advanced_settings_element_call_base_url_validation_error" = "Неправильна URL-адреса. Переконайтеся, що ви вказали протокол (http/https) та правильну адресу."; +"screen_create_room_room_access_section_anyone_option_description" = "Будь-хто може приєднатися до цієї кімнати"; +"screen_create_room_room_access_section_anyone_option_title" = "Кожний"; +"screen_create_room_room_access_section_header" = "Доступ до кімнати"; +"screen_create_room_room_access_section_knocking_option_description" = "Будь-хто може попросити приєднатися до кімнати, але адміністратор або модератор повинен буде прийняти запит"; +"screen_create_room_room_access_section_knocking_option_title" = "Запросити приєднатися"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Деякі символи не допускаються. Підтримуються тільки букви, цифри і наступні символи! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "Ця адреса кімнати вже існує, будь ласка, спробуйте відредагувати поле адреси кімнати або змінити назву кімнати"; +"screen_create_room_room_address_section_footer" = "Щоб цю кімнату було видно в каталозі загальнодоступних кімнат, вам знадобиться її адреса."; +"screen_create_room_room_address_section_title" = "Адреса кімнати"; +"screen_create_room_room_visibility_section_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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."; +"screen_media_upload_preview_error_failed_sending" = "Не вдалося завантажити медіафайл, спробуйте ще раз."; +"screen_pinned_timeline_empty_state_description" = "Натисніть на повідомлення і виберіть \"%1$@\", щоб додати його сюди."; +"screen_pinned_timeline_empty_state_headline" = "Закріпіть важливі повідомлення, щоб їх можна було легко знайти"; +"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_title" = "Ваше повідомлення не було надіслано, оскільки підтверджена особистість %1$@ змінилася"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Надіслати повідомлення в будь-якому випадку"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ використовує один або кілька неперевірених пристроїв. Ви можете відправити повідомлення в будь-якому випадку, або ж скасувати відправку і спробувати пізніше, коли %2$@ перевірить всі пристрої."; +"screen_resolve_send_failure_unsigned_device_title" = "Ваше повідомлення не було надіслано, тому що %1$@ не перевірив усі пристрої"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "Один або кілька ваших пристроїв не підтверджено. Ви можете відправити повідомлення в будь-якому випадку, або ж скасувати відправку і спробувати пізніше, коли перевірите всі свої пристрої."; +"screen_resolve_send_failure_you_unsigned_device_title" = "Ваше повідомлення не було надіслано, оскільки ви не підтвердили один або декілька своїх пристроїв"; +"screen_room_mentions_at_room_subtitle" = "Сповістити всю кімнату"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "screen_room_pinned_banner_indicator" = "%1$@ із %2$@"; -"screen_room_pinned_banner_indicator_description" = "%1$@ Закріплених повідомлень"; -"screen_room_pinned_banner_loading_description" = "Loading message…"; +"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" = "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_room_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; +"screen_room_details_pinned_events_row_title" = "Закріплені повідомлення"; +"screen_room_details_requests_to_join_title" = "Requests to join"; +"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_form_hint" = "Адреса домашнього сервера"; "screen_account_provider_form_notice" = "Уведіть пошуковий термін або адресу домену."; "screen_account_provider_form_subtitle" = "Пошук компанії, спільноти або приватного сервера."; @@ -392,25 +424,25 @@ "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_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" = "Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів."; "screen_advanced_settings_share_presence" = "Поділіться присутністю"; -"screen_advanced_settings_share_presence_description" = "Якщо вимкнено, ви не зможете надсилати та отримувати сповіщення про прочитання або введення тексту"; +"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_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_prompt_third_party_sharing" = "Ми не передаватимемо ваші дані третім особам"; +"screen_analytics_prompt_title" = "Допоможіть вдосконалити %1$@"; "screen_analytics_settings_share_data" = "Поділитися аналітичними даними"; -"screen_app_lock_biometric_authentication" = "біометрична аутентифікація"; +"screen_app_lock_biometric_authentication" = "біометрична автентифікація"; "screen_app_lock_biometric_unlock" = "біометричне розблокування"; -"screen_app_lock_biometric_unlock_reason_ios" = "Для доступу до застосунку потрібна аутентифікація"; +"screen_app_lock_biometric_unlock_reason_ios" = "Для доступу до застосунку потрібно автентифікуватися"; "screen_app_lock_forgot_pin" = "Забули PIN-код?"; "screen_app_lock_settings_change_pin" = "Змінити PIN-код"; "screen_app_lock_settings_enable_biometric_unlock" = "Дозволити біометричне розблокування"; @@ -421,55 +453,55 @@ "screen_app_lock_settings_remove_pin_alert_message" = "Ви впевнені, що хочете видалити PIN-код?"; "screen_app_lock_settings_remove_pin_alert_title" = "Видалити PIN-код?"; "screen_app_lock_setup_biometric_unlock_allow_title" = "Дозволити %1$@"; -"screen_app_lock_setup_biometric_unlock_skip" = "Я б краще використав PIN-код"; +"screen_app_lock_setup_biometric_unlock_skip" = "Мені краще використати PIN-код"; "screen_app_lock_setup_biometric_unlock_subtitle" = "Заощаджуйте час і використовуйте %1$@ для розблокування застосунку щоразу"; "screen_app_lock_setup_choose_pin" = "Виберіть PIN-код"; "screen_app_lock_setup_confirm_pin" = "Підтвердити PIN-код"; "screen_app_lock_setup_pin_context" = "Заблокуйте %1$@, щоб додати додаткову безпеку вашим чатам.\n\nВиберіть щось, що запам'ятовується. Але якщо ви забудете PIN-код, ви вийдете з застосунку."; -"screen_app_lock_setup_pin_forbidden_dialog_content" = "Ви не можете вибрати його як свій PIN-код з міркувань безпеки"; +"screen_app_lock_setup_pin_forbidden_dialog_content" = "Ви не можете вибрати його своїм PIN-кодом з міркувань безпеки"; "screen_app_lock_setup_pin_forbidden_dialog_title" = "Виберіть інший PIN-код"; "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_message" = "Щоб продовжити, вам потрібно повторно ввійти та створити новий PIN-код"; "screen_app_lock_signout_alert_title" = "Ви виходите з системи"; "screen_blocked_users_empty" = "У вас немає заблокованих користувачів."; "screen_blocked_users_unblocking" = "Розблокування…"; -"screen_bug_report_attach_screenshot" = "Прикріпити знімок екрану"; +"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" = "Опишіть, будь ласка, проблему. Що Ви зробили? Чого Ви очікували? Що сталося? Будь ласка, опишіть якомога детальніше."; +"screen_bug_report_edit_screenshot" = "Редагувати знімок екрана"; +"screen_bug_report_editor_description" = "Будь ласка, опишіть проблему. Які дії ви виконали? Який очікуваний результат? Що сталося? Будь ласка, опишіть якомога детальніше."; "screen_bug_report_editor_placeholder" = "Опишіть проблему..."; "screen_bug_report_editor_supporting" = "Якщо можливо, будь ласка, напишіть опис англійською мовою."; -"screen_bug_report_error_description_too_short" = "Опис занадто короткий, будь ласка, надайте докладнішу інформацію про те, що сталося. Дякую!"; +"screen_bug_report_error_description_too_short" = "Опис закороткий, будь ласка, надайте докладнішу інформацію про те, що сталося. Дякуємо!"; "screen_bug_report_include_crash_logs" = "Надіслати журнали збоїв"; "screen_bug_report_include_logs" = "Дозволити журнали"; "screen_bug_report_include_screenshot" = "Надіслати знімок екрана"; "screen_bug_report_logs_description" = "Журнали будуть додані до вашого повідомлення, щоб переконатися, що все працює належним чином. Щоб надіслати повідомлення без журналів, вимкніть це налаштування."; "screen_bug_report_view_logs" = "Переглянути журнали"; -"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org — це великий безплатний сервер у загальнодоступній мережі Matrix для безпечного децентралізованого зв’язку, яким керує Matrix.org Foundation."; +"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org — це великий безплатний сервер у загальнодоступній мережі Matrix для безпечного децентралізованого спілкування, яким керує Matrix.org Foundation."; "screen_change_account_provider_other" = "Інше"; "screen_change_account_provider_subtitle" = "Використати іншого провайдера облікових записів, наприклад, власний приватний сервер або робочий обліковий запис."; "screen_change_account_provider_title" = "Змінити провайдера облікового запису"; -"screen_change_server_error_invalid_homeserver" = "Не вдалося підключитися до цього домашнього сервера. Будь ласка, перевірте, чи правильно Ви ввели URL-адресу домашнього сервера. Якщо URL-адреса правильна, зверніться за додатковою допомогою до адміністратора домашнього сервера."; -"screen_change_server_error_invalid_well_known" = "Sliding sync недоступний через проблему у well-known файлі:\n%1$@"; +"screen_change_server_error_invalid_homeserver" = "Не вдалося під'єднатися до цього домашнього сервера. Перевірте правильність введеної URL-адреси домашнього сервера. Якщо URL-адреса правильна, зверніться по додаткову допомогу до адміністратора домашнього сервера."; +"screen_change_server_error_invalid_well_known" = "Sliding sync недоступний через проблему у файлі well-known:\n%1$@"; "screen_change_server_error_no_sliding_sync_message" = "Наразі цей сервер не підтримує sliding sync."; "screen_change_server_form_header" = "URL-адреса домашнього сервера"; -"screen_change_server_form_notice" = "Ви можете підключитися лише до наявного сервера, який підтримує sliding sync. Ваш адміністратор домашнього сервера повинен буде налаштувати його. %1$@"; -"screen_change_server_subtitle" = "Яка адреса Вашого сервера?"; +"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_enable" = "Увімкнути резервне копіювання"; -"screen_chat_backup_key_backup_description" = "Резервне копіювання гарантує, що ви не втратите історію повідомлень. %1$@."; +"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_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" = "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" = "Отримайте доступ до своїх зашифрованих повідомлень, якщо ви втратите всі свої пристрої або вийшли з %1$@ системи."; -"screen_create_account_title" = "Create account"; +"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_2" = "Увійдіть до вашого облікового запису знову"; "screen_create_new_recovery_key_list_item_3" = "Коли вас попросять підтвердити пристрій, виберіть %1$@"; @@ -487,53 +519,53 @@ "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_description" = "Лише запрошені люди мають доступ до цієї кімнати. Усі повідомлення захищені наскрізним шифруванням."; "screen_create_room_private_option_title" = "Приватна кімната (тільки за запрошенням)"; -"screen_create_room_public_option_description" = "Повідомлення не шифруються, і будь-хто може їх прочитати. Шифрування можна ввімкнути пізніше."; -"screen_create_room_public_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"; -"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" = "Будь ласка, підтвердіть, що ви хочете деактивувати свій обліковий запис. Ця дія не може бути скасована."; +"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$@ваш обліковий запис (ви не можете знову увійти, а ваш ідентифікатор не може бути використаний повторно)."; +"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" = "Ваші повідомлення залишатимуться видимими для зареєстрованих користувачів, але недоступними для нових або незареєстрованих користувачів, якщо ви вирішите їх видалити."; +"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_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" = "Continue reset"; -"screen_encryption_reset_bullet_1" = "Дані вашого облікового запису, контакти, налаштування й чати будуть збережені"; +"screen_encryption_reset_action_continue_reset" = "Продовжити скидання налаштувань"; +"screen_encryption_reset_bullet_1" = "Дані вашого облікового запису, контакти, налаштування й бесіди будуть збережені"; "screen_encryption_reset_bullet_2" = "Ви втратите свою наявну історію повідомлень"; -"screen_encryption_reset_bullet_3" = "Вам доведеться підтвердити всі наявні пристрої та контакти знову"; +"screen_encryption_reset_bullet_3" = "Вам доведеться верифікувати всі наявні пристрої та контакти повторно"; "screen_encryption_reset_footer" = "Скидайте ідентичність тільки якщо ви не маєте доступу до інших пристроїв в обліковому записі та втратили свій ключ відновлення."; -"screen_encryption_reset_title" = "Скиньте свою ідентичність, якщо не можете підтвердити іншим способом"; +"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" = "Use another device"; -"screen_identity_confirmation_use_recovery_key" = "Use 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_decline_direct_chat_message" = "Ви дійсно хочете відмовитися від приватної бесіди з %1$@?"; +"screen_invites_decline_direct_chat_title" = "Відхилити бесіду"; "screen_invites_empty_list" = "Немає запрошень"; -"screen_invites_invited_you" = "%1$@ (%2$@) запросив (-ла) Вас"; +"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_description" = "%1$@ ще не підтримує простори. Ви можете отримати доступ до них у вебверсії."; "screen_join_room_space_not_supported_title" = "Простори поки що не підтримуються"; "screen_join_room_subtitle_knock" = "Натисніть кнопку нижче, і адміністратор кімнати отримає сповіщення. Ви зможете приєднатися до розмови після схвалення."; "screen_join_room_subtitle_no_preview" = "Ви мусите бути учасником цієї кімнати, щоб переглядати історію повідомлень."; @@ -549,14 +581,12 @@ "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_refresh_tokens" = "Цей сервер налаштований на використання оновлюваних токенів. Вони не підтримуються, якщо використовується вхід за допомогою основі пароля."; "screen_login_error_unsupported_authentication" = "Обраний домашній сервер не підтримує вхід за допомогою пароля або OIDC. Зверніться до адміністратора або виберіть інший домашній сервер."; "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" = "Ви можете змінити свої налаштування пізніше."; @@ -565,23 +595,23 @@ "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_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_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_mode_all" = "Усі"; "screen_notification_settings_mode_mentions" = "Згадки"; -"screen_notification_settings_notification_section_title" = "Повідомляти мене про"; +"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" = "Щоб отримувати сповіщення змініть свої %1$@."; "screen_notification_settings_system_notifications_action_required_content_link" = "системні налаштування"; "screen_notification_settings_system_notifications_turned_off" = "Системні сповіщення вимкнені"; "screen_notification_settings_title" = "Сповіщення"; @@ -591,22 +621,22 @@ "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_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_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_2" = "Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість Wi-Fi"; "screen_qr_code_login_connection_note_secure_state_list_item_3" = "Якщо це не спрацює, увійдіть вручну"; -"screen_qr_code_login_connection_note_secure_state_title" = "З'єднання не є безпечним"; +"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_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" = "Вхід був відхилений на іншому пристрої."; @@ -620,57 +650,57 @@ "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" = "Відскануйте QR-код цим пристроєм"; -"screen_qr_code_login_initial_state_subtitle" = "Only available if your account provider supports it."; +"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-код"; "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" = "Надайте доступ до камери, щоб сканувати QR-код"; -"screen_qr_code_login_scanning_state_title" = "Відскануйте QR-код"; +"screen_qr_code_login_scanning_state_title" = "Зіскануйте QR-код"; "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_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_content" = "Будь ласка, спробуйте ще раз, щоб підтвердити доступ до сховища ключів."; "screen_recovery_key_confirm_error_title" = "Неправильний ключ відновлення"; "screen_recovery_key_confirm_key_description" = "Якщо у вас є ключ безпеки або фраза безпеки, це теж спрацює."; -"screen_recovery_key_confirm_key_placeholder" = "Ввести..."; +"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_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" = "Згенеруйте ключ відновлення"; +"screen_recovery_key_setup_generate_key_description" = "Не діліться цим ні з ким!"; "screen_recovery_key_setup_success" = "Налаштування відновлення виконано успішно"; "screen_recovery_key_setup_title" = "Налаштувати відновлення"; -"screen_report_content_block_user_hint" = "Перевірте, чи хочете Ви приховати всі поточні та майбутні повідомлення від цього користувача"; -"screen_report_content_explanation" = "Це повідомлення буде надіслано адміністратору вашого домашнього сервера. Він (вона) не зможе прочитати жодні зашифровані повідомлення."; +"screen_report_content_block_user_hint" = "Перевірте, чи хочете ви приховати всі поточні та майбутні повідомлення від цього користувача"; +"screen_report_content_explanation" = "Це повідомлення буде надіслано адміністраторам вашого домашнього сервера. Вони не зможуть прочитати зашифровані повідомлення."; "screen_report_content_hint" = "Причина скарги на цей вміст"; "screen_reset_encryption_confirmation_alert_action" = "Так, скинути зараз"; "screen_reset_encryption_confirmation_alert_subtitle" = "Цей процес незворотний."; "screen_reset_encryption_confirmation_alert_title" = "Ви впевнені, що хочете скинути шифрування?"; "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" = "Записати відео"; @@ -693,10 +723,10 @@ "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_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_admin_section_footer" = "Адміністратори автоматично мають повноваження модератора"; "screen_room_change_role_moderators_title" = "Керувати модераторами"; "screen_room_change_role_unsaved_changes_description" = "У вас є не збережені зміни."; "screen_room_details_add_topic_title" = "Додати тему"; @@ -704,17 +734,17 @@ "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_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_subtitle" = "Повідомлення захищені замками. Тільки ви та одержувачі маєте унікальні ключі для їх розблокування."; "screen_room_details_encryption_enabled_title" = "Шифрування повідомлень увімкнено"; "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_mode_default" = "Типово"; "screen_room_details_share_room_title" = "Поділитися кімнатою"; "screen_room_details_title" = "Інформація про кімнату"; "screen_room_details_updating_room" = "Оновлення кімнати..."; @@ -723,17 +753,17 @@ "screen_room_encrypted_history_banner" = "Історія повідомлень наразі недоступна."; "screen_room_encrypted_history_banner_unverified" = "Історія повідомлень недоступна в цій кімнаті. Перевірте цей пристрій, щоб побачити історію повідомлень."; "screen_room_error_failed_retrieving_user_details" = "Не вдалося отримати дані користувача"; -"screen_room_invite_again_alert_message" = "Чи хотіли б Ви запросити їх знову?"; -"screen_room_invite_again_alert_title" = "Ви одні в цьому чаті"; +"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" = "Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."; +"screen_room_member_details_block_alert_description" = "Заблоковані користувачі не зможуть надсилати вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."; "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_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" = "Ви точно хочете заблокувати цього користувача?"; @@ -741,8 +771,8 @@ "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" = "Видалити учасника та заборонити приєднання в майбутньому?"; +"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" = "Розблокувати користувача"; @@ -750,26 +780,26 @@ "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_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" = "Увімкнення цього параметра змінить налаштування за замовчуванням"; -"screen_room_notification_settings_custom_settings_title" = "Повідомте мене в цьому чаті для"; +"screen_room_notification_settings_allow_custom_footnote" = "Увімкнення цього параметра змінить типові налаштування"; +"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_default_setting_title" = "Типові налаштування"; "screen_room_notification_settings_edit_remove_setting" = "Вилучити користувальницькі налаштування"; "screen_room_notification_settings_error_loading_settings" = "Під час завантаження налаштувань сповіщень сталася помилка."; -"screen_room_notification_settings_error_restoring_default" = "Не вдалося відновити режим за замовчуванням, спробуйте ще раз."; +"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_room_custom_settings_title" = "У цій кімнаті сповіщати мене про"; "screen_room_retry_send_menu_send_again_action" = "Надіслати знову"; -"screen_room_retry_send_menu_title" = "Ваше повідомлення не вдалося надіслати"; +"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" = "Понизити до учасника"; @@ -787,40 +817,41 @@ "screen_room_timeline_add_reaction" = "Додати смайлики"; "screen_room_timeline_beginning_of_room" = "Це початок %1$@"; "screen_room_timeline_beginning_of_room_no_name" = "Це початок цієї розмови."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "Показувати менше"; "screen_room_timeline_message_copied" = "Повідомлення скопійовано"; -"screen_room_timeline_no_permission_to_post" = "У Вас немає дозволу на публікацію в цій кімнаті"; +"screen_room_timeline_no_permission_to_post" = "У вас немає дозволу на публікацію в цій кімнаті"; "screen_room_timeline_reactions_show_more" = "Показати більше"; "screen_room_timeline_read_marker_title" = "Нове"; -"screen_room_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_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" = "Ви можете додати чат до улюблених у налаштуваннях чату.\nНаразі ви можете зняти фільтри, щоб побачити інші ваші чати"; -"screen_roomlist_filter_favourites_empty_state_title" = "Ви ще не маєте улюблених чатів"; +"screen_roomlist_empty_title" = "Ще немає бесід."; +"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_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_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_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" = "Приватний сервер для співробітників 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" = "Щось не так. Або час очікування запиту минув, або в запиті було відмовлено."; @@ -830,151 +861,155 @@ "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" = "Повторити перевірку"; -"screen_session_verification_positive_button_initial" = "Я готовий"; +"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_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_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_use_another_device_subtitle" = "Перш ніж починати перевірку звідси, переконайтеся, що програму відкрито на іншому пристрої."; +"screen_session_verification_use_another_device_title" = "Відкрийте додаток на іншому перевіреному пристрої"; +"screen_session_verification_waiting_another_device_subtitle" = "Ви повинні побачити спливаюче вікно на іншому пристрої. Почніть перевірку звідти."; +"screen_session_verification_waiting_another_device_title" = "Почніть перевірку на іншому пристрої"; +"screen_session_verification_waiting_to_accept_subtitle" = "Щоб продовжити, прийміть запит на початок процесу верифікації в іншому сеансі."; "screen_session_verification_waiting_to_accept_title" = "Очікування на прийняття запиту"; "screen_share_location_title" = "Поділитися розташуванням"; "screen_share_my_location_action" = "Поділитися моїм розташуванням"; "screen_share_open_apple_maps" = "Відкрити в Apple Maps"; -"screen_share_open_google_maps" = "Відкрити в Google Maps"; +"screen_share_open_google_maps" = "Відкрити в Картах Google"; "screen_share_open_osm_maps" = "Відкрити в OpenStreetMap"; -"screen_share_this_location_action" = "Поділитися цим місцезнаходженням"; +"screen_share_this_location_action" = "Поділитися цим місцем перебування"; "screen_signed_out_reason_1" = "Ви змінили пароль під час іншого сеансу"; "screen_signed_out_reason_2" = "Ви видалили сеанс з іншого сеансу"; -"screen_signed_out_reason_3" = "Адміністратор вашого сервера визнав недійсним ваш доступ"; +"screen_signed_out_reason_3" = "Адміністратор вашого сервера визнав ваш доступ недійсним"; "screen_signed_out_subtitle" = "Можливо, ви вийшли з системи з однієї з причин, наведених нижче. Будь ласка, увійдіть знову, щоб продовжити використання %@."; "screen_signed_out_title" = "Ви вийшли з системи"; "screen_signout_confirmation_dialog_content" = "Ви впевнені, що бажаєте вийти?"; "screen_signout_in_progress_dialog_content" = "Вихід…"; "screen_signout_key_backup_disabled_subtitle" = "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень."; "screen_signout_key_backup_disabled_title" = "Ви вимкнули резервне копіювання"; -"screen_signout_key_backup_offline_subtitle" = "Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно підключіться, щоб зберегти резервну копію ключів перед виходом з системи."; -"screen_signout_key_backup_ongoing_subtitle" = "Зачекайте, поки це завершиться, перш ніж вийти."; +"screen_signout_key_backup_offline_subtitle" = "Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно під'єднайтеся, щоб зберегти резервну копію ключів перед виходом."; +"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_start_chat_error_starting_chat" = "Під час спроби почати чат сталася помилка"; -"screen_view_location_title" = "Місцезнаходження"; -"screen_welcome_bullet_1" = "Дзвінки, опитування, пошук тощо будуть додані пізніше цього року."; +"screen_start_chat_error_starting_chat" = "Під час спроби почати бесіду сталася помилка"; +"screen_view_location_title" = "Розташування"; +"screen_welcome_bullet_1" = "Виклики, опитування, пошук тощо будуть додані пізніше цього року."; "screen_welcome_bullet_2" = "Історія повідомлень для зашифрованих кімнат ще недоступна."; "screen_welcome_bullet_3" = "Ми хотіли б почути вас, розкажіть нам ваші враження та ідеї щодо застосунку на сторінці налаштувань."; -"screen_welcome_button" = "Пішли!"; +"screen_welcome_button" = "Уперед!"; "screen_welcome_subtitle" = "Ось що вам потрібно знати:"; "screen_welcome_title" = "Ласкаво просимо до %1$@!"; -"session_verification_banner_message" = "Схоже, Ви використовуєте новий пристрій. Щоб отримати доступ до зашифрованих повідомлень, підтвердьте особу за допомогою іншого пристрою."; -"session_verification_banner_title" = "Підтвердьте, що це Ви"; -"settings_rageshake" = "Rageshake"; +"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_display_name_changed_from" = "%1$@ змінив (-ла) своє імʼя з %2$@ на %3$@"; +"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" = "%1$@ вилучає своє ім'я (було %2$@)"; "state_event_display_name_removed_by_you" = "Ви видалили своє ім'я (було%1$@)"; -"state_event_display_name_set" = "%1$@ змінив (-ла) своє ім'я на %2$@"; +"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" = "%1$@ змінює аватар кімнати"; "state_event_room_avatar_changed_by_you" = "Ви змінили аватар кімнати"; -"state_event_room_avatar_removed" = "%1$@ видалив (-ла) аватар кімнати"; +"state_event_room_avatar_removed" = "%1$@ видаляє аватар кімнати"; "state_event_room_avatar_removed_by_you" = "Ви видалили аватар кімнати"; -"state_event_room_ban" = "%1$@ заблокував (-ла) %2$@"; +"state_event_room_ban" = "%1$@ блокує %2$@"; "state_event_room_ban_by_you" = "Ви заблокували %1$@"; -"state_event_room_created" = "%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" = "%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_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" = "%1$@ відхилив (-ла) запит %2$@ на приєднання"; -"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_denied" = "%1$@ відхиляє запит %2$@ на приєднання"; +"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" = "%1$@ виходить з кімнати"; "state_event_room_leave_by_you" = "Ви вийшли з кімнати"; -"state_event_room_name_changed" = "%1$@ змінив (-ла) назву кімнати на: %2$@"; +"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" = "%1$@ вилучає назву кімнати"; "state_event_room_name_removed_by_you" = "Ви видалили назву кімнати"; -"state_event_room_none" = "%1$@ не внесено жодних змін"; +"state_event_room_none" = "%1$@ нічого не змінює"; "state_event_room_none_by_you" = "Ви не внесли жодних змін"; -"state_event_room_pinned_events_changed" = "%1$@ змінив(-ла) закріплені повідомлення"; +"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" = "%1$@ закріплює повідомлення"; "state_event_room_pinned_events_pinned_by_you" = "Ви закріпили повідомлення"; -"state_event_room_pinned_events_unpinned" = "%1$@ відкріпив(-ла) повідомлення"; +"state_event_room_pinned_events_unpinned" = "%1$@ відкріплює повідомлення"; "state_event_room_pinned_events_unpinned_by_you" = "Ви відкріпили повідомлення"; -"state_event_room_reject" = "%1$@ відхилив (-ла) запрошення"; +"state_event_room_reject" = "%1$@ відхиляє запрошення"; "state_event_room_reject_by_you" = "Ви відхилили запрошення"; -"state_event_room_remove" = "%1$@ вилучив (-ла) %2$@"; +"state_event_room_remove" = "%1$@ вилучає %2$@"; "state_event_room_remove_by_you" = "Ви видалили %1$@"; -"state_event_room_third_party_invite" = "%1$@ запросив (-ла) %2$@ приєднатися до кімнати"; +"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_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_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$@ вніс (внесла) невідомі зміни щодо свого членства"; +"state_event_room_unknown_membership_change" = "%1$@ вносить невідомі зміни щодо свого членства"; "test_language_identifier" = "uk"; "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_failure" = "Деякі тести не пройдено. Перегляньте подробиці."; "troubleshoot_notifications_screen_notice" = "Запустіть тести, щоб виявити будь-яку проблему у вашій конфігурації, через яку сповіщення можуть не працювати належним чином."; "troubleshoot_notifications_screen_quick_fix_action" = "Спробувати виправити"; -"troubleshoot_notifications_screen_success" = "Всі тести пройшли успішно."; +"troubleshoot_notifications_screen_success" = "Всі тести успішно пройдено."; "troubleshoot_notifications_screen_title" = "Усунення неполадок сповіщень"; -"troubleshoot_notifications_screen_waiting" = "Деякі тести вимагають вашої уваги. Будь ласка, перегляньте деталі."; -"troubleshoot_notifications_test_check_permission_description" = "Перевірте, чи програма може показувати сповіщення."; +"troubleshoot_notifications_screen_waiting" = "Деякі тести вимагають вашої уваги. Перегляньте подробиці."; +"troubleshoot_notifications_test_check_permission_description" = "Перевірте, чи може застосунок показувати сповіщення."; "troubleshoot_notifications_test_check_permission_title" = "Перевірте дозволи"; "troubleshoot_notifications_test_current_push_provider_description" = "Отримує назву поточного постачальника."; -"troubleshoot_notifications_test_current_push_provider_failure" = "Постачальників push-сповіщень не обрано."; +"troubleshoot_notifications_test_current_push_provider_failure" = "Постачальників push-сповіщень не вибрано."; "troubleshoot_notifications_test_current_push_provider_success" = "Поточний постачальник: %1$@."; -"troubleshoot_notifications_test_current_push_provider_title" = "Поточний провайдер push"; -"troubleshoot_notifications_test_detect_push_provider_description" = "Переконайтеся, що програма має принаймні один push провайдер."; -"troubleshoot_notifications_test_detect_push_provider_failure" = "Не знайдено постачальників push-повідомлень."; -"troubleshoot_notifications_test_detect_push_provider_title" = "Виявлення push-провайдерів"; -"troubleshoot_notifications_test_display_notification_description" = "Перевірте, чи може програма відображати сповіщення."; +"troubleshoot_notifications_test_current_push_provider_title" = "Поточний постачальник push-сповіщень"; +"troubleshoot_notifications_test_detect_push_provider_description" = "Переконайтеся, що застосунок має принаймні одного постачальника push-сповіщень."; +"troubleshoot_notifications_test_detect_push_provider_failure" = "Не знайдено постачальників push-сповіщень."; +"troubleshoot_notifications_test_detect_push_provider_title" = "Виявлення постачальників push-сповіщень"; +"troubleshoot_notifications_test_display_notification_description" = "Перевірте, чи може застосунок показувати сповіщення."; "troubleshoot_notifications_test_display_notification_failure" = "Ви не натиснули на сповіщення."; -"troubleshoot_notifications_test_display_notification_permission_failure" = "Не вдається відобразити сповіщення."; +"troubleshoot_notifications_test_display_notification_permission_failure" = "Не вдається показати сповіщення."; "troubleshoot_notifications_test_display_notification_success" = "Ви натиснули на сповіщення!"; -"troubleshoot_notifications_test_display_notification_title" = "Відображення сповіщення"; -"troubleshoot_notifications_test_display_notification_waiting" = "Будь ласка, натисніть на сповіщення, щоб продовжити тест."; +"troubleshoot_notifications_test_display_notification_title" = "Показ сповіщення"; +"troubleshoot_notifications_test_display_notification_waiting" = "Натисніть на сповіщення, щоб продовжити тест."; "troubleshoot_notifications_test_firebase_availability_description" = "Переконується, що Firebase доступний."; "troubleshoot_notifications_test_firebase_availability_failure" = "Firebase недоступний."; "troubleshoot_notifications_test_firebase_availability_success" = "Firebase доступний."; @@ -987,8 +1022,8 @@ "troubleshoot_notifications_test_push_loop_back_failure_1" = "Помилка: постачальник push-сповіщень відхилив запит."; "troubleshoot_notifications_test_push_loop_back_failure_2" = "Помилка: %1$@."; "troubleshoot_notifications_test_push_loop_back_failure_3" = "Помилка, неможливо перевірити push."; -"troubleshoot_notifications_test_push_loop_back_failure_4" = "Error, timeout waiting for push."; -"troubleshoot_notifications_test_push_loop_back_success" = "Push loop back took %1$d ms."; +"troubleshoot_notifications_test_push_loop_back_failure_4" = "Помилка, час очікування вийшов на push-повідомлення."; +"troubleshoot_notifications_test_push_loop_back_success" = "Зворотнє відправлення push-повідомлення, зайняло %1$d мс."; "troubleshoot_notifications_test_push_loop_back_title" = "Перевірка зворотного надсилання"; "troubleshoot_notifications_test_unified_push_description" = "Переконується, що дистриб'ютори UnifiedPush доступні."; "troubleshoot_notifications_test_unified_push_failure" = "Дистриб'юторів не знайдено."; @@ -1002,18 +1037,18 @@ "notification_invitation_action_reject" = "Відхилити"; "notification_room_action_mark_as_read" = "Позначити прочитаним"; "notification_room_action_quick_reply" = "Швидка відповідь"; -"screen_pinned_timeline_screen_title_empty" = "Pinned messages"; +"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_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_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" = "Внесені зміни не буде збережено"; @@ -1021,21 +1056,21 @@ "screen_create_room_room_name_label" = "Назва кімнати"; "screen_create_room_title" = "Створити кімнату"; "screen_dm_details_block_alert_action" = "Заблокувати"; -"screen_dm_details_block_alert_description" = "Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."; +"screen_dm_details_block_alert_description" = "Заблоковані користувачі не зможуть надсилати вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."; "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" = "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" = "Enter your recovery key"; +"screen_recovery_key_change_generate_key_description" = "Не діліться цим ні з ким!"; +"screen_recovery_key_confirm_title" = "Введіть ключ відновлення"; "screen_report_content_block_user" = "Заблокувати користувача"; -"screen_reset_encryption_password_placeholder" = "Ввести..."; +"screen_reset_encryption_password_placeholder" = "Входимо..."; "screen_room_attachment_source_camera_photo" = "Зробити фото"; "screen_room_change_permissions_everyone" = "Усі"; "screen_room_change_permissions_member_moderation" = "Модерація учасників"; @@ -1059,7 +1094,7 @@ "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_session_verification_request_failure_subtitle" = "Або час очікування запиту минув, або запит було відхилено, або виникла розбіжність у верифікації."; "screen_signout_confirmation_dialog_submit" = "Вийти"; "screen_signout_confirmation_dialog_title" = "Вийти"; "screen_signout_key_backup_offline_title" = "Резервне копіювання ваших ключів ще триває"; diff --git a/ElementX/Resources/Localizations/uk.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/uk.lproj/Localizable.stringsdict index a9a812c7bb..03fd8b15ad 100644 --- a/ElementX/Resources/Localizations/uk.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/uk.lproj/Localizable.stringsdict @@ -13,11 +13,11 @@ NSStringFormatValueTypeKey d one - %1$d введена цифра + Введена %1$d цифра few - %1$d введено цифри + Введено %1$d цифри many - %1$d введено цифри + Введено %1$d цифр a11y_read_receipts_multiple_with_others @@ -141,7 +141,7 @@ one %d нове повідомлення few - %d нових повідомлень + %d нові повідомлення many %d нових повідомлень @@ -159,7 +159,7 @@ one %d непрочитане сповіщення few - %d непрочитаних сповіщень + %d непрочитані сповіщення many %d непрочитаних сповіщень @@ -229,9 +229,11 @@ NSStringFormatValueTypeKey d one - %1$d Pinned message - other - %1$d Pinned messages + %1$d Закріплене повідомлення + few + %1$d Закріплених повідомлення + many + %1$d Закріплених повідомлення screen_room_member_list_header_title @@ -252,6 +254,22 @@ %1$d осіб + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey @@ -285,7 +303,7 @@ few %1$@%2$@ та %3$d інші many - %1$@%2$@ та %3$d інші + %1$@%2$@ та %3$d інших screen_room_typing_many_members_second_component_ios @@ -337,7 +355,7 @@ one Виявлено %1$d постачальника: %2$@ few - Виявлено %1$d постачальників: %2$@ + Виявлено %1$d постачальники: %2$@ many Виявлено %1$d постачальників: %2$@ @@ -353,11 +371,11 @@ NSStringFormatValueTypeKey d one - %1$d дистриб'ютора знайдено: %2$@. + Знайдений %1$d дистриб'ютор: %2$@. few - %1$d дистриб'юторів знайдено: %2$@. + Знайдено %1$d дистриб'ютори: %2$@. many - %1$d дистриб'юторів знайдено: %2$@. + Знайдено %1$d дистриб'юторів: %2$@. diff --git a/ElementX/Resources/Localizations/uz.lproj/Localizable.strings b/ElementX/Resources/Localizations/uz.lproj/Localizable.strings index a6242a13c4..858a6e471e 100644 --- a/ElementX/Resources/Localizations/uz.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/uz.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Ovoz yozishni amalga oshiring"; "a11y_voice_message_stop_recording" = "Stop recording"; "action_accept" = "Qabul qiling"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Vaqt jadvaliga qo'shing"; "action_back" = "Orqaga"; "action_call" = "Call"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "Davom etish"; "action_copy" = "nusxa"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "Havolani nusxalash"; "action_copy_link_to_message" = "Havolani xabaraga nusxalash"; +"action_copy_text" = "Copy text"; "action_create" = "Yaratmoq"; "action_create_a_room" = "Xonani yaratish"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "Discard"; "action_done" = "Bajarildi"; "action_edit" = "Tahrirlash"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "So‘rovnomani tahrirlash"; "action_enable" = "Yoqish"; "action_end_poll" = "So‘rovnomani tugatish"; @@ -81,6 +85,8 @@ "action_react" = "Reaksiya qilish"; "action_reject" = "Reject"; "action_remove" = "Ochirish"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "Javob bering"; "action_reply_in_thread" = "Mavzuda javob bering"; "action_report_bug" = "Xato haqida xabar berish"; @@ -119,13 +125,13 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "Haqida"; "common_acceptable_use_policy" = "Qabul qilinadigan foydalanish siyosati"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Kengaytirilgan sozlamalar"; "common_analytics" = "Analitika"; "common_appearance" = "Appearance"; "common_audio" = "Audio"; "common_blocked_users" = "Blocked users"; "common_bubbles" = "Pufakchalar"; -"common_call_invite" = "Call in progress (unsupported)"; "common_call_started" = "Call started"; "common_chat_backup" = "Chatning zaxira nusxasi"; "common_copyright" = "Mualliflik huquqi"; @@ -138,6 +144,7 @@ "common_direct_chat" = "Direct chat"; "common_edited_suffix" = "(tahrirlangan)"; "common_editing" = "Tahrirlash"; +"common_editing_caption" = "Editing caption"; "common_emote" = "*%1$@%2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Shifrlash yoqilgan"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "Taklif(lar)ni yuborib bo‘lmadi"; "common_unlock" = "Unlock"; "common_unmute" = "Ovozni yoqish"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "Qo'llab-quvvatlanmagan hodisa"; "common_username" = "Foydalanuvchi nomi"; "common_verification_cancelled" = "Tasdiqlash bekor qilindi"; @@ -346,16 +354,18 @@ "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_description" = "Element qo'ng'irog'iga maxsus asosiy url or'natish"; "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Mediani yuklab bo‘lmadi, qayta urinib ko‘ring."; +"screen_media_upload_preview_error_failed_sending" = "Media yuklanmadi, qayta urinib ko‘ring."; "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."; @@ -375,11 +402,16 @@ "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" = "Notify the whole room"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "Qaytib kelganingizdan xursandmiz!"; "screen_login_title_with_homeserver" = "Kirish%1$@"; "screen_media_picker_error_failed_selection" = "Media tanlash jarayonida xatolik yuz berdi, qayta urinib ko'ring"; -"screen_media_upload_preview_error_failed_processing" = "Mediani yuklab bo‘lmadi, qayta urinib ko‘ring."; -"screen_media_upload_preview_error_failed_sending" = "Media yuklanmadi, qayta urinib ko‘ring."; "screen_migration_message" = "Bu bir martalik jarayon, kutganingiz uchun rahmat."; "screen_migration_title" = "Hisobingiz sozlanmoqda."; "screen_notification_optin_subtitle" = "Sozlamalaringizni keyinroq o'zgartirishingiz mumkin."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "Emoji qo'shmoq"; "screen_room_timeline_beginning_of_room" = "Bu %1$@ni boshlanishi"; "screen_room_timeline_beginning_of_room_no_name" = "Bu suhbatning boshlanishi."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "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"; @@ -847,6 +878,10 @@ "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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "Davom etish uchun boshqa seansda tekshirish jarayonini boshlash soʻrovini qabul qiling."; "screen_session_verification_waiting_to_accept_title" = "Soʻrovni qabul qilish kutilmoqda"; "screen_share_location_title" = "Joylashuvni ulashish"; diff --git a/ElementX/Resources/Localizations/uz.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/uz.lproj/Localizable.stringsdict index d7c1c72156..1f8d3d8868 100644 --- a/ElementX/Resources/Localizations/uz.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/uz.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ %1$dodamlar + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings index 82305720b9..a4ae040a24 100644 --- a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.strings @@ -3,7 +3,7 @@ "a11y_hide_password" = "隐藏密码"; "a11y_jump_to_bottom" = "跳转到底部"; "a11y_notifications_mentions_only" = "仅提及"; -"a11y_notifications_muted" = "关闭通知"; +"a11y_notifications_muted" = "通知已关闭"; "a11y_page_n" = "第 %1$d 页"; "a11y_pause" = "暂停"; "a11y_pin_field" = "PIN 字段"; @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "录制语音消息。"; "a11y_voice_message_stop_recording" = "停止录制"; "action_accept" = "接受"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "添加到时间线"; "action_back" = "返回"; "action_call" = "呼叫"; @@ -32,21 +33,24 @@ "action_close" = "关闭"; "action_complete_verification" = "完成验证"; "action_confirm" = "确认"; -"action_confirm_password" = "Confirm password"; +"action_confirm_password" = "确认密码"; "action_continue" = "继续"; "action_copy" = "复制"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "复制链接"; "action_copy_link_to_message" = "复制消息链接"; +"action_copy_text" = "Copy text"; "action_create" = "创建"; -"action_create_a_room" = "创建房间"; -"action_deactivate" = "Deactivate"; -"action_deactivate_account" = "Deactivate account"; +"action_create_a_room" = "创建聊天室"; +"action_deactivate" = "停用"; +"action_deactivate_account" = "停用账户"; "action_decline" = "拒绝"; "action_delete_poll" = "删除投票"; -"action_disable" = "停用"; +"action_disable" = "禁用"; "action_discard" = "丢弃"; "action_done" = "完成"; "action_edit" = "编辑"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "编辑投票"; "action_enable" = "启用"; "action_end_poll" = "结束投票"; @@ -54,7 +58,7 @@ "action_forgot_password" = "忘记密码?"; "action_forward" = "转发"; "action_go_back" = "返回"; -"action_ignore" = "Ignore"; +"action_ignore" = "忽略"; "action_invite" = "邀请"; "action_invite_friends" = "邀请朋友"; "action_invite_friends_to_app" = "邀请朋友加入 %1$@"; @@ -64,7 +68,7 @@ "action_learn_more" = "了解更多"; "action_leave" = "离开"; "action_leave_conversation" = "离开聊天"; -"action_leave_room" = "离开房间"; +"action_leave_room" = "离开聊天室"; "action_load_more" = "载入更多"; "action_manage_account" = "管理账户"; "action_manage_devices" = "管理设备"; @@ -81,6 +85,8 @@ "action_react" = "回应"; "action_reject" = "拒绝"; "action_remove" = "移除"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "回复"; "action_reply_in_thread" = "在消息列中回复"; "action_report_bug" = "报告错误"; @@ -95,7 +101,7 @@ "action_send_message" = "发送消息"; "action_share" = "分享"; "action_share_link" = "分享链接"; -"action_show" = "Show"; +"action_show" = "显示"; "action_sign_in_again" = "再次登录"; "action_signout" = "登出"; "action_signout_anyway" = "仍然登出"; @@ -119,27 +125,28 @@ "banner_set_up_recovery_title" = "设置恢复"; "common_about" = "关于"; "common_acceptable_use_policy" = "可接受的使用政策"; +"common_adding_caption" = "Adding caption"; "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_creating_room" = "正在创建聊天室..."; +"common_current_user_left_room" = "离开聊天室"; +"common_dark" = "深色"; "common_decryption_error" = "解密错误"; "common_developer_options" = "开发者选项"; -"common_device_id" = "Device ID"; +"common_device_id" = "设备 ID"; "common_direct_chat" = "私聊"; "common_edited_suffix" = "(已编辑)"; "common_editing" = "编辑中"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; -"common_encryption" = "Encryption"; +"common_encryption" = "加密"; "common_encryption_enabled" = "已启用加密"; "common_enter_your_pin" = "输入 PIN 码"; "common_error" = "错误"; @@ -150,12 +157,12 @@ "common_favourited" = "已收藏"; "common_file" = "文件"; "common_forward_message" = "转发消息"; -"common_frequently_used" = "Frequently used"; +"common_frequently_used" = "常用"; "common_gif" = "GIF"; "common_image" = "图片"; "common_in_reply_to" = "回复 %1$@"; "common_invite_unknown_profile" = "找不到此 Matrix ID,因此可能无法收到邀请。"; -"common_leaving_room" = "正在离开房间"; +"common_leaving_room" = "正在离开聊天室"; "common_light" = "浅色"; "common_link_copied_to_clipboard" = "链接已复制到剪贴板"; "common_loading" = "正在加载..."; @@ -166,7 +173,7 @@ "common_modern" = "现代"; "common_mute" = "静音"; "common_no_results" = "没有结果"; -"common_no_room_name" = "无房间名"; +"common_no_room_name" = "无聊天室名"; "common_offline" = "离线"; "common_optic_id_ios" = "光学 ID"; "common_or" = "或"; @@ -189,8 +196,8 @@ "common_report_a_problem" = "报告问题"; "common_report_submitted" = "报告已提交"; "common_rich_text_editor" = "富文本编辑器"; -"common_room" = "房间"; -"common_room_name" = "房间名称"; +"common_room" = "聊天室"; +"common_room_name" = "聊天室名称"; "common_room_name_placeholder" = "例如:项目名称"; "common_saved_changes" = "保存的更改"; "common_saving" = "正在保存"; @@ -218,41 +225,42 @@ "common_third_party_notices" = "第三方通知"; "common_thread" = "消息列"; "common_topic" = "话题"; -"common_topic_placeholder" = "这个房间是关于什么的?"; +"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_message" = "无法向部分用户发送邀请。"; "common_unable_to_invite_title" = "无法发送邀请"; "common_unlock" = "解锁"; "common_unmute" = "解除静音"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "不支持的事件"; "common_username" = "用户名"; "common_verification_cancelled" = "验证已取消"; "common_verification_complete" = "验证完成"; -"common_verification_failed" = "Verification failed"; -"common_verified" = "Verified"; +"common_verification_failed" = "验证失败"; +"common_verified" = "已验证"; "common_verify_device" = "验证设备"; -"common_verify_identity" = "Verify identity"; +"common_verify_identity" = "验证身份"; "common_video" = "视频"; "common_voice_message" = "语音消息"; "common_waiting" = "等待..."; "common_waiting_for_decryption_key" = "正在等待解密密钥"; -"common.copied_to_clipboard" = "Copied to clipboard"; +"common.copied_to_clipboard" = "已复制到剪贴板"; "common.do_not_show_this_again" = "不再显示此内容"; "common.open_source_licenses" = "开源许可证"; -"common.pinned" = "Pinned"; +"common.pinned" = "已置顶"; "common.send_to" = "发送至"; -"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" = "输入恢复密钥"; +"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$@'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" = "%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" = "请在系统设置中授予权限。"; @@ -274,7 +282,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 鉴权服务和账户创建。"; "error_failed_creating_the_permalink" = "创建固定链接失败"; "error_failed_loading_map" = "%1$@ 无法加载地图,请稍后再试。"; "error_failed_loading_messages" = "加载消息失败"; @@ -285,7 +293,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" = "由未经其所有者验证的设备加密。"; @@ -295,9 +303,9 @@ "invite_friends_rich_title" = "🔐️ 加入我 %1$@"; "invite_friends_text" = "嗨!请通过 %1$@ 与我联系:%2$@"; "leave_conversation_alert_subtitle" = "您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。"; -"leave_room_alert_empty_subtitle" = "确定要离开这个房间吗?这里只有你一个人。如果你离开此房间,包括你在内的所有人都将无法进入。"; -"leave_room_alert_private_subtitle" = "确定要离开这个房间吗?此房间不公开,没有邀请你将无法重新加入。"; -"leave_room_alert_subtitle" = "确定要离开房间吗?"; +"leave_room_alert_empty_subtitle" = "确定要离开此聊天室吗?此处只有你一个人。如果离开此聊天室,包括你在内的所有人都将无法进入。"; +"leave_room_alert_private_subtitle" = "确定要离开此聊天室吗?此聊天室不公开,没有邀请你将无法重新加入。"; +"leave_room_alert_subtitle" = "确定要离开聊天室吗?"; "login_initial_device_name_ios" = "%1$@ iOS"; "notification_channel_call" = "通话"; "notification_channel_listening_for_events" = "监听事件"; @@ -305,14 +313,14 @@ "notification_channel_ringing_calls" = "来电振铃"; "notification_channel_silent" = "静默通知"; "notification_incoming_call" = "来电"; -"notification_inline_reply_failed" = "** 无法发送——请打开房间"; +"notification_inline_reply_failed" = "** 无法发送——请打开聊天室"; "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" = "邀请你加入聊天室"; +"notification_room_invite_body_with_sender" = "%1$@ 邀请您加入房间"; "notification_sender_me" = "我"; "notification_sender_mention_reply" = "%1$@提及或回复"; "notification_test_push_notification_content" = "您正在查看通知!点击我!"; @@ -344,61 +352,85 @@ "rich_text_editor_unindent" = "取消缩进"; "rich_text_editor_url_placeholder" = "链接"; "rich_text_editor_a11y_add_attachment" = "添加附件"; -"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; +"rich_text_editor_composer_caption_placeholder" = "可选的标题……"; "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_create_room_room_access_section_anyone_option_description" = "任何人都可以加入此房间"; +"screen_create_room_room_access_section_anyone_option_title" = "任何人"; +"screen_create_room_room_access_section_header" = "房间访问权限"; +"screen_create_room_room_access_section_knocking_option_description" = "任何人都可以请求加入房间,但必须由管理员或审核人接受"; +"screen_create_room_room_access_section_knocking_option_title" = "请求加入"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; +"screen_create_room_room_address_section_footer" = "要使该房间在公开房间目录中可见,您需要一个房间地址。"; +"screen_create_room_room_address_section_title" = "房间地址"; +"screen_create_room_room_visibility_section_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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "处理要上传的媒体失败,请重试。"; +"screen_media_upload_preview_error_failed_sending" = "上传媒体失败,请重试。"; "screen_pinned_timeline_empty_state_description" = "按下消息并选择 “%1$@” 将其包含在此处。"; "screen_pinned_timeline_empty_state_headline" = "固定重要消息,以便轻松发现它们"; "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_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$@ 验证了所有设备后重试。"; "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" = "通知整个房间"; +"screen_resolve_send_failure_you_unsigned_device_subtitle" = "您有未验证的设备。您仍然可以发送消息;也可以暂时取消,并在验证所有设备后稍后重试。"; +"screen_resolve_send_failure_you_unsigned_device_title" = "您的消息未发送,因为您有尚未验证的设备。"; +"screen_room_mentions_at_room_subtitle" = "通知整个聊天室"; +"screen_room_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "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_room_details_requests_to_join_title" = "Requests to join"; +"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" = "Message not sent because you have not verified one or more of your devices."; +"screen_timeline_item_menu_send_failure_you_unsigned_device" = "消息未发送,因为您有尚未验证的设备。"; "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_form_title" = "寻找账户提供方"; +"screen_account_provider_signin_title" = "您即将登录 %@"; "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_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" = "如果关闭,您的已读回执将不会发送给别人。您仍能收到别人的已读回执。"; +"screen_advanced_settings_send_read_receipts_description" = "关闭后已读回执将不会发送给他人,但仍能收到他人的已读回执。"; "screen_advanced_settings_share_presence" = "分享在线状态"; -"screen_advanced_settings_share_presence_description" = "如果关闭,您将无法发送或接收已读回执、输入通知"; +"screen_advanced_settings_share_presence_description" = "关闭后将无法发送或接收已读回执、输入通知"; "screen_advanced_settings_view_source_description" = "启用在时间轴中查看消息源码的选项。"; "screen_analytics_prompt_data_usage" = "我们不会记录或分析任何个人数据"; "screen_analytics_prompt_help_us_improve" = "共享匿名使用数据以帮助我们排查问题。"; @@ -449,8 +481,8 @@ "screen_bug_report_view_logs" = "查看日志"; "screen_change_account_provider_matrix_org_subtitle" = "Matrix.org 由 Matrix.org 基金会运营,是用于安全、去中心化的通信的公共 Matrix 网络上的大型免费服务器。"; "screen_change_account_provider_other" = "其他"; -"screen_change_account_provider_subtitle" = "使用其他帐户提供者,例如您自己的私人服务器或工作帐户。"; -"screen_change_account_provider_title" = "更改账户提供者"; +"screen_change_account_provider_subtitle" = "使用其他账户提供商,例如您自己的私人服务器或工作账户。"; +"screen_change_account_provider_title" = "更改账户提供方"; "screen_change_server_error_invalid_homeserver" = "我们无法访问此服务器。请检查您输入的服务器网址是否正确。如果 URL 正确,请联系您的服务器管理员寻求进一步帮助。"; "screen_change_server_error_invalid_well_known" = "由于 Well Known 文件中的问题,Sliding Sync 不可用:\n%1$@"; "screen_change_server_error_no_sliding_sync_message" = "该服务器目前不支持 Sliding Sync。"; @@ -460,16 +492,16 @@ "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_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" = "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_change_description" = "如果您丢失了所有现有设备,使用恢复密钥恢复您的密码学身份和消息历史记录。"; +"screen_chat_backup_recovery_action_confirm_description" = "您的密钥存储当前不同步。"; "screen_chat_backup_recovery_action_setup_description" = "在丢失或从 %1$@ 登出所有设备的情况下访问加密消息。"; -"screen_create_account_title" = "Create account"; +"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$@"; @@ -486,23 +518,23 @@ "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_error_creating_room" = "创建聊天室时出错"; +"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" = "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" = "请确认您要停用您的账户。此操作无法撤消。"; +"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_list_item_1_bold_part" = "永久禁用"; +"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" = "您确定要删除此投票吗?"; "screen_edit_profile_display_name" = "显示名称"; "screen_edit_profile_display_name_placeholder" = "你的显示名称"; @@ -532,12 +564,12 @@ "screen_invites_empty_list" = "没有邀请"; "screen_invites_invited_you" = "%1$@ (%2$@)邀请了你"; "screen_join_room_join_action" = "加入聊天室"; -"screen_join_room_knock_action" = "加入房间"; +"screen_join_room_knock_action" = "加入聊天室"; "screen_join_room_space_not_supported_description" = "%1$@ 尚不支持空间。您可以通过 Web 端访问空间"; "screen_join_room_space_not_supported_title" = "空间尚不支持"; -"screen_join_room_subtitle_knock" = "点击下面的按钮,系统将通知房间管理员。获得批准后,您将能够加入对话。"; +"screen_join_room_subtitle_knock" = "点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。"; "screen_join_room_subtitle_no_preview" = "只有聊天室成员才能查看消息历史记录。"; -"screen_join_room_title_knock" = "想加入这个房间吗?"; +"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" = "如果您登出所有设备,您的加密消息将丢失。"; @@ -555,8 +587,6 @@ "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" = "您可以稍后更改设置。"; @@ -565,7 +595,7 @@ "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" = "全部消息"; @@ -576,11 +606,11 @@ "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_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_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" = "系统通知已关闭"; @@ -615,7 +645,7 @@ "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_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$@"; @@ -623,7 +653,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_subtitle" = "仅在您的账户提供方支持时才可用。"; "screen_qr_code_login_initial_state_title" = "在另一台设备上打开 %1$@ 以获取二维码"; "screen_qr_code_login_invalid_scan_state_description" = "使用其他设备上显示的二维码。"; "screen_qr_code_login_invalid_scan_state_subtitle" = "二维码错误"; @@ -634,7 +664,7 @@ "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_subtitle" = "您的账户提供方可能会要求您提供以下代码来验证登录。"; "screen_qr_code_login_verify_code_title" = "您的验证码"; "screen_recovery_key_change_description" = "如果您丢失了现有的恢复密钥,请获取新的恢复密钥。更改恢复密钥后,您的旧密钥将不再起作用。"; "screen_recovery_key_change_generate_key" = "生成新的恢复密钥"; @@ -642,7 +672,7 @@ "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_content" = "请重试以确认访问您的密钥存储。"; "screen_recovery_key_confirm_error_title" = "恢复密钥不正确"; "screen_recovery_key_confirm_key_description" = "如果您有安全密钥或安全短语,也可以用。"; "screen_recovery_key_confirm_key_placeholder" = "输入……"; @@ -651,14 +681,14 @@ "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" = "请确认是否要隐藏该用户当前和未来的所有信息"; @@ -671,7 +701,7 @@ "screen_reset_encryption_password_title" = "输入您的账户密码以继续"; "screen_reset_identity_confirmation_subtitle" = "您将要转到您的%1$@帐户来重置您的身份信息。之后,您将被带回该应用。"; "screen_reset_identity_confirmation_title" = "无法确认?请前往您的帐户重置您的身份。"; -"screen_room_alias_resolver_resolve_alias_failure" = "无法解析房间别名。"; +"screen_room_alias_resolver_resolve_alias_failure" = "无法解析聊天室别名。"; "screen_room_attachment_source_camera" = "相机"; "screen_room_attachment_source_camera_video" = "录制视频"; "screen_room_attachment_source_files" = "附件"; @@ -693,7 +723,7 @@ "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_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" = "管理员自动拥有协管员权限"; @@ -704,22 +734,22 @@ "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_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_muting" = "无法将此房间静音,请重试。"; -"screen_room_details_error_unmuting" = "无法取消此房间的静音,请重试。"; +"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_share_room_title" = "分享房间"; +"screen_room_details_share_room_title" = "分享聊天室"; "screen_room_details_title" = "聊天室信息"; -"screen_room_details_updating_room" = "正在更新房间……"; +"screen_room_details_updating_room" = "正在更新聊天室……"; "screen_room_directory_search_loading_error" = "加载失败"; -"screen_room_directory_search_title" = "房间目录"; +"screen_room_directory_search_title" = "聊天室目录"; "screen_room_encrypted_history_banner" = "消息历史记录当前不可用。"; "screen_room_encrypted_history_banner_unverified" = "此聊天室无法查看消息历史记录。请验证此设备以查看之。"; "screen_room_error_failed_retrieving_user_details" = "无法获取用户信息"; @@ -732,19 +762,19 @@ "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_details_verify_button_subtitle" = "使用 Web 应用程序验证此用户。"; +"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_description" = "即使受到邀请,他们也无法再次加入聊天室。"; "screen_room_member_list_ban_member_confirmation_title" = "您确定要封禁该成员吗?"; -"screen_room_member_list_banned_empty" = "这个房间里没有被封禁的用户。"; +"screen_room_member_list_banned_empty" = "此聊天室里没有被封禁的用户。"; "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" = "从聊天室移除"; "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_message" = "如果受到邀请,他们可以重新加入聊天室。"; "screen_room_member_list_manage_member_unban_title" = "解封用户"; "screen_room_member_list_manage_member_user_info" = "查看个人资料"; "screen_room_member_list_mode_banned" = "已封禁用户"; @@ -765,7 +795,7 @@ "screen_room_notification_settings_error_loading_settings" = "加载通知设置时出错。"; "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_mentions_only_disclaimer" = "服务器在加密聊天室中不支持此选项,无法在此聊天室收到通知。"; "screen_room_notification_settings_mode_all_messages" = "所有消息"; "screen_room_notification_settings_room_custom_settings_title" = "在这个聊天室,通知我:"; "screen_room_retry_send_menu_send_again_action" = "再次发送"; @@ -787,9 +817,10 @@ "screen_room_timeline_add_reaction" = "添加表情符号"; "screen_room_timeline_beginning_of_room" = "这是 %1$@ 聊天室的开始。"; "screen_room_timeline_beginning_of_room_no_name" = "这是本对话的开始。"; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "折叠"; "screen_room_timeline_message_copied" = "消息已复制"; -"screen_room_timeline_no_permission_to_post" = "您无权在此房间发言"; +"screen_room_timeline_no_permission_to_post" = "您无权在此聊天室发言"; "screen_room_timeline_reactions_show_more" = "展开"; "screen_room_timeline_read_marker_title" = "新消息"; "screen_room_title" = "聊天"; @@ -817,10 +848,10 @@ "screen_roomlist_main_space_title" = "全部聊天"; "screen_roomlist_mark_as_read" = "标记为已读"; "screen_roomlist_mark_as_unread" = "标记为未读"; -"screen_roomlist_room_directory_button_title" = "浏览所有房间"; +"screen_roomlist_room_directory_button_title" = "浏览所有聊天室"; "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" = "发生了一些错误。网络请求超时,或者被服务器拒绝。"; @@ -830,23 +861,27 @@ "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_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_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" = "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_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_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "在另一台验证的设备上打开应用"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "在另一台设备上开始验证"; "screen_session_verification_waiting_to_accept_subtitle" = "请在其他会话中接受验证请求。"; "screen_session_verification_waiting_to_accept_title" = "等待接受请求"; "screen_share_location_title" = "分享位置"; @@ -873,7 +908,7 @@ "screen_start_chat_error_starting_chat" = "在开始聊天时发生了错误"; "screen_view_location_title" = "位置"; "screen_welcome_bullet_1" = "今年晚些时候将增加通话、投票、搜索等功能。"; -"screen_welcome_bullet_2" = "加密房间的消息历史记录尚不可用。"; +"screen_welcome_bullet_2" = "加密聊天室的消息历史记录尚不可用。"; "screen_welcome_bullet_3" = "我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。"; "screen_welcome_button" = "开始吧!"; "screen_welcome_subtitle" = "以下是您需要了解的内容:"; @@ -896,21 +931,21 @@ "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_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" = "%1$@ 封禁了 %2$@"; "state_event_room_ban_by_you" = "你封禁了 %1$@"; -"state_event_room_created" = "%1$@ 创建了房间"; -"state_event_room_created_by_you" = "你创建了房间"; +"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_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$@ 加入"; @@ -920,12 +955,12 @@ "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_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$@ 更改了置顶消息"; @@ -938,14 +973,14 @@ "state_event_room_reject_by_you" = "你拒绝了邀请"; "state_event_room_remove" = "%1$@ 移除了 %2$@"; "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_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_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$@ 对其成员资格进行了未知更改"; @@ -1004,9 +1039,9 @@ "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_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" = "此处"; @@ -1018,8 +1053,8 @@ "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_create_room_room_name_label" = "聊天室名称"; +"screen_create_room_title" = "创建聊天室"; "screen_dm_details_block_alert_action" = "封禁"; "screen_dm_details_block_alert_description" = "被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。"; "screen_dm_details_block_user" = "封禁用户"; @@ -1032,8 +1067,8 @@ "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_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" = "拍摄照片"; @@ -1047,10 +1082,10 @@ "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_leave_room_title" = "离开聊天室"; "screen_room_details_notification_title" = "通知"; "screen_room_details_roles_and_permissions" = "角色与权限"; -"screen_room_details_room_name_label" = "房间名称"; +"screen_room_details_room_name_label" = "聊天室名称"; "screen_room_details_security_title" = "安全"; "screen_room_details_topic_title" = "话题"; "screen_room_error_failed_processing_media" = "处理要上传的媒体失败,请重试。"; @@ -1058,8 +1093,8 @@ "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_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" = "您的密钥仍在备份中"; diff --git a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.stringsdict index dd12b72dad..8fcb14b35d 100644 --- a/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/zh-Hans.lproj/Localizable.stringsdict @@ -139,7 +139,7 @@ NSStringFormatValueTypeKey d other - %d 个房间 + %d 个聊天室 screen_app_lock_subtitle @@ -198,6 +198,22 @@ %1$d 人 + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey @@ -209,7 +225,7 @@ NSStringFormatValueTypeKey d other - %1$d 聊天室变更 + %1$d 个聊天室变化 screen_room_typing_many_members diff --git a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings index ebb04ae153..543bf88bbb 100644 --- a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "錄製語音訊息。"; "a11y_voice_message_stop_recording" = "停止錄音"; "action_accept" = "接受"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "新增至時間軸"; "action_back" = "返回"; "action_call" = "通話"; @@ -35,8 +36,10 @@ "action_confirm_password" = "Confirm password"; "action_continue" = "繼續"; "action_copy" = "複製"; +"action_copy_caption" = "Copy caption"; "action_copy_link" = "複製連結"; "action_copy_link_to_message" = "複製訊息連結"; +"action_copy_text" = "Copy text"; "action_create" = "建立"; "action_create_a_room" = "建立聊天室"; "action_deactivate" = "Deactivate"; @@ -47,6 +50,7 @@ "action_discard" = "捨棄"; "action_done" = "完成"; "action_edit" = "編輯"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "編輯投票"; "action_enable" = "啟用"; "action_end_poll" = "結束投票"; @@ -81,6 +85,8 @@ "action_react" = "回應"; "action_reject" = "拒絕"; "action_remove" = "移除"; +"action_remove_caption" = "Remove caption"; +"action_remove_message" = "Remove message"; "action_reply" = "回覆"; "action_reply_in_thread" = "在討論串中回覆"; "action_report_bug" = "回報程式錯誤"; @@ -119,18 +125,18 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "關於"; "common_acceptable_use_policy" = "可接受使用政策"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "進階設定"; "common_analytics" = "分析"; "common_appearance" = "外觀"; "common_audio" = "音訊"; "common_blocked_users" = "封鎖的使用者"; "common_bubbles" = "泡泡"; -"common_call_invite" = "Call in progress (unsupported)"; "common_call_started" = "Call started"; "common_chat_backup" = "聊天室備份"; "common_copyright" = "著作權"; "common_creating_room" = "正在建立聊天室..."; -"common_current_user_left_room" = "離開聊天室"; +"common_current_user_left_room" = "已離開聊天室"; "common_dark" = "深色"; "common_decryption_error" = "解密錯誤"; "common_developer_options" = "開發者選項"; @@ -138,6 +144,7 @@ "common_direct_chat" = "私訊"; "common_edited_suffix" = "(已編輯)"; "common_editing" = "編輯中"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "已啟用加密"; @@ -226,6 +233,7 @@ "common_unable_to_invite_title" = "無法發送邀請"; "common_unlock" = "解鎖"; "common_unmute" = "開啟通知"; +"common_unsupported_call" = "Unsupported call"; "common_unsupported_event" = "不支援的事件"; "common_username" = "使用者名稱"; "common_verification_cancelled" = "驗證已取消"; @@ -294,7 +302,7 @@ "full_screen_intent_banner_title" = "提升您的通話體驗"; "invite_friends_rich_title" = "🔐️ Join me on %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_conversation_alert_subtitle" = "您確定要離開對話嗎?此對話不是公開的,如果沒有收到邀請,您無法重新加入。"; "leave_room_alert_empty_subtitle" = "您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。"; "leave_room_alert_private_subtitle" = "您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"; "leave_room_alert_subtitle" = "您確定要離開聊天室嗎?"; @@ -348,14 +356,16 @@ "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_access_section_anyone_option_description" = "Anyone can join this room"; +"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; +"screen_create_room_room_access_section_header" = "Room Access"; +"screen_create_room_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_room_access_section_knocking_option_title" = "Ask to join"; +"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _"; +"screen_create_room_room_address_not_available_error_description" = "This room address already exists. Please try editing the room address field or change the room name"; "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?"; @@ -363,6 +373,23 @@ "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_knock_requests_list_accept_all_alert_confirm_button_title" = "Yes, accept all"; +"screen_knock_requests_list_accept_all_alert_description" = "Are you sure you want to accept all requests to join?"; +"screen_knock_requests_list_accept_all_alert_title" = "Accept all requests"; +"screen_knock_requests_list_accept_all_button_title" = "Accept all"; +"screen_knock_requests_list_ban_alert_confirm_button_title" = "Yes, decline and ban"; +"screen_knock_requests_list_ban_alert_description" = "Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again."; +"screen_knock_requests_list_ban_alert_title" = "Decline and ban from accessing"; +"screen_knock_requests_list_decline_alert_confirm_button_title" = "Yes, decline"; +"screen_knock_requests_list_decline_alert_description" = "Are you sure you want to decline %1$@ request to join this room?"; +"screen_knock_requests_list_decline_alert_title" = "Decline access"; +"screen_knock_requests_list_decline_and_ban_action_title" = "Decline and ban"; +"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here."; +"screen_knock_requests_list_empty_state_title" = "No pending request to join"; +"screen_knock_requests_list_title" = "Requests to join"; +"screen_media_upload_preview_caption_warning" = "Captions might not be visible to people using older apps."; +"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; +"screen_media_upload_preview_error_failed_sending" = "無法上傳媒體檔案,請稍後再試。"; "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."; @@ -375,11 +402,16 @@ "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_multiple_knock_requests_view_all_button_title" = "View all"; "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_single_knock_request_accept_button_title" = "Accept"; +"screen_room_single_knock_request_title" = "%1$@ wants to join this room"; +"screen_room_single_knock_request_view_button_title" = "View"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_room_details_requests_to_join_title" = "Requests to join"; "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."; @@ -555,8 +587,6 @@ "screen_login_title" = "歡迎回來!"; "screen_login_title_with_homeserver" = "登入 %1$@"; "screen_media_picker_error_failed_selection" = "Failed selecting media, please try again."; -"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; -"screen_media_upload_preview_error_failed_sending" = "無法上傳媒體檔案,請稍後再試。"; "screen_migration_message" = "這是一次性的程序,感謝您耐心等候。"; "screen_migration_title" = "正在設定您的帳號。"; "screen_notification_optin_subtitle" = "You can change your settings later."; @@ -787,6 +817,7 @@ "screen_room_timeline_add_reaction" = "新增表情符號"; "screen_room_timeline_beginning_of_room" = "This is the beginning of %1$@."; "screen_room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation."; +"screen_room_timeline_legacy_call" = "Unsupported call. Ask if the caller can use the new Element X app."; "screen_room_timeline_less_reactions" = "較少"; "screen_room_timeline_message_copied" = "訊息已複製"; "screen_room_timeline_no_permission_to_post" = "您沒有權限在此聊天室傳送訊息"; @@ -847,6 +878,10 @@ "screen_session_verification_request_title" = "Verification requested"; "screen_session_verification_they_dont_match" = "不一樣"; "screen_session_verification_they_match" = "一樣"; +"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here."; +"screen_session_verification_use_another_device_title" = "Open the app on another verified device"; +"screen_session_verification_waiting_another_device_subtitle" = "You should see a popup on the other device. Start the verification from there now."; +"screen_session_verification_waiting_another_device_title" = "Start verification on the other device"; "screen_session_verification_waiting_to_accept_subtitle" = "準備開始驗證,請到您的其他工作階段接受請求。"; "screen_session_verification_waiting_to_accept_title" = "等待接受請求"; "screen_share_location_title" = "分享位置"; diff --git a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.stringsdict index f580e973f2..f38cd938da 100644 --- a/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/zh-Hant-TW.lproj/Localizable.stringsdict @@ -202,6 +202,22 @@ %1$d 位夥伴 + screen_room_multiple_knock_requests_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ +%2$d other want to join this room + other + %1$@ +%2$d others want to join this room + + screen_room_timeline_state_changes NSStringLocalizedFormatKey diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index e220fce2d3..b01beac1f3 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -30,8 +30,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg private var userSession: UserSessionProtocol? { didSet { userSessionObserver?.cancel() - if let userSession { - userSession.clientProxy.roomsToAwait = storedRoomsToAwait + if userSession != nil { configureElementCallService() configureNotificationManager() observeUserSessionChanges() @@ -55,8 +54,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg let notificationManager: NotificationManagerProtocol private let appRouteURLParser: AppRouteURLParser + @Consumable private var storedAppRoute: AppRoute? - private var storedRoomsToAwait: Set = [] + @Consumable private var storedInlineReply: (roomID: String, message: String)? + @Consumable private var storedRoomsToAwait: Set? init(appDelegate: AppDelegate) { let appHooks = AppHooks() @@ -235,6 +236,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } else { handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias)) } + case .share: + guard isExternalURL else { + MXLog.error("Received unexpected internal share route") + break + } + handleAppRoute(route) default: break } @@ -307,7 +314,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg if let userSession { userSession.clientProxy.roomsToAwait.insert(roomID) } else { - storedRoomsToAwait.insert(roomID) + storedRoomsToAwait = [roomID] } } @@ -315,32 +322,19 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async { - guard let userSession else { - fatalError("User session not setup") - } - MXLog.info("[AppCoordinator] handle notification reply") guard let roomID = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else { return } - guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { - MXLog.error("Tried to reply in an unjoined room: \(roomID)") + if userSession == nil { + // Store the data so it can be used after the session is established + storedInlineReply = (roomID, replyText) return } - switch await roomProxy.timeline.sendMessage(replyText, - html: nil, - inReplyToEventID: nil, - intentionalMentions: .empty) { - case .success: - break - default: - // error or no room proxy - await service.showLocalNotification(with: "⚠️ " + L10n.commonError, - subtitle: L10n.errorSomeMessagesHaveNotBeenSent) - } + await processInlineReply(roomID: roomID, replyText: replyText) } // MARK: - Private @@ -467,6 +461,24 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg authenticationFlowCoordinator?.start() } + + private func runPostSessionSetupTasks() async { + guard let userSession, let userSessionFlowCoordinator else { + fatalError("User session not setup") + } + + if let storedRoomsToAwait { + userSession.clientProxy.roomsToAwait = storedRoomsToAwait + } + + if let storedAppRoute { + userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false) + } + + if let storedInlineReply { + await processInlineReply(roomID: storedInlineReply.roomID, replyText: storedInlineReply.message) + } + } private func startAuthenticationSoftLogout() { guard let userSession else { @@ -547,9 +559,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg userSessionFlowCoordinator.start() self.userSessionFlowCoordinator = userSessionFlowCoordinator - - if let storedAppRoute { - userSessionFlowCoordinator.handleAppRoute(storedAppRoute, animated: false) + + Task { + await runPostSessionSetupTasks() } } @@ -560,7 +572,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg showLoadingIndicator() - stopSync() + stopSync(isBackgroundTask: false) userSessionFlowCoordinator?.stop() guard !isSoft else { @@ -747,7 +759,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg navigationRootCoordinator.setRootCoordinator(PlaceholderScreenCoordinator()) - stopSync() + stopSync(isBackgroundTask: false) userSessionFlowCoordinator?.stop() let userID = userSession.clientProxy.userID @@ -825,7 +837,29 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg SentrySDK.close() MXLog.info("SentrySDK stopped") } - + + private func processInlineReply(roomID: String, replyText: String) async { + guard let userSession else { + fatalError("User session not setup") + } + + guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { + MXLog.error("Tried to reply in an unjoined room: \(roomID)") + return + } + + switch await roomProxy.timeline.sendMessage(replyText, + html: nil, + inReplyToEventID: nil, + intentionalMentions: .empty) { + case .success: + break + default: + await notificationManager.showLocalNotification(with: "⚠️ " + L10n.commonError, + subtitle: L10n.errorSomeMessagesHaveNotBeenSent) + } + } + // MARK: Toasts and loading indicators private static let loadingIndicatorIdentifier = "\(AppCoordinator.self)-Loading" @@ -847,8 +881,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg // MARK: - Application State - private func stopSync() { - userSession?.clientProxy.stopSync() + private func stopSync(isBackgroundTask: Bool, completion: (() -> Void)? = nil) { + if isBackgroundTask, UIApplication.shared.applicationState == .active { + // Attempt to stop the background task sync loop cleanly, only if the app not already running + return + } + userSession?.clientProxy.stopSync(completion: completion) clientProxyObserver = nil } @@ -910,7 +948,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg @objc private func applicationWillTerminate() { - stopSync() + stopSync(isBackgroundTask: false) } @objc @@ -929,9 +967,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg backgroundTask = appMediator.beginBackgroundTask { [weak self] in guard let self else { return } - stopSync() - - if let backgroundTask { + MXLog.info("Background task is about to expire.") + stopSync(isBackgroundTask: true) { [weak self] in + guard let self, let backgroundTask else { return } + + MXLog.info("Ending background task.") appMediator.endBackgroundTask(backgroundTask) self.backgroundTask = nil } @@ -987,13 +1027,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg scheduleBackgroundAppRefresh() 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 is about to expire.") - MXLog.info("Background app refresh task expired") - task.setTaskCompleted(success: true) + self?.stopSync(isBackgroundTask: true) { + MXLog.info("Marking Background app refresh task as complete.") + task.setTaskCompleted(success: true) + } } guard let userSession else { @@ -1011,13 +1050,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg .sink(receiveValue: { [weak self] _ in guard let self else { return } MXLog.info("Background app refresh finished") + backgroundRefreshSyncObserver?.cancel() // 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) + stopSync(isBackgroundTask: true) { + MXLog.info("Marking Background app refresh task as complete.") + task.setTaskCompleted(success: true) + } }) } } diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index e3b4b39655..1adbe77bb4 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -47,7 +47,7 @@ final class AppSettings { case fuzzyRoomListSearchEnabled case enableOnlySignedDeviceIsolationMode case knockingEnabled - case frequentEmojisEnabled + case createMediaCaptionsEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -249,6 +249,10 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.optimizeMediaUploads, defaultValue: true, storageType: .userDefaults(store)) var optimizeMediaUploads + + /// Whether or not to show a warning on the media caption composer so the user knows + /// that captions might not be visible to users who are using other Matrix clients. + let shouldShowMediaCaptionWarning = true // MARK: - Element Call @@ -277,7 +281,7 @@ final class AppSettings { // MARK: - Feature Flags - @UserPreference(key: UserDefaultsKeys.publicSearchEnabled, defaultValue: isDevelopmentBuild, storageType: .volatile) + @UserPreference(key: UserDefaultsKeys.publicSearchEnabled, defaultValue: false, storageType: .userDefaults(store)) var publicSearchEnabled @UserPreference(key: UserDefaultsKeys.fuzzyRoomListSearchEnabled, defaultValue: false, storageType: .userDefaults(store)) @@ -290,9 +294,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) var knockingEnabled - @UserPreference(key: UserDefaultsKeys.frequentEmojisEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) - var frequentEmojisEnabled - + @UserPreference(key: UserDefaultsKeys.createMediaCaptionsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var createMediaCaptionsEnabled + #endif // MARK: - Shared diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index d90b9282a5..842bb436b0 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -8,7 +8,7 @@ import Foundation import MatrixRustSDK -enum AppRoute: Equatable { +enum AppRoute: Equatable, Hashable { /// The app's home screen. case roomList /// A room, shown as the root of the stack (popping any child rooms). @@ -41,6 +41,8 @@ enum AppRoute: Equatable { case settings /// The setting screen for key backup. case chatBackupSettings + /// An external share request e.g. from the ShareExtension + case share(ShareExtensionPayload) } struct AppRouteURLParser { @@ -48,6 +50,7 @@ struct AppRouteURLParser { init(appSettings: AppSettings) { urlParsers = [ + AppGroupURLParser(), MatrixPermalinkParser(), ElementWebURLParser(domains: appSettings.elementWebHosts), ElementCallURLParser() @@ -73,6 +76,30 @@ protocol URLParser { func route(from url: URL) -> AppRoute? } +struct AppGroupURLParser: URLParser { + func route(from url: URL) -> AppRoute? { + guard let scheme = url.scheme, + scheme == InfoPlistReader.app.appScheme, + url.pathComponents.last == ShareExtensionConstants.urlPath else { + return nil + } + + guard let query = url.query(percentEncoded: false), + let queryData = query.data(using: .utf8) else { + MXLog.error("Failed processing share parameters") + return nil + } + + do { + let payload = try JSONDecoder().decode(ShareExtensionPayload.self, from: queryData) + return .share(payload) + } catch { + MXLog.error("Failed decoding share payload with error: \(error)") + return nil + } + } +} + /// 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 e17c247812..058b713c8e 100644 --- a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift +++ b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift @@ -412,19 +412,17 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS private struct NavigationSplitCoordinatorView: View { @State private var columnVisibility = NavigationSplitViewVisibility.all - @State private var isInSplitMode = true @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(\.scenePhase) private var scenePhase @ObservedObject var navigationSplitCoordinator: NavigationSplitCoordinator var body: some View { Group { - if isInSplitMode { - navigationSplitView - } else { + if horizontalSizeClass == .compact { navigationStack + } else { + navigationSplitView } } // This needs to be handled on the top level otherwise sheets @@ -450,25 +448,6 @@ private struct NavigationSplitCoordinatorView: View { .animation(.elementDefault, value: navigationSplitCoordinator.overlayPresentationMode) .animation(.elementDefault, value: navigationSplitCoordinator.overlayModule) } - // Handle `horizontalSizeClass` changes breaking the navigation bar - // https://github.com/element-hq/element-x-ios/issues/617 - .onChange(of: horizontalSizeClass) { _, newValue in - guard scenePhase != .background else { - return - } - - isInSplitMode = newValue == .regular - } - .onChange(of: scenePhase) { _, newValue in - guard newValue == .active else { - return - } - - isInSplitMode = horizontalSizeClass == .regular - } - .task { - isInSplitMode = horizontalSizeClass == .regular - } } /// The NavigationStack that will be used in compact layouts diff --git a/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift index 04a37713d5..efc6b0b33e 100644 --- a/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift @@ -6,6 +6,7 @@ // import Combine +import Foundation struct BugReportFlowCoordinatorParameters { enum PresentationMode { @@ -93,12 +94,22 @@ class BugReportFlowCoordinator: FlowCoordinatorProtocol { switch action { case .done: - internalNavigationStackCoordinator?.pop() + if ProcessInfo.processInfo.isiOSAppOnMac { + internalNavigationStackCoordinator?.setSheetCoordinator(nil) + } else { + internalNavigationStackCoordinator?.pop() + } } } .store(in: &cancellables) - internalNavigationStackCoordinator?.push(coordinator) + if ProcessInfo.processInfo.isiOSAppOnMac { + // On macOS the QuickLook is a separate window, closing a pushed QuickLook + // controller closes the whole Settings sheet so lets add another one. + internalNavigationStackCoordinator?.setSheetCoordinator(coordinator) + } else { + internalNavigationStackCoordinator?.push(coordinator) + } } private func dismiss() { diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift index af620ef40b..d8be441aca 100644 --- a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -83,7 +83,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, .roomDetails, .roomMemberDetails, .userProfile, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, - .call, .genericCallLink, .settings: + .call, .genericCallLink, .settings, .share: // These routes aren't in this flow so clear the entire stack. clearRoute(animated: animated) case .chatBackupSettings: diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index 7c65f7bf4b..945e8ac50c 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -288,9 +288,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters) coordinator.actions - .sink { [weak self] action in - guard let self else { return } - + .sink { action in switch action { case .complete: break // Moving to next state is Handled by the global session verification listener diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index f900329307..fcaf4fb7dc 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -65,7 +65,9 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) - guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else { + guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) else { fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil") } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index e9fb052493..5e2d4ce3ae 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -34,6 +34,8 @@ enum RoomFlowCoordinatorEntryPoint: Hashable { case eventID(String) /// The flow will start by showing the room's details. case roomDetails + /// An external media share request + case share(ShareExtensionPayload) var isEventID: Bool { guard case .eventID = self else { return false } @@ -41,6 +43,41 @@ enum RoomFlowCoordinatorEntryPoint: Hashable { } } +struct FocusEvent: Hashable { + /// The event ID that the timeline should be focussed around + let eventID: String + /// if the focus is coming from the pinned timeline, this should also update the pin banner + let shouldSetPin: Bool +} + +private enum PinnedEventsTimelineSource: Hashable { + case room + case details(isRoot: Bool) +} + +private enum PresentationAction: Hashable { + case eventFocus(FocusEvent) + case share(ShareExtensionPayload) + + var focusedEvent: FocusEvent? { + switch self { + case .eventFocus(let focusEvent): + focusEvent + default: + nil + } + } + + var sharedText: String? { + switch self { + case .share(.text(_, let text)): + text + default: + nil + } + } +} + // swiftlint:disable:next type_body_length class RoomFlowCoordinator: FlowCoordinatorProtocol { private let roomID: String @@ -112,6 +149,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { fatalError("This flow coordinator expect a route") } + // swiftlint:disable:next cyclomatic_complexity func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { guard stateMachine.state != .complete else { fatalError("This flow coordinator is `finished` ☠️") @@ -152,7 +190,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated)) } case .event(let eventID, let roomID, let via): - Task { await handleRoomRoute(roomID: roomID, via: via, focussedEventID: eventID, animated: animated) } + Task { + await handleRoomRoute(roomID: roomID, + via: via, + presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: false)), + animated: animated) + } case .childEvent(let eventID, let roomID, let via): if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator { childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated) @@ -161,6 +204,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } else { roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false)) } + case .share(let payload): + guard let roomID = payload.roomID, roomID == self.roomID else { + fatalError("Navigation route doesn't belong to this room flow.") + } + + Task { + await handleRoomRoute(roomID: roomID, + via: [], + presentationAction: .share(payload), + animated: animated) + } case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: break // These are converted to a room ID route one level above. case .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings: @@ -176,7 +230,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) } - private func handleRoomRoute(roomID: String, via: [String], focussedEventID: String? = nil, animated: Bool) async { + private func handleRoomRoute(roomID: String, via: [String], presentationAction: PresentationAction? = nil, animated: Bool) async { guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") } guard let room = await userSession.clientProxy.roomForIdentifier(roomID) else { @@ -187,8 +241,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch room { case .joined(let roomProxy): await storeAndSubscribeToRoomProxy(roomProxy) - let focussedEvent = focussedEventID.map { FocusEvent(eventID: $0, shouldSetPin: false) } - stateMachine.tryEvent(.presentRoom(focussedEvent: focussedEvent), userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated)) default: stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated)) } @@ -256,22 +309,18 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .roomMembersList case (.roomMembersList, .dismissRoomMembersList): return .roomDetails(isRoot: false) - case (.room, .presentRoomMemberDetails(userID: let userID)): - return .roomMemberDetails(userID: userID, previousState: .room) - case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)): - return .roomMemberDetails(userID: userID, previousState: .roomMembersList) + case (_, .presentRoomMemberDetails(userID: let userID)): + return .roomMemberDetails(userID: userID, previousState: fromState) case (.roomMemberDetails(_, let previousState), .dismissRoomMemberDetails): return previousState case (.roomMemberDetails(_, let previousState), .presentUserProfile(let userID)): return .userProfile(userID: userID, previousState: previousState) case (.userProfile(_, let previousState), .dismissUserProfile): return previousState - case (.roomDetails, .presentInviteUsersScreen): - return .inviteUsersScreen(fromRoomMembersList: false) - case (.roomMembersList, .presentInviteUsersScreen): - return .inviteUsersScreen(fromRoomMembersList: true) - case (.inviteUsersScreen(let fromRoomMembersList), .dismissInviteUsersScreen): - return fromRoomMembersList ? .roomMembersList : .roomDetails(isRoot: false) + case (_, .presentInviteUsersScreen): + return .inviteUsersScreen(previousState: fromState) + case (.inviteUsersScreen(let previousState), .dismissInviteUsersScreen): + return previousState case (.room, .presentReportContent(let itemID, let senderID)): return .reportContent(itemID: itemID, senderID: senderID) case (.reportContent, .dismissReportContent): @@ -325,8 +374,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .rolesAndPermissions case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen): return .roomDetails(isRoot: false) - case (.roomDetails, .presentRoomMemberDetails(let userID)): - return .roomMemberDetails(userID: userID, previousState: fromState) case (.room, .presentResolveSendFailure): return .resolveSendFailure case (.resolveSendFailure, .dismissResolveSendFailure): @@ -336,6 +383,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .presentingChild(childRoomID: roomID, previousState: fromState) case (.presentingChild(_, let previousState), .dismissChildFlow): return previousState + case (_, .presentKnockRequestsListScreen): + return .knockRequestsList(previousState: fromState) + case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen): + return previousState default: return nil } @@ -352,9 +403,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (_, .dismissJoinRoomScreen, .complete): dismissFlow(animated: animated) - - case (_, .presentRoom(let focussedEvent), .room): - Task { await self.presentRoom(fromState: context.fromState, focussedEvent: focussedEvent, animated: animated) } + + case (_, .presentRoom(let presentationAction), .room): + Task { + await self.presentRoom(fromState: context.fromState, + presentationAction: presentationAction, + animated: animated) + } case (_, .dismissFlow, .complete): dismissFlow(animated: animated) @@ -434,10 +489,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { break case (.mediaUploadPicker, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)): - presentMediaUploadPreviewScreen(for: fileURL) + presentMediaUploadPreviewScreen(for: fileURL, animated: animated) case (.room, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)): - presentMediaUploadPreviewScreen(for: fileURL) + presentMediaUploadPreviewScreen(for: fileURL, animated: animated) case (.mediaUploadPreview, .dismissMediaUploadPreview, .room): break @@ -505,11 +560,23 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomMemberDetails, .dismissUserProfile, .roomDetails): break - case (.room, .presentResolveSendFailure(let failure, let itemID), .resolveSendFailure): - presentResolveSendFailure(failure: failure, itemID: itemID) + case (.room, .presentResolveSendFailure(let failure, let sendHandle), .resolveSendFailure): + presentResolveSendFailure(failure: failure, sendHandle: sendHandle) case (.resolveSendFailure, .dismissResolveSendFailure, .room): break + + case (.roomDetails, .presentKnockRequestsListScreen, .knockRequestsList): + presentKnockRequestsList() + + case (.knockRequestsList, .dismissKnockRequestsListScreen, .roomDetails): + break + + case (.room, .presentKnockRequestsListScreen, .knockRequestsList): + presentKnockRequestsList() + + case (.knockRequestsList, .dismissKnockRequestsListScreen, .room): + break // Child flow case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild): @@ -545,7 +612,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { /// - fromState: The state that asked for the room presentation. /// - focussedEvent: An (optional) struct that contains the event ID that the timeline should be focussed around, and a boolean telling if such event should update the pinned events banner /// - animated: whether it should animate the transition - private func presentRoom(fromState: State, focussedEvent: FocusEvent?, animated: Bool) async { + private func presentRoom(fromState: State, presentationAction: PresentationAction?, animated: Bool) async { // If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine // through the correct states before presenting the room navigationStackCoordinator.setSheetCoordinator(nil) @@ -562,40 +629,72 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { default: // The room is already on the stack, no need to present it again - // Check if we need to focus on an event - if let focussedEvent { - roomScreenCoordinator?.focusOnEvent(focussedEvent) + switch presentationAction { + case .eventFocus(let focusedEvent): + roomScreenCoordinator?.focusOnEvent(focusedEvent) + case .share(.mediaFile(_, let mediaFile)): + stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated)) + case .share(.text(_, let text)): + roomScreenCoordinator?.shareText(text) + case .none: + break } return } } - Task { - // Flag the room as read on entering, the timeline will take care of the read receipts - await roomProxy.flagAsUnread(false) - } + // Flag the room as read on entering, the timeline will take care of the read receipts + Task { await roomProxy.flagAsUnread(false) } - let userID = userSession.clientProxy.userID + analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace) + + let coordinator = makeRoomScreenCoordinator(presentationAction: presentationAction) + roomScreenCoordinator = coordinator + if !isChildFlow { + let animated = UIDevice.current.userInterfaceIdiom == .phone ? animated : false + navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in + self?.stateMachine.tryEvent(.dismissFlow) + } + } else { + if joinRoomScreenCoordinator != nil { + navigationStackCoordinator.pop() + } + + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in + self?.stateMachine.tryEvent(.dismissFlow) + } + } + + switch presentationAction { + case .share(.mediaFile(_, let mediaFile)): + stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated)) + case .share(.text), .eventFocus: + break // These are both handled in the coordinator's init. + case .none: + break + } + } + + private func makeRoomScreenCoordinator(presentationAction: PresentationAction?) -> RoomScreenCoordinator { + let userID = userSession.clientProxy.userID let timelineItemFactory = RoomTimelineItemFactory(userID: userID, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) - let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, - initialFocussedEventID: focussedEvent?.eventID, - timelineItemFactory: timelineItemFactory) + initialFocussedEventID: presentationAction?.focusedEvent?.eventID, + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) self.timelineController = timelineController - 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(clientProxy: userSession.clientProxy, roomProxy: roomProxy, - focussedEvent: focussedEvent, + focussedEvent: presentationAction?.focusedEvent, + sharedText: presentationAction?.sharedText, timelineController: timelineController, mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), @@ -637,27 +736,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) case .presentPinnedEventsTimeline: stateMachine.tryEvent(.presentPinnedEventsTimeline) - case .presentResolveSendFailure(failure: let failure, itemID: let itemID): - stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, itemID: itemID)) + case .presentResolveSendFailure(failure: let failure, sendHandle: let sendHandle): + stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle)) + case .presentKnockRequestsList: + stateMachine.tryEvent(.presentKnockRequestsListScreen) } } .store(in: &cancellables) - roomScreenCoordinator = coordinator - if !isChildFlow { - let animated = UIDevice.current.userInterfaceIdiom == .phone ? animated : false - navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in - self?.stateMachine.tryEvent(.dismissFlow) - } - } else { - if joinRoomScreenCoordinator != nil { - navigationStackCoordinator.pop() - } - - navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in - self?.stateMachine.tryEvent(.dismissFlow) - } - } + return coordinator } private func presentJoinRoomScreen(via: [String], animated: Bool) { @@ -682,7 +769,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) { await storeAndSubscribeToRoomProxy(roomProxy) - stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(presentationAction: nil), userInfo: EventUserInfo(animated: animated)) analytics.trackJoinedRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace, @@ -765,6 +852,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) case .presentPinnedEventsTimeline: stateMachine.tryEvent(.presentPinnedEventsTimeline) + case .presentKnockingRequestsListScreen: + stateMachine.tryEvent(.presentKnockRequestsListScreen) } } .store(in: &cancellables) @@ -811,6 +900,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } + private func presentKnockRequestsList() { + let parameters = KnockRequestsListScreenCoordinatorParameters(roomProxy: roomProxy, mediaProvider: userSession.mediaProvider) + let coordinator = KnockRequestsListScreenCoordinator(parameters: parameters) + + navigationStackCoordinator.push(coordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissKnockRequestsListScreen) + } + } + private func presentRoomDetailsEditScreen() { let stackCoordinator = NavigationStackCoordinator() @@ -897,14 +995,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentMediaUploadPreviewScreen(for url: URL) { + private func presentMediaUploadPreviewScreen(for url: URL, animated: Bool) { let stackCoordinator = NavigationStackCoordinator() let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController, roomProxy: roomProxy, mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: appSettings), title: url.lastPathComponent, - url: url) + url: url, + shouldShowCaptionWarning: appSettings.shouldShowMediaCaptionWarning) let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) @@ -921,7 +1020,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stackCoordinator.setRootCoordinator(mediaUploadPreviewScreenCoordinator) - navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: animated) { [weak self] in self?.stateMachine.tryEvent(.dismissMediaUploadPreview) } } @@ -1094,7 +1193,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, initialFocussedEventID: nil, - timelineItemFactory: timelineItemFactory) + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) let parameters = RoomPollsHistoryScreenCoordinatorParameters(pollInteractionHandler: PollInteractionHandler(analyticsService: analytics, roomProxy: roomProxy), roomTimelineController: roomTimelineController) @@ -1364,7 +1464,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room)) case .displayRoomScreenWithFocussedPin(let eventID): navigationStackCoordinator.setSheetCoordinator(nil) - stateMachine.tryEvent(.presentRoom(focussedEvent: .init(eventID: eventID, shouldSetPin: true))) + stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true)))) } } .store(in: &cancellables) @@ -1376,9 +1476,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.start() } - private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) { + private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) { let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure, - itemID: itemID, + sendHandle: sendHandle, roomProxy: roomProxy, userIndicatorController: userIndicatorController)) coordinator.actionsPublisher.sink { [weak self] action in @@ -1430,6 +1530,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.event(eventID: eventID, roomID: roomID, via: via), animated: true) case .roomDetails: coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) + case .share(let payload): + coordinator.handleAppRoute(.share(payload), animated: true) } } } @@ -1458,7 +1560,7 @@ private extension RoomFlowCoordinator { case roomMembersList case roomMemberDetails(userID: String, previousState: State) case userProfile(userID: String, previousState: State) - case inviteUsersScreen(fromRoomMembersList: Bool) + case inviteUsersScreen(previousState: State) case mediaUploadPicker(source: MediaPickerScreenSource) case mediaUploadPreview(fileURL: URL) case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) @@ -1471,6 +1573,7 @@ private extension RoomFlowCoordinator { case rolesAndPermissions case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource) case resolveSendFailure + case knockRequestsList(previousState: State) /// A child flow is in progress. case presentingChild(childRoomID: String, previousState: State) @@ -1486,7 +1589,7 @@ private extension RoomFlowCoordinator { case presentJoinRoomScreen(via: [String]) case dismissJoinRoomScreen - case presentRoom(focussedEvent: FocusEvent?) + case presentRoom(presentationAction: PresentationAction?) case dismissFlow case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) @@ -1543,12 +1646,15 @@ private extension RoomFlowCoordinator { case presentPinnedEventsTimeline case dismissPinnedEventsTimeline - case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) + case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case dismissResolveSendFailure // Child room flow events case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) case dismissChildFlow + + case presentKnockRequestsListScreen + case dismissKnockRequestsListScreen } } @@ -1562,15 +1668,3 @@ private extension Result { } } } - -private enum PinnedEventsTimelineSource: Hashable { - case room - case details(isRoot: Bool) -} - -struct FocusEvent: Hashable { - /// The event ID that the timeline should be focussed around - let eventID: String - /// if the focus is coming from the pinned timeline, this should also update the pin banner - let shouldSetPin: Bool -} diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 2bde7f7b58..311924d905 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -198,11 +198,20 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .userProfile(let userID): stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated)) case .call(let roomID): - Task { await presentCallScreen(roomID: roomID) } + Task { await presentCallScreen(roomID: roomID, notifyOtherParticipants: false) } case .genericCallLink(let url): presentCallScreen(genericCallLink: url) case .settings, .chatBackupSettings: settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated) + case .share(let payload): + if let roomID = payload.roomID { + stateMachine.processEvent(.selectRoom(roomID: roomID, + via: [], + entryPoint: .share(payload)), + userInfo: .init(animated: animated)) + } else { + stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated)) + } } } @@ -240,6 +249,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .room: .room(roomID: roomID, via: via) case .roomDetails: .roomDetails(roomID: roomID) case .eventID(let eventID): .event(eventID: eventID, roomID: roomID, via: via) // ignored. + case .share(let payload): .share(payload) } roomFlowCoordinator.handleAppRoute(route, animated: animated) } else { @@ -284,6 +294,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { presentUserProfileScreen(userID: userID, animated: animated) case (.userProfileScreen, .dismissedUserProfileScreen, .roomList): break + case (.roomList, .showShareExtensionRoomList, .shareExtensionRoomList(let sharePayload)): + clearRoute(animated: animated) + presentRoomSelectionScreen(sharePayload: sharePayload, animated: animated) + case (.shareExtensionRoomList, .dismissedShareExtensionRoomList, .roomList): + dismissRoomSelectionScreen() default: fatalError("Unknown transition: \(context)") } @@ -356,7 +371,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { switch info.cause { case .unknown: - analytics.trackError(context: nil, domain: .E2EE, name: .UnknownError, timeToDecryptMillis: timeToDecryptMs) + analytics.trackError(context: nil, domain: .E2EE, name: .OlmKeysNotSentError, timeToDecryptMillis: timeToDecryptMs) case .unknownDevice: analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedSentByInsecureDevice, timeToDecryptMillis: timeToDecryptMs) case .unsignedDevice: @@ -365,6 +380,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedVerificationViolation, timeToDecryptMillis: timeToDecryptMs) case .sentBeforeWeJoined: analytics.trackError(context: nil, domain: .E2EE, name: .ExpectedDueToMembership, timeToDecryptMillis: timeToDecryptMs) + case .historicalMessage: + analytics.trackError(context: nil, domain: .E2EE, name: .HistoricalMessage, timeToDecryptMillis: timeToDecryptMs) } } .store(in: &cancellables) @@ -551,7 +568,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { switch action { case .presentCallScreen(let roomProxy): - presentCallScreen(roomProxy: roomProxy) + // Here we assume that the app is running and the call state is already up to date + presentCallScreen(roomProxy: roomProxy, notifyOtherParticipants: !roomProxy.infoPublisher.value.hasRoomCall) case .finished: stateMachine.processEvent(.deselectRoom) } @@ -571,6 +589,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.event(eventID: eventID, roomID: roomID, via: via), animated: animated) case .roomDetails: coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated) + case .share(let payload): + coordinator.handleAppRoute(.share(payload), animated: animated) } Task { @@ -627,22 +647,23 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { presentCallScreen(configuration: .init(genericCallLink: url)) } - private func presentCallScreen(roomID: String) async { + private func presentCallScreen(roomID: String, notifyOtherParticipants: Bool) async { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { return } - presentCallScreen(roomProxy: roomProxy) + presentCallScreen(roomProxy: roomProxy, notifyOtherParticipants: notifyOtherParticipants) } - private func presentCallScreen(roomProxy: JoinedRoomProxyProtocol) { + private func presentCallScreen(roomProxy: JoinedRoomProxyProtocol, notifyOtherParticipants: Bool) { let colorScheme: ColorScheme = appMediator.windowManager.mainWindow.traitCollection.userInterfaceStyle == .light ? .light : .dark presentCallScreen(configuration: .init(roomProxy: roomProxy, clientProxy: userSession.clientProxy, clientID: InfoPlistReader.main.bundleIdentifier, elementCallBaseURL: appSettings.elementCallBaseURL, elementCallBaseURLOverride: appSettings.elementCallBaseURLOverride, - colorScheme: colorScheme)) + colorScheme: colorScheme, + notifyOtherParticipants: notifyOtherParticipants)) } private var callScreenPictureInPictureController: AVPictureInPictureController? @@ -869,7 +890,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationSplitCoordinator.setSheetCoordinator(nil) stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room)) case .startCall(let roomID): - Task { await self.presentCallScreen(roomID: roomID) } + Task { await self.presentCallScreen(roomID: roomID, notifyOtherParticipants: false) } case .dismiss: navigationSplitCoordinator.setSheetCoordinator(nil) } @@ -882,6 +903,54 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } + // MARK: Sharing + + private func presentRoomSelectionScreen(sharePayload: ShareExtensionPayload, animated: Bool) { + guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else { + fatalError() + } + + let stackCoordinator = NavigationStackCoordinator() + + let coordinator = RoomSelectionScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, + roomSummaryProvider: roomSummaryProvider, + mediaProvider: userSession.mediaProvider)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationSplitCoordinator.setSheetCoordinator(nil) + case .confirm(let roomID): + let sharePayload = switch sharePayload { + case .mediaFile(_, let mediaFile): + ShareExtensionPayload.mediaFile(roomID: roomID, mediaFile: mediaFile) + case .text(_, let text): + ShareExtensionPayload.text(roomID: roomID, text: text) + } + + navigationSplitCoordinator.setSheetCoordinator(nil) + + stateMachine.processEvent(.selectRoom(roomID: roomID, + via: [], + entryPoint: .share(sharePayload)), + userInfo: .init(animated: animated)) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + + navigationSplitCoordinator.setSheetCoordinator(stackCoordinator, animated: animated) { [weak self] in + self?.stateMachine.processEvent(.dismissedShareExtensionRoomList) + } + } + + private func dismissRoomSelectionScreen() { + navigationSplitCoordinator.setSheetCoordinator(nil) + } + // MARK: Toasts and loading indicators private static let loadingIndicatorIdentifier = "\(UserSessionFlowCoordinator.self)-Loading" diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index 1c482c1286..2da1dbf315 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -42,10 +42,12 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the user profile screen. This screen clears the navigation. case userProfileScreen + case shareExtensionRoomList(sharePayload: ShareExtensionPayload) + /// The selected room ID from the state if available. var selectedRoomID: String? { switch self { - case .initial, .userProfileScreen: + case .initial, .userProfileScreen, .shareExtensionRoomList: nil case .roomList(let selectedRoomID), .feedbackScreen(let selectedRoomID), @@ -116,6 +118,9 @@ class UserSessionFlowCoordinatorStateMachine { case showUserProfileScreen(userID: String) /// The user profile screen has been dismissed. case dismissedUserProfileScreen + + case showShareExtensionRoomList(sharePayload: ShareExtensionPayload) + case dismissedShareExtensionRoomList } private let stateMachine: StateMachine @@ -193,6 +198,12 @@ class UserSessionFlowCoordinatorStateMachine { case (.userProfileScreen, .dismissedUserProfileScreen): return .roomList(selectedRoomID: nil) + case (.roomList, .showShareExtensionRoomList(let sharePayload)): + return .shareExtensionRoomList(sharePayload: sharePayload) + + case (.shareExtensionRoomList, .dismissedShareExtensionRoomList): + return .roomList(selectedRoomID: nil) + default: return nil } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 2c89b62e8c..cb7e231c6d 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -76,6 +76,8 @@ internal enum L10n { internal static var a11yVoiceMessageStopRecording: String { return L10n.tr("Localizable", "a11y_voice_message_stop_recording") } /// Accept internal static var actionAccept: String { return L10n.tr("Localizable", "action_accept") } + /// Add caption + internal static var actionAddCaption: String { return L10n.tr("Localizable", "action_add_caption") } /// Add to timeline internal static var actionAddToTimeline: String { return L10n.tr("Localizable", "action_add_to_timeline") } /// Back @@ -102,10 +104,14 @@ internal enum L10n { internal static var actionContinue: String { return L10n.tr("Localizable", "action_continue") } /// Copy internal static var actionCopy: String { return L10n.tr("Localizable", "action_copy") } + /// Copy caption + internal static var actionCopyCaption: String { return L10n.tr("Localizable", "action_copy_caption") } /// Copy link internal static var actionCopyLink: String { return L10n.tr("Localizable", "action_copy_link") } /// Copy link to message internal static var actionCopyLinkToMessage: String { return L10n.tr("Localizable", "action_copy_link_to_message") } + /// Copy text + internal static var actionCopyText: String { return L10n.tr("Localizable", "action_copy_text") } /// Create internal static var actionCreate: String { return L10n.tr("Localizable", "action_create") } /// Create a room @@ -126,6 +132,8 @@ internal enum L10n { internal static var actionDone: String { return L10n.tr("Localizable", "action_done") } /// Edit internal static var actionEdit: String { return L10n.tr("Localizable", "action_edit") } + /// Edit caption + internal static var actionEditCaption: String { return L10n.tr("Localizable", "action_edit_caption") } /// Edit poll internal static var actionEditPoll: String { return L10n.tr("Localizable", "action_edit_poll") } /// Enable @@ -198,6 +206,10 @@ internal enum L10n { internal static var actionReject: String { return L10n.tr("Localizable", "action_reject") } /// Remove internal static var actionRemove: String { return L10n.tr("Localizable", "action_remove") } + /// Remove caption + internal static var actionRemoveCaption: String { return L10n.tr("Localizable", "action_remove_caption") } + /// Remove message + internal static var actionRemoveMessage: String { return L10n.tr("Localizable", "action_remove_message") } /// Reply internal static var actionReply: String { return L10n.tr("Localizable", "action_reply") } /// Reply in thread @@ -276,6 +288,8 @@ internal enum L10n { internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } /// Acceptable use policy internal static var commonAcceptableUsePolicy: String { return L10n.tr("Localizable", "common_acceptable_use_policy") } + /// Adding caption + internal static var commonAddingCaption: String { return L10n.tr("Localizable", "common_adding_caption") } /// Advanced settings internal static var commonAdvancedSettings: String { return L10n.tr("Localizable", "common_advanced_settings") } /// Analytics @@ -288,8 +302,6 @@ internal enum L10n { internal static var commonBlockedUsers: String { return L10n.tr("Localizable", "common_blocked_users") } /// Bubbles internal static var commonBubbles: String { return L10n.tr("Localizable", "common_bubbles") } - /// Call in progress (unsupported) - internal static var commonCallInvite: String { return L10n.tr("Localizable", "common_call_invite") } /// Call started internal static var commonCallStarted: String { return L10n.tr("Localizable", "common_call_started") } /// Chat backup @@ -314,6 +326,8 @@ internal enum L10n { internal static var commonEditedSuffix: String { return L10n.tr("Localizable", "common_edited_suffix") } /// Editing internal static var commonEditing: String { return L10n.tr("Localizable", "common_editing") } + /// Editing caption + internal static var commonEditingCaption: String { return L10n.tr("Localizable", "common_editing_caption") } /// * %1$@ %2$@ internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) @@ -512,6 +526,8 @@ internal enum L10n { internal static var commonUnlock: String { return L10n.tr("Localizable", "common_unlock") } /// Unmute internal static var commonUnmute: String { return L10n.tr("Localizable", "common_unmute") } + /// Unsupported call + internal static var commonUnsupportedCall: String { return L10n.tr("Localizable", "common_unsupported_call") } /// Unsupported event internal static var commonUnsupportedEvent: String { return L10n.tr("Localizable", "common_unsupported_event") } /// Username @@ -780,7 +796,7 @@ 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… + /// Add a 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") } @@ -1093,16 +1109,6 @@ 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 @@ -1118,6 +1124,20 @@ internal enum L10n { internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") } /// Public room internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") } + /// Anyone can join this room + internal static var screenCreateRoomRoomAccessSectionAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_anyone_option_description") } + /// Anyone + internal static var screenCreateRoomRoomAccessSectionAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_anyone_option_title") } + /// Room Access + internal static var screenCreateRoomRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_create_room_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 screenCreateRoomRoomAccessSectionKnockingOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_description") } + /// Ask to join + internal static var screenCreateRoomRoomAccessSectionKnockingOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_title") } + /// Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _ + internal static var screenCreateRoomRoomAddressInvalidSymbolsErrorDescription: String { return L10n.tr("Localizable", "screen_create_room_room_address_invalid_symbols_error_description") } + /// This room address already exists. Please try editing the room address field or change the room name + internal static var screenCreateRoomRoomAddressNotAvailableErrorDescription: String { return L10n.tr("Localizable", "screen_create_room_room_address_not_available_error_description") } /// 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 @@ -1284,6 +1304,38 @@ internal enum L10n { } /// 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") } + /// Yes, accept all + internal static var screenKnockRequestsListAcceptAllAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_confirm_button_title") } + /// Are you sure you want to accept all requests to join? + internal static var screenKnockRequestsListAcceptAllAlertDescription: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_description") } + /// Accept all requests + internal static var screenKnockRequestsListAcceptAllAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_title") } + /// Accept all + internal static var screenKnockRequestsListAcceptAllButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_button_title") } + /// Yes, decline and ban + internal static var screenKnockRequestsListBanAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_confirm_button_title") } + /// Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again. + internal static func screenKnockRequestsListBanAlertDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_description", String(describing: p1)) + } + /// Decline and ban from accessing + internal static var screenKnockRequestsListBanAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_title") } + /// Yes, decline + internal static var screenKnockRequestsListDeclineAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_confirm_button_title") } + /// Are you sure you want to decline %1$@ request to join this room? + internal static func screenKnockRequestsListDeclineAlertDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_description", String(describing: p1)) + } + /// Decline access + internal static var screenKnockRequestsListDeclineAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_title") } + /// Decline and ban + internal static var screenKnockRequestsListDeclineAndBanActionTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_and_ban_action_title") } + /// When somebody will ask to join the room, you’ll be able to see their request here. + internal static var screenKnockRequestsListEmptyStateDescription: String { return L10n.tr("Localizable", "screen_knock_requests_list_empty_state_description") } + /// No pending request to join + internal static var screenKnockRequestsListEmptyStateTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_empty_state_title") } + /// Requests to join + internal static var screenKnockRequestsListTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_title") } /// This account has been deactivated. internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } /// Incorrect username and/or password @@ -1306,6 +1358,8 @@ internal enum L10n { } /// Failed selecting media, please try again. internal static var screenMediaPickerErrorFailedSelection: String { return L10n.tr("Localizable", "screen_media_picker_error_failed_selection") } + /// Captions might not be visible to people using older apps. + internal static var screenMediaUploadPreviewCaptionWarning: String { return L10n.tr("Localizable", "screen_media_upload_preview_caption_warning") } /// Failed processing media to upload, please try again. internal static var screenMediaUploadPreviewErrorFailedProcessing: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_processing") } /// Failed uploading media, please try again. @@ -1732,6 +1786,8 @@ internal enum L10n { internal static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") } /// Pinned messages internal static var screenRoomDetailsPinnedEventsRowTitle: String { return L10n.tr("Localizable", "screen_room_details_pinned_events_row_title") } + /// Requests to join + internal static var screenRoomDetailsRequestsToJoinTitle: String { return L10n.tr("Localizable", "screen_room_details_requests_to_join_title") } /// Roles and permissions internal static var screenRoomDetailsRolesAndPermissions: String { return L10n.tr("Localizable", "screen_room_details_roles_and_permissions") } /// Room name @@ -1840,6 +1896,12 @@ internal enum L10n { internal static var screenRoomMentionsAtRoomSubtitle: String { return L10n.tr("Localizable", "screen_room_mentions_at_room_subtitle") } /// Everyone internal static var screenRoomMentionsAtRoomTitle: String { return L10n.tr("Localizable", "screen_room_mentions_at_room_title") } + /// Plural format key: "%#@COUNT@" + internal static func screenRoomMultipleKnockRequestsTitle(_ p1: Int) -> String { + return L10n.tr("Localizable", "screen_room_multiple_knock_requests_title", p1) + } + /// View all + internal static var screenRoomMultipleKnockRequestsViewAllButtonTitle: String { return L10n.tr("Localizable", "screen_room_multiple_knock_requests_view_all_button_title") } /// Allow custom setting internal static var screenRoomNotificationSettingsAllowCustom: String { return L10n.tr("Localizable", "screen_room_notification_settings_allow_custom") } /// Turning this on will override your default setting @@ -1914,6 +1976,14 @@ internal enum L10n { internal static var screenRoomRolesAndPermissionsRoomDetails: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_room_details") } /// Roles and permissions internal static var screenRoomRolesAndPermissionsTitle: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_title") } + /// Accept + internal static var screenRoomSingleKnockRequestAcceptButtonTitle: String { return L10n.tr("Localizable", "screen_room_single_knock_request_accept_button_title") } + /// %1$@ wants to join this room + internal static func screenRoomSingleKnockRequestTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_room_single_knock_request_title", String(describing: p1)) + } + /// View + internal static var screenRoomSingleKnockRequestViewButtonTitle: String { return L10n.tr("Localizable", "screen_room_single_knock_request_view_button_title") } /// Add emoji internal static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") } /// This is the beginning of %1$@. @@ -1922,6 +1992,8 @@ internal enum L10n { } /// This is the beginning of this conversation. internal static var screenRoomTimelineBeginningOfRoomNoName: String { return L10n.tr("Localizable", "screen_room_timeline_beginning_of_room_no_name") } + /// Unsupported call. Ask if the caller can use the new Element X app. + internal static var screenRoomTimelineLegacyCall: String { return L10n.tr("Localizable", "screen_room_timeline_legacy_call") } /// Show less internal static var screenRoomTimelineLessReactions: String { return L10n.tr("Localizable", "screen_room_timeline_less_reactions") } /// Message copied @@ -2078,6 +2150,14 @@ internal enum L10n { internal static var screenSessionVerificationTheyDontMatch: String { return L10n.tr("Localizable", "screen_session_verification_they_dont_match") } /// They match internal static var screenSessionVerificationTheyMatch: String { return L10n.tr("Localizable", "screen_session_verification_they_match") } + /// Make sure you have the app open in the other device before starting verification from here. + internal static var screenSessionVerificationUseAnotherDeviceSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_use_another_device_subtitle") } + /// Open the app on another verified device + internal static var screenSessionVerificationUseAnotherDeviceTitle: String { return L10n.tr("Localizable", "screen_session_verification_use_another_device_title") } + /// You should see a popup on the other device. Start the verification from there now. + internal static var screenSessionVerificationWaitingAnotherDeviceSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_another_device_subtitle") } + /// Start verification on the other device + internal static var screenSessionVerificationWaitingAnotherDeviceTitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_another_device_title") } /// Accept the request to start the verification process in your other session to continue. internal static var screenSessionVerificationWaitingToAcceptSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_subtitle") } /// Waiting to accept request @@ -2274,11 +2354,11 @@ internal enum L10n { } /// You joined the room internal static var stateEventRoomJoinByYou: String { return L10n.tr("Localizable", "state_event_room_join_by_you") } - /// %1$@ requested to join + /// %1$@ is requesting to join internal static func stateEventRoomKnock(_ p1: Any) -> String { return L10n.tr("Localizable", "state_event_room_knock", String(describing: p1)) } - /// %1$@ allowed %2$@ to join + /// %1$@ granted access to %2$@ internal static func stateEventRoomKnockAccepted(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "state_event_room_knock_accepted", String(describing: p1), String(describing: p2)) } diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index b5b52018bd..9a21f91205 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -9,6 +9,8 @@ import Combine import Foundation struct ClientProxyMockConfiguration { + var homeserver = "" + var userIDServerName: String? var userID: String = RoomMemberProxyMock.mockMe.userID var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) @@ -28,7 +30,8 @@ extension ClientProxyMock { userID = configuration.userID deviceID = configuration.deviceID - homeserver = "" + homeserver = configuration.homeserver + userIDServerName = configuration.userIDServerName roomSummaryProvider = configuration.roomSummaryProvider alternateRoomSummaryProvider = RoomSummaryProviderMock(.init()) @@ -52,13 +55,14 @@ extension ClientProxyMock { canDeactivateAccount = false directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) loadUserAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) setUserAvatarMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) removeUserAvatarReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + isAliasAvailableReturnValue = .success(true) logoutReturnValue = nil searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false)) profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user")) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index c7524379ca..576eff68fd 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1200,12 +1200,7 @@ class AudioConverterMock: AudioConverterProtocol { } } class AudioPlayerMock: AudioPlayerProtocol { - var actions: AnyPublisher { - get { return underlyingActions } - set(value) { underlyingActions = value } - } - var underlyingActions: AnyPublisher! - var mediaSource: MediaSourceProxy? + var sourceURL: URL? var duration: TimeInterval { get { return underlyingDuration } set(value) { underlyingDuration = value } @@ -1216,24 +1211,29 @@ class AudioPlayerMock: AudioPlayerProtocol { set(value) { underlyingCurrentTime = value } } var underlyingCurrentTime: TimeInterval! - var url: URL? + var playbackURL: URL? var state: MediaPlayerState { get { return underlyingState } set(value) { underlyingState = value } } var underlyingState: MediaPlayerState! + var actions: AnyPublisher { + get { return underlyingActions } + set(value) { underlyingActions = value } + } + var underlyingActions: AnyPublisher! //MARK: - load - var loadMediaSourceUsingAutoplayUnderlyingCallsCount = 0 - var loadMediaSourceUsingAutoplayCallsCount: Int { + var loadSourceURLPlaybackURLAutoplayUnderlyingCallsCount = 0 + var loadSourceURLPlaybackURLAutoplayCallsCount: Int { get { if Thread.isMainThread { - return loadMediaSourceUsingAutoplayUnderlyingCallsCount + return loadSourceURLPlaybackURLAutoplayUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = loadMediaSourceUsingAutoplayUnderlyingCallsCount + returnValue = loadSourceURLPlaybackURLAutoplayUnderlyingCallsCount } return returnValue! @@ -1241,28 +1241,28 @@ class AudioPlayerMock: AudioPlayerProtocol { } set { if Thread.isMainThread { - loadMediaSourceUsingAutoplayUnderlyingCallsCount = newValue + loadSourceURLPlaybackURLAutoplayUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - loadMediaSourceUsingAutoplayUnderlyingCallsCount = newValue + loadSourceURLPlaybackURLAutoplayUnderlyingCallsCount = newValue } } } } - var loadMediaSourceUsingAutoplayCalled: Bool { - return loadMediaSourceUsingAutoplayCallsCount > 0 + var loadSourceURLPlaybackURLAutoplayCalled: Bool { + return loadSourceURLPlaybackURLAutoplayCallsCount > 0 } - var loadMediaSourceUsingAutoplayReceivedArguments: (mediaSource: MediaSourceProxy, url: URL, autoplay: Bool)? - var loadMediaSourceUsingAutoplayReceivedInvocations: [(mediaSource: MediaSourceProxy, url: URL, autoplay: Bool)] = [] - var loadMediaSourceUsingAutoplayClosure: ((MediaSourceProxy, URL, Bool) -> Void)? + var loadSourceURLPlaybackURLAutoplayReceivedArguments: (sourceURL: URL, playbackURL: URL, autoplay: Bool)? + var loadSourceURLPlaybackURLAutoplayReceivedInvocations: [(sourceURL: URL, playbackURL: URL, autoplay: Bool)] = [] + var loadSourceURLPlaybackURLAutoplayClosure: ((URL, URL, Bool) -> Void)? - func load(mediaSource: MediaSourceProxy, using url: URL, autoplay: Bool) { - loadMediaSourceUsingAutoplayCallsCount += 1 - loadMediaSourceUsingAutoplayReceivedArguments = (mediaSource: mediaSource, url: url, autoplay: autoplay) + func load(sourceURL: URL, playbackURL: URL, autoplay: Bool) { + loadSourceURLPlaybackURLAutoplayCallsCount += 1 + loadSourceURLPlaybackURLAutoplayReceivedArguments = (sourceURL: sourceURL, playbackURL: playbackURL, autoplay: autoplay) DispatchQueue.main.async { - self.loadMediaSourceUsingAutoplayReceivedInvocations.append((mediaSource: mediaSource, url: url, autoplay: autoplay)) + self.loadSourceURLPlaybackURLAutoplayReceivedInvocations.append((sourceURL: sourceURL, playbackURL: playbackURL, autoplay: autoplay)) } - loadMediaSourceUsingAutoplayClosure?(mediaSource, url, autoplay) + loadSourceURLPlaybackURLAutoplayClosure?(sourceURL, playbackURL, autoplay) } //MARK: - reset @@ -2346,6 +2346,41 @@ class ClientProxyMock: ClientProxyProtocol { stopSyncCallsCount += 1 stopSyncClosure?() } + //MARK: - stopSync + + var stopSyncCompletionUnderlyingCallsCount = 0 + var stopSyncCompletionCallsCount: Int { + get { + if Thread.isMainThread { + return stopSyncCompletionUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = stopSyncCompletionUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + stopSyncCompletionUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + stopSyncCompletionUnderlyingCallsCount = newValue + } + } + } + } + var stopSyncCompletionCalled: Bool { + return stopSyncCompletionCallsCount > 0 + } + var stopSyncCompletionClosure: (((() -> Void)?) -> Void)? + + func stopSync(completion: (() -> Void)?) { + stopSyncCompletionCallsCount += 1 + stopSyncCompletionClosure?(completion) + } //MARK: - accountURL var accountURLActionUnderlyingCallsCount = 0 @@ -2628,15 +2663,15 @@ class ClientProxyMock: ClientProxyProtocol { } //MARK: - createRoom - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = 0 - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount: Int { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount: Int { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount } return returnValue! @@ -2644,29 +2679,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCalled: Bool { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount > 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled: Bool { + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount > 0 } - 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 createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)] = [] - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue: Result! - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result! { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue: Result! + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue: Result! { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue } return returnValue! @@ -2674,26 +2709,26 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure: ((String, String?, Bool, Bool, [String], URL?) async -> Result)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure: ((String, String?, Bool, Bool, [String], URL?, String?) async -> Result)? - 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) + func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?) async -> Result { + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount += 1 + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart) DispatchQueue.main.async { - self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL)) + self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart)) } - if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure { - return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL) + if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure { + return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL, aliasLocalPart) } else { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue } } //MARK: - joinRoom @@ -3973,6 +4008,76 @@ class ClientProxyMock: ClientProxyProtocol { return resolveRoomAliasReturnValue } } + //MARK: - isAliasAvailable + + var isAliasAvailableUnderlyingCallsCount = 0 + var isAliasAvailableCallsCount: Int { + get { + if Thread.isMainThread { + return isAliasAvailableUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = isAliasAvailableUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isAliasAvailableUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + isAliasAvailableUnderlyingCallsCount = newValue + } + } + } + } + var isAliasAvailableCalled: Bool { + return isAliasAvailableCallsCount > 0 + } + var isAliasAvailableReceivedAlias: String? + var isAliasAvailableReceivedInvocations: [String] = [] + + var isAliasAvailableUnderlyingReturnValue: Result! + var isAliasAvailableReturnValue: Result! { + get { + if Thread.isMainThread { + return isAliasAvailableUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = isAliasAvailableUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isAliasAvailableUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + isAliasAvailableUnderlyingReturnValue = newValue + } + } + } + } + var isAliasAvailableClosure: ((String) async -> Result)? + + func isAliasAvailable(_ alias: String) async -> Result { + isAliasAvailableCallsCount += 1 + isAliasAvailableReceivedAlias = alias + DispatchQueue.main.async { + self.isAliasAvailableReceivedInvocations.append(alias) + } + if let isAliasAvailableClosure = isAliasAvailableClosure { + return await isAliasAvailableClosure(alias) + } else { + return isAliasAvailableReturnValue + } + } //MARK: - getElementWellKnown var getElementWellKnownUnderlyingCallsCount = 0 @@ -5761,11 +5866,12 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { } } class InvitedRoomProxyMock: InvitedRoomProxyProtocol { - var info: RoomInfoProxy { + var info: BaseRoomInfoProxyProtocol { get { return underlyingInfo } set(value) { underlyingInfo = value } } - var underlyingInfo: RoomInfoProxy! + var underlyingInfo: BaseRoomInfoProxyProtocol! + var inviter: RoomMemberProxyProtocol? var id: String { get { return underlyingId } set(value) { underlyingId = value } @@ -5841,70 +5947,6 @@ class InvitedRoomProxyMock: InvitedRoomProxyProtocol { return rejectInvitationReturnValue } } - //MARK: - acceptInvitation - - var acceptInvitationUnderlyingCallsCount = 0 - var acceptInvitationCallsCount: Int { - get { - if Thread.isMainThread { - return acceptInvitationUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = acceptInvitationUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - acceptInvitationUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - acceptInvitationUnderlyingCallsCount = newValue - } - } - } - } - var acceptInvitationCalled: Bool { - return acceptInvitationCallsCount > 0 - } - - var acceptInvitationUnderlyingReturnValue: Result! - var acceptInvitationReturnValue: Result! { - get { - if Thread.isMainThread { - return acceptInvitationUnderlyingReturnValue - } else { - var returnValue: Result? = nil - DispatchQueue.main.sync { - returnValue = acceptInvitationUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - acceptInvitationUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - acceptInvitationUnderlyingReturnValue = newValue - } - } - } - } - var acceptInvitationClosure: (() async -> Result)? - - func acceptInvitation() async -> Result { - acceptInvitationCallsCount += 1 - if let acceptInvitationClosure = acceptInvitationClosure { - return await acceptInvitationClosure() - } else { - return acceptInvitationReturnValue - } - } } class JoinedRoomProxyMock: JoinedRoomProxyProtocol { var isEncrypted: Bool { @@ -6969,87 +7011,17 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { return sendTypingNotificationIsTypingReturnValue } } - //MARK: - resend - - var resendItemIDUnderlyingCallsCount = 0 - var resendItemIDCallsCount: Int { - get { - if Thread.isMainThread { - return resendItemIDUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = resendItemIDUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - resendItemIDUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - resendItemIDUnderlyingCallsCount = newValue - } - } - } - } - var resendItemIDCalled: Bool { - return resendItemIDCallsCount > 0 - } - var resendItemIDReceivedItemID: TimelineItemIdentifier? - var resendItemIDReceivedInvocations: [TimelineItemIdentifier] = [] - - var resendItemIDUnderlyingReturnValue: Result! - var resendItemIDReturnValue: Result! { - get { - if Thread.isMainThread { - return resendItemIDUnderlyingReturnValue - } else { - var returnValue: Result? = nil - DispatchQueue.main.sync { - returnValue = resendItemIDUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - resendItemIDUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - resendItemIDUnderlyingReturnValue = newValue - } - } - } - } - var resendItemIDClosure: ((TimelineItemIdentifier) async -> Result)? - - func resend(itemID: TimelineItemIdentifier) async -> Result { - resendItemIDCallsCount += 1 - resendItemIDReceivedItemID = itemID - DispatchQueue.main.async { - self.resendItemIDReceivedInvocations.append(itemID) - } - if let resendItemIDClosure = resendItemIDClosure { - return await resendItemIDClosure(itemID) - } else { - return resendItemIDReturnValue - } - } //MARK: - ignoreDeviceTrustAndResend - var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = 0 - var ignoreDeviceTrustAndResendDevicesItemIDCallsCount: Int { + var ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = 0 + var ignoreDeviceTrustAndResendDevicesSendHandleCallsCount: Int { get { if Thread.isMainThread { - return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount + return ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount + returnValue = ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount } return returnValue! @@ -7057,29 +7029,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { } set { if Thread.isMainThread { - ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = newValue } } } } - var ignoreDeviceTrustAndResendDevicesItemIDCalled: Bool { - return ignoreDeviceTrustAndResendDevicesItemIDCallsCount > 0 + var ignoreDeviceTrustAndResendDevicesSendHandleCalled: Bool { + return ignoreDeviceTrustAndResendDevicesSendHandleCallsCount > 0 } - var ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments: (devices: [String: [String]], itemID: TimelineItemIdentifier)? - var ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations: [(devices: [String: [String]], itemID: TimelineItemIdentifier)] = [] + var ignoreDeviceTrustAndResendDevicesSendHandleReceivedArguments: (devices: [String: [String]], sendHandle: SendHandleProxy)? + var ignoreDeviceTrustAndResendDevicesSendHandleReceivedInvocations: [(devices: [String: [String]], sendHandle: SendHandleProxy)] = [] - var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue: Result! - var ignoreDeviceTrustAndResendDevicesItemIDReturnValue: Result! { + var ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingReturnValue: Result! + var ignoreDeviceTrustAndResendDevicesSendHandleReturnValue: Result! { get { if Thread.isMainThread { - return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue + return ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue + returnValue = ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingReturnValue } return returnValue! @@ -7087,39 +7059,39 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { } set { if Thread.isMainThread { - ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingReturnValue = newValue } } } } - var ignoreDeviceTrustAndResendDevicesItemIDClosure: (([String: [String]], TimelineItemIdentifier) async -> Result)? + var ignoreDeviceTrustAndResendDevicesSendHandleClosure: (([String: [String]], SendHandleProxy) async -> Result)? - func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result { - ignoreDeviceTrustAndResendDevicesItemIDCallsCount += 1 - ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments = (devices: devices, itemID: itemID) + func ignoreDeviceTrustAndResend(devices: [String: [String]], sendHandle: SendHandleProxy) async -> Result { + ignoreDeviceTrustAndResendDevicesSendHandleCallsCount += 1 + ignoreDeviceTrustAndResendDevicesSendHandleReceivedArguments = (devices: devices, sendHandle: sendHandle) DispatchQueue.main.async { - self.ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations.append((devices: devices, itemID: itemID)) + self.ignoreDeviceTrustAndResendDevicesSendHandleReceivedInvocations.append((devices: devices, sendHandle: sendHandle)) } - if let ignoreDeviceTrustAndResendDevicesItemIDClosure = ignoreDeviceTrustAndResendDevicesItemIDClosure { - return await ignoreDeviceTrustAndResendDevicesItemIDClosure(devices, itemID) + if let ignoreDeviceTrustAndResendDevicesSendHandleClosure = ignoreDeviceTrustAndResendDevicesSendHandleClosure { + return await ignoreDeviceTrustAndResendDevicesSendHandleClosure(devices, sendHandle) } else { - return ignoreDeviceTrustAndResendDevicesItemIDReturnValue + return ignoreDeviceTrustAndResendDevicesSendHandleReturnValue } } //MARK: - withdrawVerificationAndResend - var withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = 0 - var withdrawVerificationAndResendUserIDsItemIDCallsCount: Int { + var withdrawVerificationAndResendUserIDsSendHandleUnderlyingCallsCount = 0 + var withdrawVerificationAndResendUserIDsSendHandleCallsCount: Int { get { if Thread.isMainThread { - return withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount + return withdrawVerificationAndResendUserIDsSendHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount + returnValue = withdrawVerificationAndResendUserIDsSendHandleUnderlyingCallsCount } return returnValue! @@ -7127,29 +7099,29 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { } set { if Thread.isMainThread { - withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue + withdrawVerificationAndResendUserIDsSendHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue + withdrawVerificationAndResendUserIDsSendHandleUnderlyingCallsCount = newValue } } } } - var withdrawVerificationAndResendUserIDsItemIDCalled: Bool { - return withdrawVerificationAndResendUserIDsItemIDCallsCount > 0 + var withdrawVerificationAndResendUserIDsSendHandleCalled: Bool { + return withdrawVerificationAndResendUserIDsSendHandleCallsCount > 0 } - var withdrawVerificationAndResendUserIDsItemIDReceivedArguments: (userIDs: [String], itemID: TimelineItemIdentifier)? - var withdrawVerificationAndResendUserIDsItemIDReceivedInvocations: [(userIDs: [String], itemID: TimelineItemIdentifier)] = [] + var withdrawVerificationAndResendUserIDsSendHandleReceivedArguments: (userIDs: [String], sendHandle: SendHandleProxy)? + var withdrawVerificationAndResendUserIDsSendHandleReceivedInvocations: [(userIDs: [String], sendHandle: SendHandleProxy)] = [] - var withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue: Result! - var withdrawVerificationAndResendUserIDsItemIDReturnValue: Result! { + var withdrawVerificationAndResendUserIDsSendHandleUnderlyingReturnValue: Result! + var withdrawVerificationAndResendUserIDsSendHandleReturnValue: Result! { get { if Thread.isMainThread { - return withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue + return withdrawVerificationAndResendUserIDsSendHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue + returnValue = withdrawVerificationAndResendUserIDsSendHandleUnderlyingReturnValue } return returnValue! @@ -7157,26 +7129,26 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { } set { if Thread.isMainThread { - withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue + withdrawVerificationAndResendUserIDsSendHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue + withdrawVerificationAndResendUserIDsSendHandleUnderlyingReturnValue = newValue } } } } - var withdrawVerificationAndResendUserIDsItemIDClosure: (([String], TimelineItemIdentifier) async -> Result)? + var withdrawVerificationAndResendUserIDsSendHandleClosure: (([String], SendHandleProxy) async -> Result)? - func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result { - withdrawVerificationAndResendUserIDsItemIDCallsCount += 1 - withdrawVerificationAndResendUserIDsItemIDReceivedArguments = (userIDs: userIDs, itemID: itemID) + func withdrawVerificationAndResend(userIDs: [String], sendHandle: SendHandleProxy) async -> Result { + withdrawVerificationAndResendUserIDsSendHandleCallsCount += 1 + withdrawVerificationAndResendUserIDsSendHandleReceivedArguments = (userIDs: userIDs, sendHandle: sendHandle) DispatchQueue.main.async { - self.withdrawVerificationAndResendUserIDsItemIDReceivedInvocations.append((userIDs: userIDs, itemID: itemID)) + self.withdrawVerificationAndResendUserIDsSendHandleReceivedInvocations.append((userIDs: userIDs, sendHandle: sendHandle)) } - if let withdrawVerificationAndResendUserIDsItemIDClosure = withdrawVerificationAndResendUserIDsItemIDClosure { - return await withdrawVerificationAndResendUserIDsItemIDClosure(userIDs, itemID) + if let withdrawVerificationAndResendUserIDsSendHandleClosure = withdrawVerificationAndResendUserIDsSendHandleClosure { + return await withdrawVerificationAndResendUserIDsSendHandleClosure(userIDs, sendHandle) } else { - return withdrawVerificationAndResendUserIDsItemIDReturnValue + return withdrawVerificationAndResendUserIDsSendHandleReturnValue } } //MARK: - flagAsUnread @@ -9569,11 +9541,11 @@ class KeychainControllerMock: KeychainControllerProtocol { } } class KnockedRoomProxyMock: KnockedRoomProxyProtocol { - var info: RoomInfoProxy { + var info: BaseRoomInfoProxyProtocol { get { return underlyingInfo } set(value) { underlyingInfo = value } } - var underlyingInfo: RoomInfoProxy! + var underlyingInfo: BaseRoomInfoProxyProtocol! var id: String { get { return underlyingId } set(value) { underlyingId = value } @@ -9875,320 +9847,13 @@ class MediaLoaderMock: MediaLoaderProtocol { } } } -class MediaPlayerMock: MediaPlayerProtocol { - var mediaSource: MediaSourceProxy? - var duration: TimeInterval { - get { return underlyingDuration } - set(value) { underlyingDuration = value } - } - var underlyingDuration: TimeInterval! - var currentTime: TimeInterval { - get { return underlyingCurrentTime } - set(value) { underlyingCurrentTime = value } - } - var underlyingCurrentTime: TimeInterval! - var url: URL? - var state: MediaPlayerState { - get { return underlyingState } - set(value) { underlyingState = value } - } - var underlyingState: MediaPlayerState! - - //MARK: - load - - var loadMediaSourceUsingAutoplayUnderlyingCallsCount = 0 - var loadMediaSourceUsingAutoplayCallsCount: Int { - get { - if Thread.isMainThread { - return loadMediaSourceUsingAutoplayUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = loadMediaSourceUsingAutoplayUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - loadMediaSourceUsingAutoplayUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - loadMediaSourceUsingAutoplayUnderlyingCallsCount = newValue - } - } - } - } - var loadMediaSourceUsingAutoplayCalled: Bool { - return loadMediaSourceUsingAutoplayCallsCount > 0 - } - var loadMediaSourceUsingAutoplayReceivedArguments: (mediaSource: MediaSourceProxy, url: URL, autoplay: Bool)? - var loadMediaSourceUsingAutoplayReceivedInvocations: [(mediaSource: MediaSourceProxy, url: URL, autoplay: Bool)] = [] - var loadMediaSourceUsingAutoplayClosure: ((MediaSourceProxy, URL, Bool) -> Void)? - - func load(mediaSource: MediaSourceProxy, using url: URL, autoplay: Bool) { - loadMediaSourceUsingAutoplayCallsCount += 1 - loadMediaSourceUsingAutoplayReceivedArguments = (mediaSource: mediaSource, url: url, autoplay: autoplay) - DispatchQueue.main.async { - self.loadMediaSourceUsingAutoplayReceivedInvocations.append((mediaSource: mediaSource, url: url, autoplay: autoplay)) - } - loadMediaSourceUsingAutoplayClosure?(mediaSource, url, autoplay) - } - //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: - play - - var playUnderlyingCallsCount = 0 - var playCallsCount: Int { - get { - if Thread.isMainThread { - return playUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = playUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - playUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - playUnderlyingCallsCount = newValue - } - } - } - } - var playCalled: Bool { - return playCallsCount > 0 - } - var playClosure: (() -> Void)? - - func play() { - playCallsCount += 1 - playClosure?() - } - //MARK: - pause - - var pauseUnderlyingCallsCount = 0 - var pauseCallsCount: Int { - get { - if Thread.isMainThread { - return pauseUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = pauseUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - pauseUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - pauseUnderlyingCallsCount = newValue - } - } - } - } - var pauseCalled: Bool { - return pauseCallsCount > 0 - } - var pauseClosure: (() -> Void)? - - func pause() { - pauseCallsCount += 1 - pauseClosure?() - } - //MARK: - stop - - var stopUnderlyingCallsCount = 0 - var stopCallsCount: Int { - get { - if Thread.isMainThread { - return stopUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = stopUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - stopUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - stopUnderlyingCallsCount = newValue - } - } - } - } - var stopCalled: Bool { - return stopCallsCount > 0 - } - var stopClosure: (() -> Void)? - - func stop() { - stopCallsCount += 1 - stopClosure?() - } - //MARK: - seek - - var seekToUnderlyingCallsCount = 0 - var seekToCallsCount: Int { - get { - if Thread.isMainThread { - return seekToUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = seekToUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - seekToUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - seekToUnderlyingCallsCount = newValue - } - } - } - } - var seekToCalled: Bool { - return seekToCallsCount > 0 - } - var seekToReceivedProgress: Double? - var seekToReceivedInvocations: [Double] = [] - var seekToClosure: ((Double) async -> Void)? - - func seek(to progress: Double) async { - seekToCallsCount += 1 - seekToReceivedProgress = progress - DispatchQueue.main.async { - self.seekToReceivedInvocations.append(progress) - } - await seekToClosure?(progress) - } -} class MediaPlayerProviderMock: MediaPlayerProviderProtocol { - - //MARK: - player - - var playerForUnderlyingCallsCount = 0 - var playerForCallsCount: Int { - get { - if Thread.isMainThread { - return playerForUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = playerForUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - playerForUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - playerForUnderlyingCallsCount = newValue - } - } - } - } - var playerForCalled: Bool { - return playerForCallsCount > 0 + var player: AudioPlayerProtocol { + get { return underlyingPlayer } + set(value) { underlyingPlayer = value } } - var playerForReceivedMediaSource: MediaSourceProxy? - var playerForReceivedInvocations: [MediaSourceProxy] = [] - - var playerForUnderlyingReturnValue: Result! - var playerForReturnValue: Result! { - get { - if Thread.isMainThread { - return playerForUnderlyingReturnValue - } else { - var returnValue: Result? = nil - DispatchQueue.main.sync { - returnValue = playerForUnderlyingReturnValue - } + var underlyingPlayer: AudioPlayerProtocol! - 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 @@ -12869,15 +12534,15 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { //MARK: - buildRoomTimelineController - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount: Int { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount } return returnValue! @@ -12885,29 +12550,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCalled: Bool { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount > 0 } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol! + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol! { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue } return returnValue! @@ -12915,39 +12580,39 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((JoinedRoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) -> RoomTimelineControllerProtocol)? - func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) + func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount += 1 + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) DispatchQueue.main.async { - self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) + self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)) } - if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, initialFocussedEventID, timelineItemFactory) + if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure(roomProxy, initialFocussedEventID, timelineItemFactory, mediaProvider) } else { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue } } //MARK: - buildRoomPinnedTimelineController - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount: Int { + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int { get { if Thread.isMainThread { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount } return returnValue! @@ -12955,29 +12620,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } } } } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCalled: Bool { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount > 0 + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCalled: Bool { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0 } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol? - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol? { + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol? { get { if Thread.isMainThread { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol?? = nil DispatchQueue.main.sync { - returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } return returnValue! @@ -12985,26 +12650,26 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } } } } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol?)? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> RoomTimelineControllerProtocol?)? - func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount += 1 - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) + func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1 + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) DispatchQueue.main.async { - self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory)) + self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)) } - if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure { - return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure(roomProxy, timelineItemFactory) + if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure { + return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure(roomProxy, timelineItemFactory, mediaProvider) } else { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue } } } @@ -14196,8 +13861,8 @@ class TimelineProxyMock: TimelineProxyProtocol { var editNewContentCalled: Bool { return editNewContentCallsCount > 0 } - var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)? - var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)] = [] + var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: EditedContent)? + var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: EditedContent)] = [] var editNewContentUnderlyingReturnValue: Result! var editNewContentReturnValue: Result! { @@ -14223,9 +13888,9 @@ class TimelineProxyMock: TimelineProxyProtocol { } } } - var editNewContentClosure: ((EventOrTransactionId, RoomMessageEventContentWithoutRelation) async -> Result)? + var editNewContentClosure: ((EventOrTransactionId, EditedContent) async -> Result)? - func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: EditedContent) async -> Result { editNewContentCallsCount += 1 editNewContentReceivedArguments = (eventOrTransactionID: eventOrTransactionID, newContent: newContent) DispatchQueue.main.async { @@ -14449,15 +14114,15 @@ class TimelineProxyMock: TimelineProxyProtocol { } //MARK: - sendAudio - var sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingCallsCount = 0 - var sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount: Int { + var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = 0 + var sendAudioUrlAudioInfoCaptionRequestHandleCallsCount: Int { get { if Thread.isMainThread { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingCallsCount + return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingCallsCount + returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount } return returnValue! @@ -14465,27 +14130,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingCallsCount = newValue } } } } - var sendAudioUrlAudioInfoProgressSubjectRequestHandleCalled: Bool { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount > 0 + var sendAudioUrlAudioInfoCaptionRequestHandleCalled: Bool { + return sendAudioUrlAudioInfoCaptionRequestHandleCallsCount > 0 } - var sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingReturnValue: Result! - var sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue: Result! { + var sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue: Result! + var sendAudioUrlAudioInfoCaptionRequestHandleReturnValue: Result! { get { if Thread.isMainThread { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingReturnValue + return sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingReturnValue + returnValue = sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue } return returnValue! @@ -14493,35 +14158,35 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendAudioUrlAudioInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendAudioUrlAudioInfoCaptionRequestHandleUnderlyingReturnValue = newValue } } } } - var sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure: ((URL, AudioInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + var sendAudioUrlAudioInfoCaptionRequestHandleClosure: ((URL, AudioInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - func sendAudio(url: URL, audioInfo: AudioInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure = sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure { - return await sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure(url, audioInfo, progressSubject, requestHandle) + func sendAudio(url: URL, audioInfo: AudioInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendAudioUrlAudioInfoCaptionRequestHandleCallsCount += 1 + if let sendAudioUrlAudioInfoCaptionRequestHandleClosure = sendAudioUrlAudioInfoCaptionRequestHandleClosure { + return await sendAudioUrlAudioInfoCaptionRequestHandleClosure(url, audioInfo, caption, requestHandle) } else { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue + return sendAudioUrlAudioInfoCaptionRequestHandleReturnValue } } //MARK: - sendFile - var sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingCallsCount = 0 - var sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount: Int { + var sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = 0 + var sendFileUrlFileInfoCaptionRequestHandleCallsCount: Int { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingCallsCount + return sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingCallsCount + returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount } return returnValue! @@ -14529,27 +14194,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendFileUrlFileInfoCaptionRequestHandleUnderlyingCallsCount = newValue } } } } - var sendFileUrlFileInfoProgressSubjectRequestHandleCalled: Bool { - return sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount > 0 + var sendFileUrlFileInfoCaptionRequestHandleCalled: Bool { + return sendFileUrlFileInfoCaptionRequestHandleCallsCount > 0 } - var sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingReturnValue: Result! - var sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue: Result! { + var sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue: Result! + var sendFileUrlFileInfoCaptionRequestHandleReturnValue: Result! { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingReturnValue + return sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingReturnValue + returnValue = sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue } return returnValue! @@ -14557,35 +14222,35 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendFileUrlFileInfoCaptionRequestHandleUnderlyingReturnValue = newValue } } } } - var sendFileUrlFileInfoProgressSubjectRequestHandleClosure: ((URL, FileInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + var sendFileUrlFileInfoCaptionRequestHandleClosure: ((URL, FileInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - func sendFile(url: URL, fileInfo: FileInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendFileUrlFileInfoProgressSubjectRequestHandleClosure = sendFileUrlFileInfoProgressSubjectRequestHandleClosure { - return await sendFileUrlFileInfoProgressSubjectRequestHandleClosure(url, fileInfo, progressSubject, requestHandle) + func sendFile(url: URL, fileInfo: FileInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendFileUrlFileInfoCaptionRequestHandleCallsCount += 1 + if let sendFileUrlFileInfoCaptionRequestHandleClosure = sendFileUrlFileInfoCaptionRequestHandleClosure { + return await sendFileUrlFileInfoCaptionRequestHandleClosure(url, fileInfo, caption, requestHandle) } else { - return sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue + return sendFileUrlFileInfoCaptionRequestHandleReturnValue } } //MARK: - sendImage - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingCallsCount = 0 - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount: Int { + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = 0 + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount: Int { get { if Thread.isMainThread { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingCallsCount + return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingCallsCount + returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount } return returnValue! @@ -14593,27 +14258,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingCallsCount = newValue } } } } - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCalled: Bool { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount > 0 + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCalled: Bool { + return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount > 0 } - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingReturnValue: Result! - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue: Result! { + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue: Result! + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue: Result! { get { if Thread.isMainThread { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingReturnValue + return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingReturnValue + returnValue = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue } return returnValue! @@ -14621,22 +14286,22 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendImageUrlThumbnailURLImageInfoCaptionRequestHandleUnderlyingReturnValue = newValue } } } } - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure: ((URL, URL, ImageInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + var sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure: ((URL, URL, ImageInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure = sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure { - return await sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, imageInfo, progressSubject, requestHandle) + func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendImageUrlThumbnailURLImageInfoCaptionRequestHandleCallsCount += 1 + if let sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure { + return await sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure(url, thumbnailURL, imageInfo, caption, requestHandle) } else { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue + return sendImageUrlThumbnailURLImageInfoCaptionRequestHandleReturnValue } } //MARK: - sendLocation @@ -14711,15 +14376,15 @@ class TimelineProxyMock: TimelineProxyProtocol { } //MARK: - sendVideo - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingCallsCount = 0 - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount: Int { + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = 0 + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount: Int { get { if Thread.isMainThread { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingCallsCount + return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingCallsCount + returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount } return returnValue! @@ -14727,27 +14392,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingCallsCount = newValue } } } } - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCalled: Bool { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount > 0 + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCalled: Bool { + return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount > 0 } - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingReturnValue: Result! - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue: Result! { + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue: Result! + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue: Result! { get { if Thread.isMainThread { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingReturnValue + return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingReturnValue + returnValue = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue } return returnValue! @@ -14755,35 +14420,35 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleUnderlyingReturnValue = newValue } } } } - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure: ((URL, URL, VideoInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + var sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure: ((URL, URL, VideoInfo, String?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure { - return await sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, videoInfo, progressSubject, requestHandle) + func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleCallsCount += 1 + if let sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure { + return await sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure(url, thumbnailURL, videoInfo, caption, requestHandle) } else { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue + return sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleReturnValue } } //MARK: - sendVoiceMessage - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingCallsCount = 0 - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount: Int { + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = 0 + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount: Int { get { if Thread.isMainThread { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingCallsCount + return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingCallsCount + returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount } return returnValue! @@ -14791,27 +14456,27 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingCallsCount = newValue + sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingCallsCount = newValue } } } } - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled: Bool { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount > 0 + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled: Bool { + return sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount > 0 } - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingReturnValue: Result! - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue: Result! { + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue: Result! + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue: Result! { get { if Thread.isMainThread { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingReturnValue + return sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingReturnValue + returnValue = sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue } return returnValue! @@ -14819,22 +14484,22 @@ class TimelineProxyMock: TimelineProxyProtocol { } set { if Thread.isMainThread { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleUnderlyingReturnValue = newValue + sendVoiceMessageUrlAudioInfoWaveformRequestHandleUnderlyingReturnValue = newValue } } } } - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure: ((URL, AudioInfo, [UInt16], CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + var sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure: ((URL, AudioInfo, [UInt16], @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount += 1 - if let sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure { - return await sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure(url, audioInfo, waveform, progressSubject, requestHandle) + func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendVoiceMessageUrlAudioInfoWaveformRequestHandleCallsCount += 1 + if let sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure { + return await sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure(url, audioInfo, waveform, requestHandle) } else { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue + return sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue } } //MARK: - sendReadReceipt diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 43a24c5c6f..9c76fc5c50 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -625,6 +625,52 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - createRoomAlias + + open var createRoomAliasRoomAliasRoomIdThrowableError: Error? + var createRoomAliasRoomAliasRoomIdUnderlyingCallsCount = 0 + open var createRoomAliasRoomAliasRoomIdCallsCount: Int { + get { + if Thread.isMainThread { + return createRoomAliasRoomAliasRoomIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = createRoomAliasRoomAliasRoomIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + createRoomAliasRoomAliasRoomIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + createRoomAliasRoomAliasRoomIdUnderlyingCallsCount = newValue + } + } + } + } + open var createRoomAliasRoomAliasRoomIdCalled: Bool { + return createRoomAliasRoomAliasRoomIdCallsCount > 0 + } + open var createRoomAliasRoomAliasRoomIdReceivedArguments: (roomAlias: String, roomId: String)? + open var createRoomAliasRoomAliasRoomIdReceivedInvocations: [(roomAlias: String, roomId: String)] = [] + open var createRoomAliasRoomAliasRoomIdClosure: ((String, String) async throws -> Void)? + + open override func createRoomAlias(roomAlias: String, roomId: String) async throws { + if let error = createRoomAliasRoomAliasRoomIdThrowableError { + throw error + } + createRoomAliasRoomAliasRoomIdCallsCount += 1 + createRoomAliasRoomAliasRoomIdReceivedArguments = (roomAlias: roomAlias, roomId: roomId) + DispatchQueue.main.async { + self.createRoomAliasRoomAliasRoomIdReceivedInvocations.append((roomAlias: roomAlias, roomId: roomId)) + } + try await createRoomAliasRoomAliasRoomIdClosure?(roomAlias, roomId) + } + //MARK: - customLoginWithJwt open var customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError: Error? @@ -4674,6 +4720,77 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } } + //MARK: - crossProcessStoreLocksHolderName + + var crossProcessStoreLocksHolderNameHolderNameUnderlyingCallsCount = 0 + open var crossProcessStoreLocksHolderNameHolderNameCallsCount: Int { + get { + if Thread.isMainThread { + return crossProcessStoreLocksHolderNameHolderNameUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = crossProcessStoreLocksHolderNameHolderNameUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + crossProcessStoreLocksHolderNameHolderNameUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + crossProcessStoreLocksHolderNameHolderNameUnderlyingCallsCount = newValue + } + } + } + } + open var crossProcessStoreLocksHolderNameHolderNameCalled: Bool { + return crossProcessStoreLocksHolderNameHolderNameCallsCount > 0 + } + open var crossProcessStoreLocksHolderNameHolderNameReceivedHolderName: String? + open var crossProcessStoreLocksHolderNameHolderNameReceivedInvocations: [String] = [] + + var crossProcessStoreLocksHolderNameHolderNameUnderlyingReturnValue: ClientBuilder! + open var crossProcessStoreLocksHolderNameHolderNameReturnValue: ClientBuilder! { + get { + if Thread.isMainThread { + return crossProcessStoreLocksHolderNameHolderNameUnderlyingReturnValue + } else { + var returnValue: ClientBuilder? = nil + DispatchQueue.main.sync { + returnValue = crossProcessStoreLocksHolderNameHolderNameUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + crossProcessStoreLocksHolderNameHolderNameUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + crossProcessStoreLocksHolderNameHolderNameUnderlyingReturnValue = newValue + } + } + } + } + open var crossProcessStoreLocksHolderNameHolderNameClosure: ((String) -> ClientBuilder)? + + open override func crossProcessStoreLocksHolderName(holderName: String) -> ClientBuilder { + crossProcessStoreLocksHolderNameHolderNameCallsCount += 1 + crossProcessStoreLocksHolderNameHolderNameReceivedHolderName = holderName + DispatchQueue.main.async { + self.crossProcessStoreLocksHolderNameHolderNameReceivedInvocations.append(holderName) + } + if let crossProcessStoreLocksHolderNameHolderNameClosure = crossProcessStoreLocksHolderNameHolderNameClosure { + return crossProcessStoreLocksHolderNameHolderNameClosure(holderName) + } else { + return crossProcessStoreLocksHolderNameHolderNameReturnValue + } + } + //MARK: - disableAutomaticTokenRefresh var disableAutomaticTokenRefreshUnderlyingCallsCount = 0 @@ -4869,17 +4986,17 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } } - //MARK: - enableCrossProcessRefreshLock + //MARK: - enableOidcRefreshLock - var enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingCallsCount = 0 - open var enableCrossProcessRefreshLockProcessIdSessionDelegateCallsCount: Int { + var enableOidcRefreshLockUnderlyingCallsCount = 0 + open var enableOidcRefreshLockCallsCount: Int { get { if Thread.isMainThread { - return enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingCallsCount + return enableOidcRefreshLockUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingCallsCount + returnValue = enableOidcRefreshLockUnderlyingCallsCount } return returnValue! @@ -4887,29 +5004,27 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } set { if Thread.isMainThread { - enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingCallsCount = newValue + enableOidcRefreshLockUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingCallsCount = newValue + enableOidcRefreshLockUnderlyingCallsCount = newValue } } } } - open var enableCrossProcessRefreshLockProcessIdSessionDelegateCalled: Bool { - return enableCrossProcessRefreshLockProcessIdSessionDelegateCallsCount > 0 + open var enableOidcRefreshLockCalled: Bool { + return enableOidcRefreshLockCallsCount > 0 } - open var enableCrossProcessRefreshLockProcessIdSessionDelegateReceivedArguments: (processId: String, sessionDelegate: ClientSessionDelegate)? - open var enableCrossProcessRefreshLockProcessIdSessionDelegateReceivedInvocations: [(processId: String, sessionDelegate: ClientSessionDelegate)] = [] - var enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingReturnValue: ClientBuilder! - open var enableCrossProcessRefreshLockProcessIdSessionDelegateReturnValue: ClientBuilder! { + var enableOidcRefreshLockUnderlyingReturnValue: ClientBuilder! + open var enableOidcRefreshLockReturnValue: ClientBuilder! { get { if Thread.isMainThread { - return enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingReturnValue + return enableOidcRefreshLockUnderlyingReturnValue } else { var returnValue: ClientBuilder? = nil DispatchQueue.main.sync { - returnValue = enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingReturnValue + returnValue = enableOidcRefreshLockUnderlyingReturnValue } return returnValue! @@ -4917,26 +5032,22 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } set { if Thread.isMainThread { - enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingReturnValue = newValue + enableOidcRefreshLockUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - enableCrossProcessRefreshLockProcessIdSessionDelegateUnderlyingReturnValue = newValue + enableOidcRefreshLockUnderlyingReturnValue = newValue } } } } - open var enableCrossProcessRefreshLockProcessIdSessionDelegateClosure: ((String, ClientSessionDelegate) -> ClientBuilder)? + open var enableOidcRefreshLockClosure: (() -> ClientBuilder)? - open override func enableCrossProcessRefreshLock(processId: String, sessionDelegate: ClientSessionDelegate) -> ClientBuilder { - enableCrossProcessRefreshLockProcessIdSessionDelegateCallsCount += 1 - enableCrossProcessRefreshLockProcessIdSessionDelegateReceivedArguments = (processId: processId, sessionDelegate: sessionDelegate) - DispatchQueue.main.async { - self.enableCrossProcessRefreshLockProcessIdSessionDelegateReceivedInvocations.append((processId: processId, sessionDelegate: sessionDelegate)) - } - if let enableCrossProcessRefreshLockProcessIdSessionDelegateClosure = enableCrossProcessRefreshLockProcessIdSessionDelegateClosure { - return enableCrossProcessRefreshLockProcessIdSessionDelegateClosure(processId, sessionDelegate) + open override func enableOidcRefreshLock() -> ClientBuilder { + enableOidcRefreshLockCallsCount += 1 + if let enableOidcRefreshLockClosure = enableOidcRefreshLockClosure { + return enableOidcRefreshLockClosure() } else { - return enableCrossProcessRefreshLockProcessIdSessionDelegateReturnValue + return enableOidcRefreshLockReturnValue } } @@ -7832,6 +7943,71 @@ open class LazyTimelineItemProviderSDKMock: MatrixRustSDK.LazyTimelineItemProvid } } + //MARK: - getSendHandle + + var getSendHandleUnderlyingCallsCount = 0 + open var getSendHandleCallsCount: Int { + get { + if Thread.isMainThread { + return getSendHandleUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = getSendHandleUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + getSendHandleUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + getSendHandleUnderlyingCallsCount = newValue + } + } + } + } + open var getSendHandleCalled: Bool { + return getSendHandleCallsCount > 0 + } + + var getSendHandleUnderlyingReturnValue: SendHandle? + open var getSendHandleReturnValue: SendHandle? { + get { + if Thread.isMainThread { + return getSendHandleUnderlyingReturnValue + } else { + var returnValue: SendHandle?? = nil + DispatchQueue.main.sync { + returnValue = getSendHandleUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + getSendHandleUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + getSendHandleUnderlyingReturnValue = newValue + } + } + } + } + open var getSendHandleClosure: (() -> SendHandle?)? + + open override func getSendHandle() -> SendHandle? { + getSendHandleCallsCount += 1 + if let getSendHandleClosure = getSendHandleClosure { + return getSendHandleClosure() + } else { + return getSendHandleReturnValue + } + } + //MARK: - getShields var getShieldsStrictUnderlyingCallsCount = 0 @@ -8072,71 +8248,6 @@ open class MediaSourceSDKMock: MatrixRustSDK.MediaSource { { } - //MARK: - toJson - - var toJsonUnderlyingCallsCount = 0 - open var toJsonCallsCount: Int { - get { - if Thread.isMainThread { - return toJsonUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = toJsonUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - toJsonUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - toJsonUnderlyingCallsCount = newValue - } - } - } - } - open var toJsonCalled: Bool { - return toJsonCallsCount > 0 - } - - var toJsonUnderlyingReturnValue: String! - open var toJsonReturnValue: String! { - get { - if Thread.isMainThread { - return toJsonUnderlyingReturnValue - } else { - var returnValue: String? = nil - DispatchQueue.main.sync { - returnValue = toJsonUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - toJsonUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - toJsonUnderlyingReturnValue = newValue - } - } - } - } - open var toJsonClosure: (() -> String)? - - open override func toJson() -> String { - toJsonCallsCount += 1 - if let toJsonClosure = toJsonClosure { - return toJsonClosure() - } else { - return toJsonReturnValue - } - } - //MARK: - url var urlUnderlyingCallsCount = 0 @@ -11164,16 +11275,16 @@ open class RoomSDKMock: MatrixRustSDK.Room { //MARK: - ignoreDeviceTrustAndResend - open var ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError: Error? - var ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = 0 - open var ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount: Int { + open var ignoreDeviceTrustAndResendDevicesSendHandleThrowableError: Error? + var ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = 0 + open var ignoreDeviceTrustAndResendDevicesSendHandleCallsCount: Int { get { if Thread.isMainThread { - return ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount + return ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount + returnValue = ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount } return returnValue! @@ -11181,31 +11292,31 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue + ignoreDeviceTrustAndResendDevicesSendHandleUnderlyingCallsCount = newValue } } } } - open var ignoreDeviceTrustAndResendDevicesTransactionIdCalled: Bool { - return ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount > 0 + open var ignoreDeviceTrustAndResendDevicesSendHandleCalled: Bool { + return ignoreDeviceTrustAndResendDevicesSendHandleCallsCount > 0 } - open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments: (devices: [String: [String]], transactionId: String)? - open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations: [(devices: [String: [String]], transactionId: String)] = [] - open var ignoreDeviceTrustAndResendDevicesTransactionIdClosure: (([String: [String]], String) async throws -> Void)? + open var ignoreDeviceTrustAndResendDevicesSendHandleReceivedArguments: (devices: [String: [String]], sendHandle: SendHandle)? + open var ignoreDeviceTrustAndResendDevicesSendHandleReceivedInvocations: [(devices: [String: [String]], sendHandle: SendHandle)] = [] + open var ignoreDeviceTrustAndResendDevicesSendHandleClosure: (([String: [String]], SendHandle) async throws -> Void)? - open override func ignoreDeviceTrustAndResend(devices: [String: [String]], transactionId: String) async throws { - if let error = ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError { + open override func ignoreDeviceTrustAndResend(devices: [String: [String]], sendHandle: SendHandle) async throws { + if let error = ignoreDeviceTrustAndResendDevicesSendHandleThrowableError { throw error } - ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount += 1 - ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments = (devices: devices, transactionId: transactionId) + ignoreDeviceTrustAndResendDevicesSendHandleCallsCount += 1 + ignoreDeviceTrustAndResendDevicesSendHandleReceivedArguments = (devices: devices, sendHandle: sendHandle) DispatchQueue.main.async { - self.ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations.append((devices: devices, transactionId: transactionId)) + self.ignoreDeviceTrustAndResendDevicesSendHandleReceivedInvocations.append((devices: devices, sendHandle: sendHandle)) } - try await ignoreDeviceTrustAndResendDevicesTransactionIdClosure?(devices, transactionId) + try await ignoreDeviceTrustAndResendDevicesSendHandleClosure?(devices, sendHandle) } //MARK: - ignoreUser @@ -14036,52 +14147,6 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } - //MARK: - tryResend - - open var tryResendTransactionIdThrowableError: Error? - var tryResendTransactionIdUnderlyingCallsCount = 0 - open var tryResendTransactionIdCallsCount: Int { - get { - if Thread.isMainThread { - return tryResendTransactionIdUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = tryResendTransactionIdUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - tryResendTransactionIdUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - tryResendTransactionIdUnderlyingCallsCount = newValue - } - } - } - } - open var tryResendTransactionIdCalled: Bool { - return tryResendTransactionIdCallsCount > 0 - } - open var tryResendTransactionIdReceivedTransactionId: String? - open var tryResendTransactionIdReceivedInvocations: [String] = [] - open var tryResendTransactionIdClosure: ((String) async throws -> Void)? - - open override func tryResend(transactionId: String) async throws { - if let error = tryResendTransactionIdThrowableError { - throw error - } - tryResendTransactionIdCallsCount += 1 - tryResendTransactionIdReceivedTransactionId = transactionId - DispatchQueue.main.async { - self.tryResendTransactionIdReceivedInvocations.append(transactionId) - } - try await tryResendTransactionIdClosure?(transactionId) - } - //MARK: - typingNotice open var typingNoticeIsTypingThrowableError: Error? @@ -14268,16 +14333,16 @@ open class RoomSDKMock: MatrixRustSDK.Room { //MARK: - withdrawVerificationAndResend - open var withdrawVerificationAndResendUserIdsTransactionIdThrowableError: Error? - var withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = 0 - open var withdrawVerificationAndResendUserIdsTransactionIdCallsCount: Int { + open var withdrawVerificationAndResendUserIdsSendHandleThrowableError: Error? + var withdrawVerificationAndResendUserIdsSendHandleUnderlyingCallsCount = 0 + open var withdrawVerificationAndResendUserIdsSendHandleCallsCount: Int { get { if Thread.isMainThread { - return withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount + return withdrawVerificationAndResendUserIdsSendHandleUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount + returnValue = withdrawVerificationAndResendUserIdsSendHandleUnderlyingCallsCount } return returnValue! @@ -14285,31 +14350,31 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue + withdrawVerificationAndResendUserIdsSendHandleUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue + withdrawVerificationAndResendUserIdsSendHandleUnderlyingCallsCount = newValue } } } } - open var withdrawVerificationAndResendUserIdsTransactionIdCalled: Bool { - return withdrawVerificationAndResendUserIdsTransactionIdCallsCount > 0 + open var withdrawVerificationAndResendUserIdsSendHandleCalled: Bool { + return withdrawVerificationAndResendUserIdsSendHandleCallsCount > 0 } - open var withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments: (userIds: [String], transactionId: String)? - open var withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations: [(userIds: [String], transactionId: String)] = [] - open var withdrawVerificationAndResendUserIdsTransactionIdClosure: (([String], String) async throws -> Void)? + open var withdrawVerificationAndResendUserIdsSendHandleReceivedArguments: (userIds: [String], sendHandle: SendHandle)? + open var withdrawVerificationAndResendUserIdsSendHandleReceivedInvocations: [(userIds: [String], sendHandle: SendHandle)] = [] + open var withdrawVerificationAndResendUserIdsSendHandleClosure: (([String], SendHandle) async throws -> Void)? - open override func withdrawVerificationAndResend(userIds: [String], transactionId: String) async throws { - if let error = withdrawVerificationAndResendUserIdsTransactionIdThrowableError { + open override func withdrawVerificationAndResend(userIds: [String], sendHandle: SendHandle) async throws { + if let error = withdrawVerificationAndResendUserIdsSendHandleThrowableError { throw error } - withdrawVerificationAndResendUserIdsTransactionIdCallsCount += 1 - withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments = (userIds: userIds, transactionId: transactionId) + withdrawVerificationAndResendUserIdsSendHandleCallsCount += 1 + withdrawVerificationAndResendUserIdsSendHandleReceivedArguments = (userIds: userIds, sendHandle: sendHandle) DispatchQueue.main.async { - self.withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations.append((userIds: userIds, transactionId: transactionId)) + self.withdrawVerificationAndResendUserIdsSendHandleReceivedInvocations.append((userIds: userIds, sendHandle: sendHandle)) } - try await withdrawVerificationAndResendUserIdsTransactionIdClosure?(userIds, transactionId) + try await withdrawVerificationAndResendUserIdsSendHandleClosure?(userIds, sendHandle) } } open class RoomDirectorySearchSDKMock: MatrixRustSDK.RoomDirectorySearch { @@ -16721,6 +16786,71 @@ open class RoomPreviewSDKMock: MatrixRustSDK.RoomPreview { } } + //MARK: - inviter + + var inviterUnderlyingCallsCount = 0 + open var inviterCallsCount: Int { + get { + if Thread.isMainThread { + return inviterUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = inviterUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + inviterUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + inviterUnderlyingCallsCount = newValue + } + } + } + } + open var inviterCalled: Bool { + return inviterCallsCount > 0 + } + + var inviterUnderlyingReturnValue: RoomMember? + open var inviterReturnValue: RoomMember? { + get { + if Thread.isMainThread { + return inviterUnderlyingReturnValue + } else { + var returnValue: RoomMember?? = nil + DispatchQueue.main.sync { + returnValue = inviterUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + inviterUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + inviterUnderlyingReturnValue = newValue + } + } + } + } + open var inviterClosure: (() async -> RoomMember?)? + + open override func inviter() async -> RoomMember? { + inviterCallsCount += 1 + if let inviterClosure = inviterClosure { + return await inviterClosure() + } else { + return inviterReturnValue + } + } + //MARK: - leave open var leaveThrowableError: Error? @@ -16927,6 +17057,46 @@ open class SendHandleSDKMock: MatrixRustSDK.SendHandle { return abortReturnValue } } + + //MARK: - tryResend + + open var tryResendThrowableError: Error? + var tryResendUnderlyingCallsCount = 0 + open var tryResendCallsCount: Int { + get { + if Thread.isMainThread { + return tryResendUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = tryResendUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + tryResendUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + tryResendUnderlyingCallsCount = newValue + } + } + } + } + open var tryResendCalled: Bool { + return tryResendCallsCount > 0 + } + open var tryResendClosure: (() async throws -> Void)? + + open override func tryResend() async throws { + if let error = tryResendThrowableError { + throw error + } + tryResendCallsCount += 1 + try await tryResendClosure?() + } } open class SessionVerificationControllerSDKMock: MatrixRustSDK.SessionVerificationController { init() { @@ -17986,15 +18156,15 @@ open class SyncServiceBuilderSDKMock: MatrixRustSDK.SyncServiceBuilder { //MARK: - withCrossProcessLock - var withCrossProcessLockAppIdentifierUnderlyingCallsCount = 0 - open var withCrossProcessLockAppIdentifierCallsCount: Int { + var withCrossProcessLockUnderlyingCallsCount = 0 + open var withCrossProcessLockCallsCount: Int { get { if Thread.isMainThread { - return withCrossProcessLockAppIdentifierUnderlyingCallsCount + return withCrossProcessLockUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = withCrossProcessLockAppIdentifierUnderlyingCallsCount + returnValue = withCrossProcessLockUnderlyingCallsCount } return returnValue! @@ -18002,29 +18172,27 @@ open class SyncServiceBuilderSDKMock: MatrixRustSDK.SyncServiceBuilder { } set { if Thread.isMainThread { - withCrossProcessLockAppIdentifierUnderlyingCallsCount = newValue + withCrossProcessLockUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - withCrossProcessLockAppIdentifierUnderlyingCallsCount = newValue + withCrossProcessLockUnderlyingCallsCount = newValue } } } } - open var withCrossProcessLockAppIdentifierCalled: Bool { - return withCrossProcessLockAppIdentifierCallsCount > 0 + open var withCrossProcessLockCalled: Bool { + return withCrossProcessLockCallsCount > 0 } - open var withCrossProcessLockAppIdentifierReceivedAppIdentifier: String? - open var withCrossProcessLockAppIdentifierReceivedInvocations: [String?] = [] - var withCrossProcessLockAppIdentifierUnderlyingReturnValue: SyncServiceBuilder! - open var withCrossProcessLockAppIdentifierReturnValue: SyncServiceBuilder! { + var withCrossProcessLockUnderlyingReturnValue: SyncServiceBuilder! + open var withCrossProcessLockReturnValue: SyncServiceBuilder! { get { if Thread.isMainThread { - return withCrossProcessLockAppIdentifierUnderlyingReturnValue + return withCrossProcessLockUnderlyingReturnValue } else { var returnValue: SyncServiceBuilder? = nil DispatchQueue.main.sync { - returnValue = withCrossProcessLockAppIdentifierUnderlyingReturnValue + returnValue = withCrossProcessLockUnderlyingReturnValue } return returnValue! @@ -18032,26 +18200,22 @@ open class SyncServiceBuilderSDKMock: MatrixRustSDK.SyncServiceBuilder { } set { if Thread.isMainThread { - withCrossProcessLockAppIdentifierUnderlyingReturnValue = newValue + withCrossProcessLockUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - withCrossProcessLockAppIdentifierUnderlyingReturnValue = newValue + withCrossProcessLockUnderlyingReturnValue = newValue } } } } - open var withCrossProcessLockAppIdentifierClosure: ((String?) -> SyncServiceBuilder)? + open var withCrossProcessLockClosure: (() -> SyncServiceBuilder)? - open override func withCrossProcessLock(appIdentifier: String?) -> SyncServiceBuilder { - withCrossProcessLockAppIdentifierCallsCount += 1 - withCrossProcessLockAppIdentifierReceivedAppIdentifier = appIdentifier - DispatchQueue.main.async { - self.withCrossProcessLockAppIdentifierReceivedInvocations.append(appIdentifier) - } - if let withCrossProcessLockAppIdentifierClosure = withCrossProcessLockAppIdentifierClosure { - return withCrossProcessLockAppIdentifierClosure(appIdentifier) + open override func withCrossProcessLock() -> SyncServiceBuilder { + withCrossProcessLockCallsCount += 1 + if let withCrossProcessLockClosure = withCrossProcessLockClosure { + return withCrossProcessLockClosure() } else { - return withCrossProcessLockAppIdentifierReturnValue + return withCrossProcessLockReturnValue } } @@ -19268,15 +19432,15 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { //MARK: - sendFile - var sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = 0 - open var sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount: Int { + var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = 0 + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount: Int { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount + return sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount + returnValue = sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount } return returnValue! @@ -19284,29 +19448,29 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = newValue + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingCallsCount = newValue + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingCallsCount = newValue } } } } - open var sendFileUrlFileInfoProgressWatcherUseSendQueueCalled: Bool { - return sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount > 0 + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCalled: Bool { + return sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount > 0 } - open var sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedArguments: (url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? - open var sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedInvocations: [(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments: (url: String, fileInfo: FileInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)? + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations: [(url: String, fileInfo: FileInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool)] = [] - var sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! - open var sendFileUrlFileInfoProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { + var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue: SendAttachmentJoinHandle! + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue: SendAttachmentJoinHandle! { get { if Thread.isMainThread { - return sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue + return sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } else { var returnValue: SendAttachmentJoinHandle? = nil DispatchQueue.main.sync { - returnValue = sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue + returnValue = sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue } return returnValue! @@ -19314,26 +19478,26 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline { } set { if Thread.isMainThread { - sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue = newValue + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sendFileUrlFileInfoProgressWatcherUseSendQueueUnderlyingReturnValue = newValue + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueUnderlyingReturnValue = newValue } } } } - open var sendFileUrlFileInfoProgressWatcherUseSendQueueClosure: ((String, FileInfo, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? + open var sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure: ((String, FileInfo, String?, FormattedBody?, ProgressWatcher?, Bool) -> SendAttachmentJoinHandle)? - open override func sendFile(url: String, fileInfo: FileInfo, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { - sendFileUrlFileInfoProgressWatcherUseSendQueueCallsCount += 1 - sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedArguments = (url: url, fileInfo: fileInfo, progressWatcher: progressWatcher, useSendQueue: useSendQueue) + open override func sendFile(url: String, fileInfo: FileInfo, caption: String?, formattedCaption: FormattedBody?, progressWatcher: ProgressWatcher?, useSendQueue: Bool) -> SendAttachmentJoinHandle { + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueCallsCount += 1 + sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedArguments = (url: url, fileInfo: fileInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue) DispatchQueue.main.async { - self.sendFileUrlFileInfoProgressWatcherUseSendQueueReceivedInvocations.append((url: url, fileInfo: fileInfo, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) + self.sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReceivedInvocations.append((url: url, fileInfo: fileInfo, caption: caption, formattedCaption: formattedCaption, progressWatcher: progressWatcher, useSendQueue: useSendQueue)) } - if let sendFileUrlFileInfoProgressWatcherUseSendQueueClosure = sendFileUrlFileInfoProgressWatcherUseSendQueueClosure { - return sendFileUrlFileInfoProgressWatcherUseSendQueueClosure(url, fileInfo, progressWatcher, useSendQueue) + if let sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure = sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure { + return sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueClosure(url, fileInfo, caption, formattedCaption, progressWatcher, useSendQueue) } else { - return sendFileUrlFileInfoProgressWatcherUseSendQueueReturnValue + return sendFileUrlFileInfoCaptionFormattedCaptionProgressWatcherUseSendQueueReturnValue } } diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift index 66bc9a93f2..79b48100c8 100644 --- a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -23,6 +23,7 @@ extension InvitedRoomProxyMock { convenience init(_ configuration: InvitedRoomProxyMockConfiguration) { self.init() id = configuration.id + inviter = configuration.inviter info = RoomInfoProxy(roomInfo: .init(configuration)) } } @@ -58,7 +59,8 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: []) + pinnedEventIds: [], + joinRule: .invite) } } diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 2f147104fb..d8acfaf791 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -39,6 +39,7 @@ struct JoinedRoomProxyMockConfiguration { var canUserPin = true var shouldUseAutoUpdatingTimeline = false + var joinRule: JoinRule? } extension JoinedRoomProxyMock { @@ -49,22 +50,8 @@ extension JoinedRoomProxyMock { id = configuration.id isEncrypted = configuration.isEncrypted - let timeline = TimelineProxyMock() - timeline.sendMessageEventContentReturnValue = .success(()) - timeline.paginateBackwardsRequestSizeReturnValue = .success(()) - timeline.paginateForwardsRequestSizeReturnValue = .success(()) - timeline.sendReadReceiptForTypeReturnValue = .success(()) - - if configuration.shouldUseAutoUpdatingTimeline { - timeline.underlyingTimelineProvider = AutoUpdatingRoomTimelineProviderMock() - } else { - let timelineProvider = RoomTimelineProviderMock() - timelineProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached) - timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher() - timeline.underlyingTimelineProvider = timelineProvider - } - - self.timeline = timeline + timeline = TimelineProxyMock(.init(isAutoUpdating: configuration.shouldUseAutoUpdatingTimeline, + timelineStartReached: configuration.timelineStartReached)) ownUserID = configuration.ownUserID @@ -83,9 +70,8 @@ extension JoinedRoomProxyMock { return .success(member) } - resendItemIDReturnValue = .success(()) - ignoreDeviceTrustAndResendDevicesItemIDReturnValue = .success(()) - withdrawVerificationAndResendUserIDsItemIDReturnValue = .success(()) + ignoreDeviceTrustAndResendDevicesSendHandleReturnValue = .success(()) + withdrawVerificationAndResendUserIDsSendHandleReturnValue = .success(()) flagAsUnreadReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(()) @@ -138,6 +124,7 @@ extension JoinedRoomProxyMock { matrixToEventPermalinkReturnValue = .success(.homeDirectory) loadDraftReturnValue = .success(nil) clearDraftReturnValue = .success(()) + sendTypingNotificationIsTypingReturnValue = .success(()) } } @@ -180,6 +167,7 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: Array(configuration.pinnedEventIDs)) + pinnedEventIds: Array(configuration.pinnedEventIDs), + joinRule: configuration.joinRule) } } diff --git a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift index 76559bae13..82d84d31ab 100644 --- a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift @@ -57,6 +57,7 @@ extension RoomInfo { numUnreadMessages: 0, numUnreadNotifications: 0, numUnreadMentions: 0, - pinnedEventIds: []) + pinnedEventIds: [], + joinRule: .knock) } } diff --git a/ElementX/Sources/Mocks/MediaProviderMock.swift b/ElementX/Sources/Mocks/MediaProviderMock.swift index eb1929eff4..c2ae134960 100644 --- a/ElementX/Sources/Mocks/MediaProviderMock.swift +++ b/ElementX/Sources/Mocks/MediaProviderMock.swift @@ -18,7 +18,8 @@ extension MediaProviderMock { return nil } - if mediaSource?.url == .picturesDirectory { + // At some stage it would be nice to return different images, but for now they can be the same. + if mediaSource?.url == .mockMXCImage || mediaSource?.url == .mockMXCAvatar { return Asset.Images.appLogo.image } diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 2074c014b1..8c81c727e1 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -43,14 +43,14 @@ extension RoomMemberProxyMock { static var mockMe: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@me:matrix.org", displayName: "Me", - avatarURL: URL.picturesDirectory, + avatarURL: .mockMXCAvatar, membership: .join)) } static var mockMeAdmin: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@me:matrix.org", displayName: "Me", - avatarURL: URL.picturesDirectory, + avatarURL: .mockMXCAvatar, membership: .join, powerLevel: 100, role: .administrator)) @@ -83,7 +83,7 @@ extension RoomMemberProxyMock { static var mockDan: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@dan:matrix.org", displayName: "Dan", - avatarURL: URL.picturesDirectory, + avatarURL: .mockMXCAvatar, membership: .join)) } diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index ed88e9efe2..7c71582868 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -91,7 +91,7 @@ extension Array where Element == RoomSummary { joinRequestType: nil, name: "Foundation and Empire", isDirect: false, - avatarURL: URL.picturesDirectory, + avatarURL: .mockMXCAvatar, heroes: [], lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"), lastMessageFormattedTimestamp: "2:56 PM", @@ -226,7 +226,7 @@ extension Array where Element == RoomSummary { joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), name: "First room", isDirect: false, - avatarURL: URL.picturesDirectory, + avatarURL: .mockMXCAvatar, heroes: [], lastMessage: nil, lastMessageFormattedTimestamp: nil, diff --git a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift index 6dd55f8e35..8c99f36ec7 100644 --- a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift @@ -15,7 +15,7 @@ extension RoomTimelineControllerFactoryMock { convenience init(configuration: RoomTimelineControllerFactoryMockConfiguration) { self.init() - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue = configuration.timelineController ?? { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController diff --git a/ElementX/Sources/Mocks/TimelineProxyMock.swift b/ElementX/Sources/Mocks/TimelineProxyMock.swift new file mode 100644 index 0000000000..13147820e6 --- /dev/null +++ b/ElementX/Sources/Mocks/TimelineProxyMock.swift @@ -0,0 +1,35 @@ +// +// 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 TimelineProxyMock { + struct Configuration { + var isAutoUpdating = false + var timelineStartReached = false + } + + @MainActor + convenience init(_ configuration: Configuration) { + self.init() + + sendMessageEventContentReturnValue = .success(()) + paginateBackwardsRequestSizeReturnValue = .success(()) + paginateForwardsRequestSizeReturnValue = .success(()) + sendReadReceiptForTypeReturnValue = .success(()) + + if configuration.isAutoUpdating { + underlyingTimelineProvider = AutoUpdatingRoomTimelineProviderMock() + } else { + let timelineProvider = RoomTimelineProviderMock() + timelineProvider.paginationState = .init(backward: configuration.timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached) + timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher() + underlyingTimelineProvider = timelineProvider + } + } +} diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/Avatars.swift similarity index 50% rename from ElementX/Sources/Other/AvatarSize.swift rename to ElementX/Sources/Other/Avatars.swift index aa1b477b1c..96fc58aaaa 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -6,29 +6,55 @@ // import Foundation -import UIKit +import SwiftUI -enum AvatarSize { - case user(on: UserAvatarSizeOnScreen) - case room(on: RoomAvatarSizeOnScreen) - // custom - case custom(CGFloat) +enum Avatars { + enum Size { + case user(on: UserAvatarSizeOnScreen) + case room(on: RoomAvatarSizeOnScreen) + // custom + case custom(CGFloat) - /// Value in UIKit points - var value: CGFloat { - switch self { - case .user(let screen): - return screen.value - case .room(let screen): - return screen.value - case .custom(let val): - return val + /// Value in UIKit points + var value: CGFloat { + switch self { + case .user(let screen): + return screen.value + case .room(let screen): + return screen.value + case .custom(let val): + return val + } } - } - /// Value in pixels by using the scale of the main screen - var scaledValue: CGFloat { - value * UIScreen.main.scale + /// Value in pixels by using the scale of the main screen + var scaledValue: CGFloat { + value * UIScreen.main.scale + } + + var scaledSize: CGSize { + CGSize(width: scaledValue, height: scaledValue) + } + } + + @MainActor + static func generatePlaceholderAvatarImageData(name: String, id: String, size: CGSize) -> Data? { + let image = PlaceholderAvatarImage(name: name, contentID: id) + .clipShape(Circle()) + .frame(width: size.width, height: size.height) + + let renderer = ImageRenderer(content: image) + + // Specify the scale so the image is rendered correctly. We don't have access to the screen + // here so a hardcoded 3.0 will have to do + renderer.scale = 3.0 + + guard let image = renderer.uiImage else { + MXLog.info("Generating notification icon placeholder failed") + return nil + } + + return image.pngData() } } @@ -46,6 +72,9 @@ enum UserAvatarSizeOnScreen { case editUserDetails case suggestions case blockedUsers + case knockingUsersBannerStack + case knockingUserBanner + case knockingUserList var value: CGFloat { switch self { @@ -75,6 +104,12 @@ enum UserAvatarSizeOnScreen { return 96 case .dmDetails: return 75 + case .knockingUsersBannerStack: + return 28 + case .knockingUserBanner: + return 32 + case .knockingUserList: + return 52 } } } @@ -84,6 +119,7 @@ enum RoomAvatarSizeOnScreen { case home case messageForwarding case globalSearch + case roomSelection case details case notificationSettings case roomDirectorySearch @@ -101,6 +137,8 @@ enum RoomAvatarSizeOnScreen { return 36 case .globalSearch: return 36 + case .roomSelection: + return 36 case .home: return 52 case .details: @@ -110,9 +148,3 @@ enum RoomAvatarSizeOnScreen { } } } - -extension AvatarSize { - var scaledSize: CGSize { - CGSize(width: scaledValue, height: scaledValue) - } -} diff --git a/ElementX/Sources/Other/Extensions/ClientBuilder.swift b/ElementX/Sources/Other/Extensions/ClientBuilder.swift index 6abce9a3b7..6ca8cb335a 100644 --- a/ElementX/Sources/Other/Extensions/ClientBuilder.swift +++ b/ElementX/Sources/Other/Extensions/ClientBuilder.swift @@ -17,7 +17,9 @@ extension ClientBuilder { appHooks: AppHooks, enableOnlySignedDeviceIsolationMode: Bool) -> ClientBuilder { var builder = ClientBuilder() - .enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, sessionDelegate: sessionDelegate) + .crossProcessStoreLocksHolderName(holderName: InfoPlistReader.main.bundleIdentifier) + .enableOidcRefreshLock() + .setSessionDelegate(sessionDelegate: sessionDelegate) .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent()) .requestConfig(config: .init(retryLimit: 0, timeout: 30000, maxConcurrentRequests: nil, retryTimeout: nil)) diff --git a/ElementX/Sources/Other/Extensions/FileManager.swift b/ElementX/Sources/Other/Extensions/FileManager.swift index 0709dec3a4..a969d63265 100644 --- a/ElementX/Sources/Other/Extensions/FileManager.swift +++ b/ElementX/Sources/Other/Extensions/FileManager.swift @@ -38,7 +38,7 @@ extension FileManager { @discardableResult func writeDataToTemporaryDirectory(data: Data, fileName: String) throws -> URL { - let newURL = URL.temporaryDirectory.appendingPathComponent(fileName) + let newURL = URL.appGroupTemporaryDirectory.appendingPathComponent(fileName) try data.write(to: newURL) diff --git a/ElementX/Sources/Other/Extensions/NSItemProvider.swift b/ElementX/Sources/Other/Extensions/NSItemProvider.swift index fef3478dbb..67aedca111 100644 --- a/ElementX/Sources/Other/Extensions/NSItemProvider.swift +++ b/ElementX/Sources/Other/Extensions/NSItemProvider.swift @@ -6,23 +6,149 @@ // import Foundation +import SwiftUI import UniformTypeIdentifiers extension NSItemProvider { + struct PreferredContentType { + let type: UTType + let fileExtension: String + } + + func loadTransferable(type transferableType: T.Type) async -> T? { + try? await withCheckedContinuation { continuation in + _ = loadTransferable(type: T.self) { result in + continuation.resume(returning: result) + } + } + .get() + } + + func loadString() async -> String? { + try? await loadItem(forTypeIdentifier: UTType.text.identifier) as? String + } + + func storeData() async -> URL? { + guard let contentType = preferredContentType else { + MXLog.error("Invalid NSItemProvider: \(self)") + return nil + } + + if contentType.type.identifier == UTType.image.identifier { + return await generateURLForUIImage(contentType) + } else { + return await generateURLForGenericData(contentType) + } + } + + private func generateURLForUIImage(_ contentType: PreferredContentType) async -> URL? { + guard let uiImage = try? await loadItem(forTypeIdentifier: contentType.type.identifier) as? UIImage else { + MXLog.error("Failed casting UIImage, invalid NSItemProvider: \(self)") + return nil + } + + guard let pngData = uiImage.pngData() else { + MXLog.error("Failed extracting PNG data out of the UIImage") + return nil + } + + do { + if let suggestedName = suggestedName as? NSString, + // Suggestions are nice but their extension is `jpeg` + let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) { + return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename) + } else { + let filename = "\(UUID().uuidString).\(contentType.fileExtension)" + return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename) + } + } catch { + MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)") + return nil + } + } + + private func generateURLForGenericData(_ contentType: PreferredContentType) async -> URL? { + let providerDescription = description + let shareData: Data? = await withCheckedContinuation { continuation in + _ = loadDataRepresentation(for: contentType.type) { data, error in + if let error { + MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)") + continuation.resume(returning: nil) + return + } + + guard let data else { + MXLog.error("Invalid NSItemProvider data: \(providerDescription)") + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: data) + } + } + + guard let shareData else { + MXLog.error("Invalid share data") + return nil + } + + do { + if let filename = suggestedName { + let hasExtension = !(filename as NSString).pathExtension.isEmpty + let filename = hasExtension ? filename : "\(filename).\(contentType.fileExtension)" + return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename) + } else { + let filename = "\(UUID().uuidString).\(contentType.fileExtension)" + return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename) + } + } catch { + MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)") + return nil + } + } + var isSupportedForPasteOrDrop: Bool { preferredContentType != nil } - var preferredContentType: UTType? { + var preferredContentType: PreferredContentType? { let supportedContentTypes = registeredContentTypes - .filter { isMimeTypeSupported($0.preferredMIMEType) } + .filter { isMimeTypeSupported($0.preferredMIMEType) || isIdentifierSupported($0.identifier) } // Have .jpeg take priority over .heic if supportedContentTypes.contains(.jpeg) { - return .jpeg + guard let fileExtension = preferredFileExtension(for: .jpeg) else { + return nil + } + + return .init(type: .jpeg, fileExtension: fileExtension) } - return supportedContentTypes.first + guard let preferredContentType = supportedContentTypes.first, + let fileExtension = preferredFileExtension(for: preferredContentType) else { + return nil + } + + return .init(type: preferredContentType, fileExtension: fileExtension) + } + + private func preferredFileExtension(for contentType: UTType) -> String? { + if let fileExtension = contentType.preferredFilenameExtension { + return fileExtension + } + + switch contentType.identifier { + case UTType.image.identifier: + return "png" + default: + return nil + } + } + + private func isIdentifierSupported(_ identifier: String?) -> Bool { + // Don't filter out generic public.image content as screenshots are in this format + // and we can convert them to a PNG ourselves. + identifier == UTType.image.identifier } private func isMimeTypeSupported(_ mimeType: String?) -> Bool { diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift index 2fa224d5a6..f9a45d8d77 100644 --- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -183,37 +183,17 @@ extension UNMutableNotificationContent { @MainActor private func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? { // The version value is used in case the design of the placeholder is updated to force a replacement - let shouldFlipAvatar = shouldFlipAvatar() - let prefix = "notification_placeholder\(shouldFlipAvatar ? "V8F" : "V8")" + let prefix = "notification_placeholderV9" + let fileName = "\(prefix)_\(name)_\(id).png" if let data = try? Data(contentsOf: URL.temporaryDirectory.appendingPathComponent(fileName)) { MXLog.info("Found existing notification icon placeholder") return data } - - MXLog.info("Generating notification icon placeholder") - let image = PlaceholderAvatarImage(name: name, - contentID: id) - .clipShape(Circle()) - .frame(width: 50, height: 50) - let renderer = ImageRenderer(content: image) - - // Specify the scale so the image is rendered correctly. We don't have access to the screen - // here so a hardcoded 3.0 will have to do - renderer.scale = 3.0 - - guard let image = renderer.uiImage else { - MXLog.info("Generating notification icon placeholder failed") - return nil - } - let data: Data? + MXLog.info("Generating notification icon placeholder") - if shouldFlipAvatar { - data = image.flippedVertically().pngData() - } else { - data = image.pngData() - } + let data = Avatars.generatePlaceholderAvatarImageData(name: name, id: id, size: .init(width: 50, height: 50)) if let data { do { @@ -224,44 +204,7 @@ extension UNMutableNotificationContent { return data } } - return data - } - - /// On simulators and macOS the image is rendered correctly - /// On devices before iOS 17 and iOS 17.2.0 it's rendered upside down and needs to be flipped - /// On all other versions it's rendered correctly and **doesn't** need to be flipped - private func shouldFlipAvatar() -> Bool { - #if targetEnvironment(simulator) - return false - #else - if ProcessInfo.processInfo.isiOSAppOnMac { - return false - } - - guard let version = Version(UIDevice.current.systemVersion) else { - return false - } - - if version < Version(17, 0, 0) { - return true - } - - if version == Version(17, 2, 0) { - return true - } - return false - #endif - } -} - -private extension UIImage { - func flippedVertically() -> UIImage { - let format = UIGraphicsImageRendererFormat() - format.scale = scale - return UIGraphicsImageRenderer(size: size, format: format).image { context in - context.cgContext.concatenate(CGAffineTransform(scaleX: 1, y: -1)) - self.draw(at: CGPoint(x: 0, y: -size.height)) - } + return data } } diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index 3506d46c5f..0c026ef129 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -60,7 +60,7 @@ extension URL: @retroactive ExpressibleByStringLiteral { } /// The base directory where all application support data is stored. - static var cachesBaseDirectory: URL { + static var sessionCachesBaseDirectory: URL { let url = appGroupContainerDirectory .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true) @@ -69,7 +69,20 @@ extension URL: @retroactive ExpressibleByStringLiteral { try? FileManager.default.createDirectoryIfNeeded(at: url) - // Caches are excluded from backups automatically anyway. + // Caches are excluded from backups automatically. + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html + + return url + } + + /// The app group temporary directory + static var appGroupTemporaryDirectory: URL { + let url = appGroupContainerDirectory + .appendingPathComponent("tmp", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + // Temporary files are excluded from backups automatically. // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html return url @@ -87,4 +100,12 @@ extension URL: @retroactive ExpressibleByStringLiteral { } return nil } + + // MARK: Mocks + + static var mockMXCAudio: URL { "mxc://matrix.org/1234567890FiLe" } + static var mockMXCFile: URL { "mxc://matrix.org/1234567890FiLe" } + static var mockMXCImage: URL { "mxc://matrix.org/1234567890ImAgE" } + static var mockMXCVideo: URL { "mxc://matrix.org/1234567890ViDeO" } + static var mockMXCAvatar: URL { "mxc://matrix.org/1234567890AvAtAr" } } diff --git a/NSE/Sources/Other/NSELogger.swift b/ElementX/Sources/Other/Logging/ExtensionLogger.swift similarity index 93% rename from NSE/Sources/Other/NSELogger.swift rename to ElementX/Sources/Other/Logging/ExtensionLogger.swift index 985038136a..3367d1c7ee 100644 --- a/NSE/Sources/Other/NSELogger.swift +++ b/ElementX/Sources/Other/Logging/ExtensionLogger.swift @@ -8,7 +8,7 @@ import Foundation import MatrixRustSDK -enum NSELogger { +enum ExtensionLogger { private static var isConfigured = false /// Memory formatter, uses exact 2 fraction digits and no grouping @@ -66,13 +66,13 @@ enum NSELogger { return "\(formattedStr) MB" } - static func configure(logLevel: TracingConfiguration.LogLevel) { + static func configure(currentTarget: String, logLevel: TracingConfiguration.LogLevel) { guard !isConfigured else { return } isConfigured = true - MXLog.configure(currentTarget: "nse", filePrefix: "nse", logLevel: logLevel) + MXLog.configure(currentTarget: currentTarget, filePrefix: currentTarget, logLevel: logLevel) } static func logMemory(with tag: String) { diff --git a/ElementX/Sources/Other/SwiftUI/Layout/MenuSheetLabelStyle.swift b/ElementX/Sources/Other/SwiftUI/Layout/MenuSheetLabelStyle.swift index 0ea17f68e4..12f701be93 100644 --- a/ElementX/Sources/Other/SwiftUI/Layout/MenuSheetLabelStyle.swift +++ b/ElementX/Sources/Other/SwiftUI/Layout/MenuSheetLabelStyle.swift @@ -7,14 +7,34 @@ import SwiftUI -extension LabelStyle where Self == MenuSheetLabelStyle { - /// A label style for labels that are within a menu that is being presented as a sheet. - static var menuSheet: Self { MenuSheetLabelStyle() } +extension ButtonStyle where Self == MenuSheetButtonStyle { + /// A button style for buttons that are within a menu that is being presented as a sheet. + static var menuSheet: Self { MenuSheetButtonStyle() } } -/// The style used for labels that are part of a menu that's presented as -/// a sheet as `TimelineItemMenu` and `RoomAttachmentPicker`. -struct MenuSheetLabelStyle: LabelStyle { +/// The style used for buttons that are part of a menu that's presented as +/// a sheet such as `TimelineItemMenu`. +struct MenuSheetButtonStyle: ButtonStyle { + @Environment(\.accessibilityShowButtonShapes) private var accessibilityShowButtonShapes + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .labelStyle(MenuSheetLabelStyle()) + .foregroundStyle(configuration.role == .destructive ? .compound.textCriticalPrimary : .compound.textActionPrimary) + .contentShape(.rect) + .opacity(configuration.isPressed ? 0.3 : 1) + .background { + if accessibilityShowButtonShapes { + RoundedRectangle(cornerRadius: 12) + .fill(Color(uiColor: .secondarySystemFill)) + .opacity(configuration.isPressed ? 0.8 : 1) + .padding(4) + } + } + } +} + +private struct MenuSheetLabelStyle: LabelStyle { var spacing: CGFloat = 16 func makeBody(configuration: Configuration) -> some View { diff --git a/ElementX/Sources/Other/SwiftUI/Layout/ScaledFrameModifier.swift b/ElementX/Sources/Other/SwiftUI/Layout/ScaledFrameModifier.swift deleted file mode 100644 index 84de64af9b..0000000000 --- a/ElementX/Sources/Other/SwiftUI/Layout/ScaledFrameModifier.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright 2023, 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import SwiftUI - -extension View { - func scaledFrame(size: CGFloat, alignment: Alignment = .center, relativeTo textStyle: Font.TextStyle = .body) -> some View { - scaledFrame(width: size, height: size, alignment: alignment, relativeTo: textStyle) - } - - func scaledFrame(width: CGFloat, height: CGFloat, alignment: Alignment = .center, relativeTo textStyle: Font.TextStyle = .body) -> some View { - modifier(ScaledFrameModifier(width: width, height: height, alignment: alignment, relativeTo: textStyle)) - } -} - -private struct ScaledFrameModifier: ViewModifier { - @ScaledMetric var width: CGFloat - @ScaledMetric var height: CGFloat - let alignment: Alignment - - init(width: CGFloat, height: CGFloat, alignment: Alignment, relativeTo textStyle: Font.TextStyle) { - _width = ScaledMetric(wrappedValue: width, relativeTo: textStyle) - _height = ScaledMetric(wrappedValue: height, relativeTo: textStyle) - self.alignment = alignment - } - - func body(content: Content) -> some View { - content.frame(width: width, height: height, alignment: alignment) - } -} diff --git a/ElementX/Sources/Other/SwiftUI/Layout/ScaledOffsetModifier.swift b/ElementX/Sources/Other/SwiftUI/Layout/ScaledOffsetModifier.swift deleted file mode 100644 index 550e470f4b..0000000000 --- a/ElementX/Sources/Other/SwiftUI/Layout/ScaledOffsetModifier.swift +++ /dev/null @@ -1,28 +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 SwiftUI - -extension View { - func scaledOffset(x: CGFloat = 0, y: CGFloat = 0, relativeTo textStyle: Font.TextStyle = .body) -> some View { - modifier(ScaledOffsetModifier(x: x, y: y, relativeTo: textStyle)) - } -} - -private struct ScaledOffsetModifier: ViewModifier { - @ScaledMetric var x: CGFloat - @ScaledMetric var y: CGFloat - - init(x: CGFloat, y: CGFloat, relativeTo textStyle: Font.TextStyle) { - _x = ScaledMetric(wrappedValue: x, relativeTo: textStyle) - _y = ScaledMetric(wrappedValue: y, relativeTo: textStyle) - } - - func body(content: Content) -> some View { - content.offset(x: x, y: y) - } -} diff --git a/ElementX/Sources/Other/SwiftUI/Layout/ScaledPaddingModifier.swift b/ElementX/Sources/Other/SwiftUI/Layout/ScaledPaddingModifier.swift deleted file mode 100644 index c2e5e86136..0000000000 --- a/ElementX/Sources/Other/SwiftUI/Layout/ScaledPaddingModifier.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright 2023, 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import SwiftUI - -extension View { - func scaledPadding(_ length: CGFloat, relativeTo textStyle: Font.TextStyle = .body) -> some View { - scaledPadding(.all, length, relativeTo: textStyle) - } - - func scaledPadding(_ edges: Edge.Set, _ length: CGFloat, relativeTo textStyle: Font.TextStyle = .body) -> some View { - modifier(ScaledPaddingModifier(edges: edges, length: length, textStyle: textStyle)) - } -} - -private struct ScaledPaddingModifier: ViewModifier { - let edges: Edge.Set - @ScaledMetric var length: CGFloat - - init(edges: Edge.Set, length: CGFloat, textStyle: Font.TextStyle) { - self.edges = edges - _length = ScaledMetric(wrappedValue: length, relativeTo: textStyle) - } - - func body(content: Content) -> some View { - content.padding(edges, length) - } -} diff --git a/ElementX/Sources/Other/SwiftUI/Layout/TimelineMediaFrame.swift b/ElementX/Sources/Other/SwiftUI/Layout/TimelineMediaFrame.swift index 1cec148401..c715f5fc93 100644 --- a/ElementX/Sources/Other/SwiftUI/Layout/TimelineMediaFrame.swift +++ b/ElementX/Sources/Other/SwiftUI/Layout/TimelineMediaFrame.swift @@ -10,18 +10,18 @@ import SwiftUI extension View { /// Constrains the max height of a media item in the timeline, whilst preserving its aspect ratio. @ViewBuilder - func timelineMediaFrame(height contentHeight: CGFloat?, aspectRatio contentAspectRatio: CGFloat?) -> some View { + func timelineMediaFrame(imageInfo: ImageInfoProxy?) -> some View { let defaultMediaSize = 100.0 let minMediaHeight = 100.0 let maxMediaHeight = 300.0 - if let contentHeight, contentHeight < minMediaHeight { // Special case very small images - aspectRatio(contentAspectRatio, contentMode: .fit) + if let contentHeight = imageInfo?.size?.height, contentHeight < minMediaHeight { // Special case very small images + aspectRatio(imageInfo?.aspectRatio, contentMode: .fit) .frame(minHeight: minMediaHeight, maxHeight: minMediaHeight) } else { - if let contentAspectRatio { + if let contentAspectRatio = imageInfo?.aspectRatio { aspectRatio(contentAspectRatio, contentMode: .fit) - .frame(maxHeight: min(maxMediaHeight, max(minMediaHeight, contentHeight ?? .infinity))) + .frame(maxHeight: min(maxMediaHeight, max(minMediaHeight, imageInfo?.size?.height ?? .infinity))) // Required to prevent the reply details to get higher priority in rendering the width of the view. .aspectRatio(contentAspectRatio, contentMode: .fit) } else { // Otherwise force the image to be `defaultMediaSize` x `defaultMediaSize` diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index bca095d560..0070c00141 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -25,13 +25,13 @@ struct AvatarHeaderView: View { private let subtitle: String? private let badges: [Badge] - private let avatarSize: AvatarSize + private let avatarSize: Avatars.Size private let mediaProvider: MediaProviderProtocol? private var onAvatarTap: ((URL) -> Void)? @ViewBuilder private var footer: () -> Footer init(room: RoomDetails, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { @@ -72,7 +72,7 @@ struct AvatarHeaderView: View { init(member: RoomMemberDetails, isVerified: Bool = false, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { @@ -88,7 +88,7 @@ struct AvatarHeaderView: View { init(user: UserProfileProxy, isVerified: Bool, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { @@ -193,7 +193,7 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview { name: "Test Room", avatar: .room(id: "@test:matrix.org", name: "Test Room", - avatarURL: .picturesDirectory), + avatarURL: .mockMXCAvatar), canonicalAlias: "#test:matrix.org", isEncrypted: true, isPublic: true), diff --git a/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift b/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift index 2551c02ca1..a32482f47e 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/BigIcon.swift @@ -50,7 +50,7 @@ struct BigIcon: View { var style: Style = .defaultSolid var body: some View { - CompoundIcon(icon, size: .custom(32), relativeTo: .title) + CompoundIcon(icon, size: .custom(32), relativeTo: .compound.headingLG) .modifier(BigIconModifier(style: style)) } } @@ -62,7 +62,7 @@ extension Image { resizable() .renderingMode(.template) .aspectRatio(contentMode: .fit) - .scaledPadding(insets, relativeTo: .title) + .scaledPadding(insets, relativeTo: .compound.headingLG) .modifier(BigIconModifier(style: style)) } } @@ -72,7 +72,7 @@ private struct BigIconModifier: ViewModifier { func body(content: Content) -> some View { content - .scaledFrame(size: 64, relativeTo: .title) + .scaledFrame(size: 64, relativeTo: .compound.headingLG) .foregroundColor(style.foregroundColor) .background { RoundedRectangle(cornerRadius: 14) diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index 7daf4655de..1c048bd114 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -11,7 +11,7 @@ struct LoadableAvatarImage: View { private let url: URL? private let name: String? private let contentID: String? - private let avatarSize: AvatarSize + private let avatarSize: Avatars.Size private let mediaProvider: MediaProviderProtocol? private let onTap: ((URL) -> Void)? @@ -19,7 +19,7 @@ struct LoadableAvatarImage: View { init(url: URL?, name: String?, contentID: String?, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol?, onTap: ((URL) -> Void)? = nil) { self.url = url diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift index 1f909688bb..c34420f7ca 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift @@ -11,17 +11,17 @@ import Kingfisher import SwiftUI /// Used to configure animations -enum LoadableImageMediaType { +enum LoadableImageMediaType: Equatable { /// An avatar (can be displayed anywhere within the app). case avatar /// An image displayed in the timeline. - case timelineItem + case timelineItem(uniqueID: String) /// Any other media (can be displayed anywhere within the app). case generic } struct LoadableImage: View { - private let mediaSource: MediaSourceProxy + private let mediaSource: MediaSourceProxy? private let mediaType: LoadableImageMediaType private let blurhash: String? private let size: CGSize? @@ -60,26 +60,40 @@ struct LoadableImage: View { mediaProvider: MediaProviderProtocol?, transformer: @escaping (AnyView) -> TransformerView = { $0 }, placeholder: @escaping () -> PlaceholderView) { - self.init(mediaSource: MediaSourceProxy(url: url, mimeType: nil), - mediaType: mediaType, - blurhash: blurhash, - size: size, - mediaProvider: mediaProvider, - transformer: transformer, - placeholder: placeholder) + mediaSource = try? MediaSourceProxy(url: url, mimeType: nil) + self.mediaType = mediaType + self.blurhash = blurhash + self.size = size + self.mediaProvider = mediaProvider + self.transformer = transformer + self.placeholder = placeholder } var body: some View { - LoadableImageContent(mediaSource: mediaSource, - mediaType: mediaType, - blurhash: blurhash, - size: size, - mediaProvider: mediaProvider, - transformer: transformer, - placeholder: placeholder) + if let mediaSource { + LoadableImageContent(mediaSource: mediaSource, + mediaType: mediaType, + blurhash: blurhash, + size: size, + mediaProvider: mediaProvider, + transformer: transformer, + placeholder: placeholder) + .id(stableMediaIdentifier) + } else { + placeholder() + } + } + + private var stableMediaIdentifier: String? { + switch mediaType { + case .timelineItem(let uniqueID): + // Consider media for the same item to be the same view + uniqueID + default: // Binds the lifecycle of the LoadableImage to the associated URL. // This fixes the problem of the cache returning old values after a change in the URL. - .id(mediaSource.url) + mediaSource?.url.absoluteString + } } } @@ -194,7 +208,7 @@ private struct LoadableImageContent Void)? @@ -97,7 +97,7 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview { RoomAvatarImage(avatar: .room(id: "!2:server.com", name: "Room", - avatarURL: .picturesDirectory), + avatarURL: .mockMXCAvatar), avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init())) @@ -109,7 +109,7 @@ struct RoomAvatarImage_Previews: PreviewProvider, TestablePreview { RoomAvatarImage(avatar: .heroes([.init(userID: "@user:server.com", displayName: "User", - avatarURL: .picturesDirectory)]), + avatarURL: .mockMXCAvatar)]), avatarSize: .room(on: .home), mediaProvider: MediaProviderMock(configuration: .init())) diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift index c7291c4412..ff25c03dad 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift @@ -41,7 +41,7 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { RoomHeaderView(roomName: "Some Room name", roomAvatar: .room(id: "1", name: "Some Room Name", - avatarURL: URL.picturesDirectory), + avatarURL: .mockMXCAvatar), mediaProvider: MediaProviderMock(configuration: .init())) .previewLayout(.sizeThatFits) .padding() diff --git a/ElementX/Sources/Other/SwiftUI/Views/SFNumberedListView.swift b/ElementX/Sources/Other/SwiftUI/Views/SFNumberedListView.swift index b3e4e36026..c3f78573ad 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/SFNumberedListView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/SFNumberedListView.swift @@ -21,7 +21,7 @@ struct SFNumberedListView: View { Image(systemSymbol: getSymbol(for: index)) .imageScale(.large) .fontWeight(.light) - .foregroundColor(.compound.textPlaceholder) + .foregroundColor(.compound.textSecondary) } .foregroundColor(.compound.textPrimary) .font(.compound.bodyMD) diff --git a/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift new file mode 100644 index 0000000000..81db43cb88 --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.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 SwiftUI + +struct StackedAvatarInfo { + let url: URL? + let name: String? + let contentID: String +} + +struct StackedAvatarsView: View { + let overlap: CGFloat + let lineWidth: CGFloat + var shouldStackFromLast = false + let avatars: [StackedAvatarInfo] + let avatarSize: Avatars.Size + let mediaProvider: MediaProviderProtocol? + + var body: some View { + HStack(spacing: -overlap) { + ForEach(0.. = [] + var aliasErrorDescription: String? { + if aliasErrors.contains(.alreadyExists) { + return L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription + } else if aliasErrors.contains(.invalidSymbols) { + return L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription + } + return nil } } struct CreateRoomViewStateBindings { - var roomName: String var roomTopic: String var isRoomPrivate: Bool - var isKnockingOnly = false + var isKnockingOnly: Bool var showAttachmentConfirmationDialog = false /// Information describing the currently displayed alert. @@ -51,4 +63,11 @@ enum CreateRoomViewAction { case displayCameraPicker case displayMediaPicker case removeImage + case updateRoomName(String) + case updateAliasLocalPart(String) +} + +enum CreateRoomAliasErrorState { + case alreadyExists + case invalidSymbols } diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index da70c7afb2..8f9a5d8dc5 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -6,6 +6,7 @@ // import Combine +import MatrixRustSDK import SwiftUI typealias CreateRoomViewModelType = StateStoreViewModel @@ -15,6 +16,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private var createRoomParameters: CreateRoomFlowParameters private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol + private var syncNameAndAlias = true + @CancellableTask private var checkAliasAvailabilityTask: Task? private var actionsSubject: PassthroughSubject = .init() @@ -35,9 +38,17 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol self.analytics = analytics self.userIndicatorController = userIndicatorController - let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate) + let bindings = CreateRoomViewStateBindings(roomTopic: parameters.topic, + isRoomPrivate: parameters.isRoomPrivate, + isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false) - super.init(initialViewState: CreateRoomViewState(isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) + super.init(initialViewState: CreateRoomViewState(roomName: parameters.name, + serverName: userSession.clientProxy.userIDServerName ?? "", + isKnockingFeatureEnabled: appSettings.knockingEnabled, + selectedUsers: selectedUsers.value, + aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name), + bindings: bindings), + mediaProvider: userSession.mediaProvider) createRoomParameters .map(\.avatarImageMedia) @@ -80,31 +91,111 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol actionsSubject.send(.displayMediaPicker) case .removeImage: actionsSubject.send(.removeImage) + case .updateAliasLocalPart(let aliasLocalPart): + state.aliasLocalPart = aliasLocalPart.lowercased() + // If this has been called this means that the user wants a custom address not necessarily reflecting the name + // So we disable the two from syncing. + syncNameAndAlias = false + case .updateRoomName(let name): + // Reset the syncing if the name is fully cancelled + if name.isEmpty { + syncNameAndAlias = true + } + state.roomName = name + if syncNameAndAlias { + state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: name) + } } } // MARK: - Private private func setupBindings() { + // Reset the state related to public rooms if the user choses the room to be empty + context.$viewState + .dropFirst() + .map(\.bindings.isRoomPrivate) + .removeDuplicates() + .filter { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { return } + state.bindings.isKnockingOnly = false + state.aliasErrors = [] + state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: state.roomName) + syncNameAndAlias = true + } + .store(in: &cancellables) + context.$viewState - .map(\.bindings) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .removeDuplicates { old, new in - old.roomName == new.roomName && old.roomTopic == new.roomTopic && old.isRoomPrivate == new.isRoomPrivate + old.roomName == new.roomName && + old.bindings.roomTopic == new.bindings.roomTopic && + old.bindings.isRoomPrivate == new.bindings.isRoomPrivate && + old.bindings.isKnockingOnly == new.bindings.isKnockingOnly && + old.aliasLocalPart == new.aliasLocalPart } - .sink { [weak self] bindings in + .sink { [weak self] state in guard let self else { return } - updateParameters(bindings: bindings) + updateParameters(state: state) actionsSubject.send(.updateDetails(createRoomParameters)) } .store(in: &cancellables) + + context.$viewState + .map(\.aliasLocalPart) + .removeDuplicates() + .debounce(for: 1, scheduler: DispatchQueue.main) + .sink { [weak self] aliasLocalPart in + guard let self else { + return + } + + guard state.isKnockingFeatureEnabled, + !state.bindings.isRoomPrivate, + let canonicalAlias = canonicalAlias(aliasLocalPart: aliasLocalPart) else { + // While is empty or private room we don't change or display the error + return + } + + if !isRoomAliasFormatValid(alias: canonicalAlias) { + state.aliasErrors.insert(.invalidSymbols) + // If the alias is invalid we don't need to check for availability + state.aliasErrors.remove(.alreadyExists) + checkAliasAvailabilityTask = nil + return + } + + state.aliasErrors.remove(.invalidSymbols) + + checkAliasAvailabilityTask = Task { [weak self] in + guard let self else { + return + } + + if case .success(false) = await self.userSession.clientProxy.isAliasAvailable(canonicalAlias) { + guard !Task.isCancelled else { return } + state.aliasErrors.insert(.alreadyExists) + } else { + guard !Task.isCancelled else { return } + state.aliasErrors.remove(.alreadyExists) + } + } + } + .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 updateParameters(state: CreateRoomViewState) { + createRoomParameters.name = state.roomName + createRoomParameters.topic = state.bindings.roomTopic + createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate + createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly + if state.isKnockingFeatureEnabled, !state.aliasLocalPart.isEmpty { + createRoomParameters.aliasLocalPart = state.aliasLocalPart + } else { + createRoomParameters.aliasLocalPart = nil + } } private func createRoom() async { @@ -114,7 +205,28 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol showLoadingIndicator() // Since the parameters are throttled, we need to make sure that the latest values are used - updateParameters(bindings: state.bindings) + updateParameters(state: state) + + // Better to double check the errors also when trying to create the room + if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate { + guard let canonicalAlias = canonicalAlias(aliasLocalPart: createRoomParameters.aliasLocalPart), + isRoomAliasFormatValid(alias: canonicalAlias) else { + state.aliasErrors = [.invalidSymbols] + return + } + + switch await userSession.clientProxy.isAliasAvailable(canonicalAlias) { + case .success(true): + break + case .success(false): + state.aliasErrors = [.alreadyExists] + return + case .failure: + state.bindings.alertInfo = AlertInfo(id: .unknown) + return + } + } + let avatarURL: URL? if let media = createRoomParameters.avatarImageMedia { switch await userSession.clientProxy.uploadMedia(media) { @@ -147,7 +259,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol // 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) { + avatarURL: avatarURL, + aliasLocalPart: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.aliasLocalPart) { case .success(let roomId): analytics.trackCreatedRoom(isDM: false) actionsSubject.send(.openRoom(withIdentifier: roomId)) @@ -158,6 +271,14 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } } + func canonicalAlias(aliasLocalPart: String?) -> String? { + guard let aliasLocalPart, + !aliasLocalPart.isEmpty else { + return nil + } + return "#\(aliasLocalPart):\(state.serverName)" + } + // MARK: Loading indicator private static let loadingIndicatorIdentifier = "\(CreateRoomViewModel.self)-Loading" diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 726cf99f0e..22f8dbd78b 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -16,7 +16,23 @@ struct CreateRoomScreen: View { case name case topic } - + + private var aliasBinding: Binding { + .init(get: { + context.viewState.aliasLocalPart + }, set: { + context.send(viewAction: .updateAliasLocalPart($0)) + }) + } + + private var roomNameBinding: Binding { + .init(get: { + context.viewState.roomName + }, set: { + context.send(viewAction: .updateRoomName($0)) + }) + } + var body: some View { Form { roomSection @@ -25,6 +41,7 @@ struct CreateRoomScreen: View { if context.viewState.isKnockingFeatureEnabled, !context.isRoomPrivate { roomAccessSection + roomAliasSection } } .compoundList() @@ -48,8 +65,8 @@ struct CreateRoomScreen: View { .compoundListSectionHeader() TextField(L10n.screenCreateRoomRoomNameLabel, - text: $context.roomName, - prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textPlaceholder), + text: roomNameBinding, + prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textSecondary), axis: .horizontal) .focused($focus, equals: .name) .accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomName) @@ -150,22 +167,62 @@ struct CreateRoomScreen: View { iconAlignment: .top), kind: .selection(isSelected: !context.isRoomPrivate) { context.isRoomPrivate = false }) } header: { - Text(L10n.commonSecurity.uppercased()) + Text(L10n.screenCreateRoomRoomVisibilitySectionTitle) .compoundListSectionHeader() } } private var roomAccessSection: some View { Section { - ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionAnyoneOptionTitle, - description: L10n.screenCreateRoomAccessSectionAnyoneOptionDescription), + ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionTitle, + description: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionDescription), kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) - ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionKnockingOptionTitle, - description: L10n.screenCreateRoomAccessSectionKnockingOptionDescription), + ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionKnockingOptionTitle, + description: L10n.screenCreateRoomRoomAccessSectionKnockingOptionDescription), kind: .selection(isSelected: context.isKnockingOnly) { context.isKnockingOnly = true }) } header: { - Text(L10n.screenCreateRoomAccessSectionHeader.uppercased()) + Text(L10n.screenCreateRoomRoomAccessSectionHeader) + .compoundListSectionHeader() + } + } + + private var roomAliasSection: some View { + Section { + ListRow(kind: .custom { + HStack(spacing: 0) { + Text("#") + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + + TextField("", text: aliasBinding) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .tint(.compound.iconAccentTertiary) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + .padding(.horizontal, 8) + Text(":\(context.viewState.serverName)") + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + } + .padding(ListRowPadding.textFieldInsets) + .environment(\.layoutDirection, .leftToRight) + .errorBackground(!context.viewState.aliasErrors.isEmpty) + }) + } header: { + Text(L10n.screenCreateRoomRoomAddressSectionTitle) .compoundListSectionHeader() + } footer: { + VStack(alignment: .leading, spacing: 12) { + if let errorDescription = context.viewState.aliasErrorDescription { + Label(errorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + .foregroundStyle(.compound.textCriticalPrimary) + .font(.compound.bodySM) + } + Text(L10n.screenCreateRoomRoomAddressSectionFooter) + .compoundListSectionFooter() + .font(.compound.bodySM) + } } } @@ -180,6 +237,15 @@ struct CreateRoomScreen: View { } } +private extension View { + func errorBackground(_ shouldDisplay: Bool) -> some View { + listRowBackground(shouldDisplay ? AnyView(RoundedRectangle(cornerRadius: 10) + .inset(by: 1) + .fill(.compound.bgCriticalSubtleHovered) + .stroke(Color.compound.borderCriticalPrimary)) : AnyView(Color.compound.bgCanvasDefaultLevel1)) + } +} + // MARK: - Previews struct CreateRoom_Previews: PreviewProvider, TestablePreview { @@ -208,7 +274,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { }() static let publicRoomViewModel = { - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com")))) + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com")))) let parameters = CreateRoomFlowParameters(isRoomPrivate: false) let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true @@ -220,6 +286,32 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings) }() + static let publicRoomInvalidAliasViewModel = { + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com")))) + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "#:") + 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 let publicRoomExistingAliasViewModel = { + let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com")) + clientProxy.isAliasAvailableReturnValue = .success(false) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "existing") + 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 { NavigationStack { CreateRoomScreen(context: viewModel.context) @@ -233,5 +325,15 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { CreateRoomScreen(context: publicRoomViewModel.context) } .previewDisplayName("Create Public Room") + NavigationStack { + CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context) + } + .snapshotPreferences(delay: 1.5) + .previewDisplayName("Create Public Room, invalid alias") + NavigationStack { + CreateRoomScreen(context: publicRoomExistingAliasViewModel.context) + } + .snapshotPreferences(delay: 1.5) + .previewDisplayName("Create Public Room, existing alias") } } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift index 586a95647a..b6723a4b0a 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift @@ -30,7 +30,7 @@ enum GlobalSearchScreenViewAction { struct GlobalSearchRoom: Identifiable, Equatable { let id: String - let name: String - let alias: String? + let title: String + let description: String let avatar: RoomAvatar } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift index ed85554fe0..6ea45004a9 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift @@ -36,7 +36,11 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch .map(\.bindings.searchQuery) .removeDuplicates() .sink { [weak self] searchQuery in - self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } } .store(in: &cancellables) @@ -66,8 +70,8 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch private func updateRooms(with summaries: [RoomSummary]) { state.rooms = summaries.compactMap { summary in GlobalSearchRoom(id: summary.id, - name: summary.name, - alias: summary.canonicalAlias, + title: summary.name, + description: summary.roomListDescription, avatar: summary.avatar) } } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift index 6a9225facd..e16b0485dd 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift @@ -16,8 +16,8 @@ struct GlobalSearchScreenListRow: View { var body: some View { ZStack { // The list row swallows listRowBackgrounds for some reason - ListRow(label: .avatar(title: room.name, - description: room.alias ?? room.id, + ListRow(label: .avatar(title: room.title, + description: room.description, icon: avatar), kind: .label) } @@ -42,11 +42,11 @@ struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview { static var previews: some View { List { GlobalSearchScreenListRow(room: .init(id: "123", - name: "Tech central", - alias: "The best place in the whole wide world", + title: "Tech central", + description: "The best place in the whole wide world", avatar: .room(id: "123", name: "Tech central", - avatarURL: .picturesDirectory)), + avatarURL: .mockMXCAvatar)), context: viewModel.context) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 2e58c17ae7..7671b37748 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -66,10 +66,28 @@ enum HomeScreenRoomListMode: CustomStringConvertible { } } -enum HomeScreenBannerMode { +enum HomeScreenSecurityBannerMode: Equatable { case none case dismissed - case show + case show(HomeScreenRecoveryKeyConfirmationBanner.State) + + var isDismissed: Bool { + switch self { + case .dismissed: true + default: false + } + } + + var isShown: Bool { + switch self { + case .show: true + default: false + } + } +} + +enum HomeScreenMigrationBannerMode { + case none, show, dismissed } struct HomeScreenViewState: BindableState { @@ -77,8 +95,8 @@ struct HomeScreenViewState: BindableState { var userDisplayName: String? var userAvatarURL: URL? - var securityBannerMode = HomeScreenBannerMode.none - var slidingSyncMigrationBannerMode = HomeScreenBannerMode.none + var securityBannerMode = HomeScreenSecurityBannerMode.none + var slidingSyncMigrationBannerMode = HomeScreenMigrationBannerMode.none var requiresExtraAccountSetup = false diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index c1b21cf4f2..12313779a4 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -55,16 +55,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .sink { [weak self] securityState in guard let self else { return } - switch (securityState.verificationState, securityState.recoveryState) { - case (.verified, .disabled): + switch securityState.recoveryState { + case .disabled: state.requiresExtraAccountSetup = true - state.securityBannerMode = .show - case (.verified, .incomplete): - state.requiresExtraAccountSetup = true - - if state.securityBannerMode != .dismissed { - state.securityBannerMode = .show + if !state.securityBannerMode.isDismissed { + state.securityBannerMode = .show(.setUpRecovery) } + case .incomplete: + state.requiresExtraAccountSetup = true + state.securityBannerMode = .show(.recoveryOutOfSync) default: state.securityBannerMode = .none state.requiresExtraAccountSetup = false @@ -409,7 +408,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - switch await roomProxy.acceptInvitation() { + switch await userSession.clientProxy.joinRoom(roomID, via: []) { case .success: actionsSubject.send(.presentRoom(roomIdentifier: roomID)) analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index 68c7408b9c..969d1882a6 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -120,7 +120,7 @@ struct HomeScreenContent: View { private var topSection: some View { // An empty VStack causes glitches within the room list if context.viewState.shouldShowFilters || - context.viewState.securityBannerMode == .show || + context.viewState.securityBannerMode.isShown || context.viewState.slidingSyncMigrationBannerMode == .show { VStack(spacing: 0) { if context.viewState.shouldShowFilters { @@ -129,8 +129,8 @@ struct HomeScreenContent: View { if context.viewState.slidingSyncMigrationBannerMode == .show { HomeScreenSlidingSyncMigrationBanner(context: context) - } else if context.viewState.securityBannerMode == .show { - HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: context.viewState.requiresExtraAccountSetup, context: context) + } else if case let .show(state) = context.viewState.securityBannerMode { + HomeScreenRecoveryKeyConfirmationBanner(state: state, 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 e8ee14d769..db5c73a3ed 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -73,7 +73,7 @@ struct HomeScreenInviteCell: View { !room.isDirect { RoomInviterLabel(inviter: inviter, mediaProvider: context.mediaProvider) .font(.compound.bodyMD) - .foregroundStyle(.compound.textPlaceholder) + .foregroundStyle(.compound.textSecondary) } } @@ -90,7 +90,7 @@ struct HomeScreenInviteCell: View { if let subtitle { Text(subtitle) .font(.compound.bodyMD) - .foregroundColor(.compound.textPlaceholder) + .foregroundColor(.compound.textSecondary) } #endif } @@ -148,7 +148,7 @@ struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview { HomeScreenInviteCell(room: .roomInvite(), context: viewModel().context) - HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org", avatarURL: .picturesDirectory), + HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar), context: viewModel().context) HomeScreenInviteCell(room: .roomInvite(alias: "#footest:somewhere.org"), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift index a1cdf9bfb4..2a79f62e18 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift @@ -56,7 +56,7 @@ struct HomeScreenKnockedCell: View { Text(L10n.screenRoomlistKnockEventSentDescription) .font(.compound.bodyMD) - .foregroundStyle(.compound.textPlaceholder) + .foregroundStyle(.compound.textSecondary) .padding(.top, room.canonicalAlias == nil ? 0 : 4) .padding(.trailing, 16) } @@ -76,7 +76,7 @@ struct HomeScreenKnockedCell: View { if let subtitle { Text(subtitle) .font(.compound.bodyMD) - .foregroundColor(.compound.textPlaceholder) + .foregroundColor(.compound.textSecondary) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -119,7 +119,7 @@ struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview { HomeScreenKnockedCell(room: .roomKnocked(), context: viewModel().context) - HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .picturesDirectory), + HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .mockMXCAvatar), context: viewModel().context) HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org"), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift index 1adf1fac7b..3442762890 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift @@ -10,13 +10,37 @@ import Compound import SwiftUI struct HomeScreenRecoveryKeyConfirmationBanner: View { - let requiresExtraAccountSetup: Bool + enum State { case setUpRecovery, recoveryOutOfSync } + let state: State 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 title: String { + switch state { + case .setUpRecovery: L10n.bannerSetUpRecoveryTitle + case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerTitle + } + } + + var message: String { + switch state { + case .setUpRecovery: L10n.bannerSetUpRecoveryContent + case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerMessage + } + } + + var actionTitle: String { + switch state { + case .setUpRecovery: L10n.bannerSetUpRecoverySubmit + case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerPrimaryButtonTitle + } + } + + var primaryAction: HomeScreenViewAction { + switch state { + case .setUpRecovery: .setupRecovery + case .recoveryOutOfSync: .confirmRecoveryKey + } + } var body: some View { VStack(spacing: 16) { @@ -37,7 +61,7 @@ struct HomeScreenRecoveryKeyConfirmationBanner: View { .foregroundColor(.compound.textPrimary) .frame(maxWidth: .infinity, alignment: .leading) - if requiresExtraAccountSetup { + if state == .setUpRecovery { Button { context.send(viewAction: .skipRecoveryKeyConfirmation) } label: { @@ -63,7 +87,7 @@ struct HomeScreenRecoveryKeyConfirmationBanner: View { .buttonStyle(.compound(.primary, size: .medium)) .accessibilityIdentifier(A11yIdentifiers.homeScreen.recoveryKeyConfirmationBannerContinue) - if !requiresExtraAccountSetup { + if state == .recoveryOutOfSync { Button { context.send(viewAction: .resetEncryption) } label: { @@ -81,10 +105,10 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab static let viewModel = buildViewModel() static var previews: some View { - HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: true, + HomeScreenRecoveryKeyConfirmationBanner(state: .setUpRecovery, context: viewModel.context) .previewDisplayName("Set up recovery") - HomeScreenRecoveryKeyConfirmationBanner(requiresExtraAccountSetup: false, + HomeScreenRecoveryKeyConfirmationBanner(state: .recoveryOutOfSync, context: viewModel.context) .previewDisplayName("Out of sync") } diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift index 2d88bfb14e..6312924698 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift @@ -102,27 +102,27 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo } private func updateRoomDetails() { - var roomInfo: RoomInfoProxy? + var roomPreviewInfo: BaseRoomInfoProxyProtocol? var inviter: RoomInviterDetails? switch room { case .joined(let joinedRoomProxy): - roomInfo = joinedRoomProxy.infoPublisher.value + roomPreviewInfo = joinedRoomProxy.infoPublisher.value case .invited(let invitedRoomProxy): - inviter = invitedRoomProxy.info.inviter.flatMap(RoomInviterDetails.init) - roomInfo = invitedRoomProxy.info + inviter = invitedRoomProxy.inviter.map(RoomInviterDetails.init) + roomPreviewInfo = invitedRoomProxy.info case .knocked(let knockedRoomProxy): - roomInfo = knockedRoomProxy.info + roomPreviewInfo = knockedRoomProxy.info default: break } - let name = roomInfo?.displayName ?? roomPreviewDetails?.name + let name = roomPreviewInfo?.displayName ?? roomPreviewDetails?.name state.roomDetails = JoinRoomScreenRoomDetails(name: name, - topic: roomInfo?.topic ?? roomPreviewDetails?.topic, - canonicalAlias: roomInfo?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias, - avatar: roomInfo?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL), - memberCount: UInt(roomInfo?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)), + topic: roomPreviewInfo?.topic ?? roomPreviewDetails?.topic, + canonicalAlias: roomPreviewInfo?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias, + avatar: roomPreviewInfo?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL), + memberCount: UInt(roomPreviewInfo?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)), inviter: inviter) updateMode() diff --git a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift index f47e63c632..3953ed96c6 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift @@ -9,6 +9,7 @@ import Compound import SwiftUI struct JoinRoomScreen: View { + private let maxKnockMessageLength = 500 @Environment(\.dynamicTypeSize) private var dynamicTypeSize @ObservedObject var context: JoinRoomScreenViewModel.Context @@ -103,6 +104,13 @@ struct JoinRoomScreen: View { } } } + + private var knockMessageFooterString: String { + guard !context.knockMessage.isEmpty else { + return L10n.screenJoinRoomKnockMessageDescription + } + return "\(context.knockMessage.count)/\(maxKnockMessageLength)" + } @ViewBuilder private var knockMessage: some View { @@ -110,7 +118,7 @@ struct JoinRoomScreen: View { HStack(spacing: 0) { TextField("", text: $context.knockMessage, axis: .vertical) .onChange(of: context.knockMessage) { _, newValue in - context.knockMessage = String(newValue.prefix(1000)) + context.knockMessage = String(newValue.prefix(maxKnockMessageLength)) } .lineLimit(4, reservesSpace: true) .font(.compound.bodyMD) @@ -125,8 +133,8 @@ struct JoinRoomScreen: View { .stroke(.compound.borderInteractivePrimary) } - Text(L10n.screenJoinRoomKnockMessageDescription) - .font(.compound.bodyMD) + Text(knockMessageFooterString) + .font(.compound.bodySM) .foregroundStyle(.compound.textSecondary) } } @@ -237,11 +245,11 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview { switch mode { case .knocked: clientProxy.roomForIdentifierClosure = { _ in - .knocked(KnockedRoomProxyMock(.init(avatarURL: URL.homeDirectory))) + .knocked(KnockedRoomProxyMock(.init(avatarURL: .mockMXCAvatar))) } case .invited: clientProxy.roomForIdentifierClosure = { _ in - .invited(InvitedRoomProxyMock(.init(avatarURL: URL.homeDirectory))) + .invited(InvitedRoomProxyMock(.init(avatarURL: .mockMXCAvatar))) } default: break @@ -251,7 +259,7 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview { canonicalAlias: "#3🌞problem:matrix.org", // swiftlint:disable:next line_length topic: "“Science and technology were the only keys to opening the door to the future, and people approached science with the faith and sincerity of elementary school students.”", - avatarURL: URL.homeDirectory, + avatarURL: .mockMXCAvatar, memberCount: UInt(100), isHistoryWorldReadable: false, isJoined: membership.isJoined, diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift new file mode 100644 index 0000000000..5d14686abb --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift @@ -0,0 +1,40 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +// periphery:ignore:all - this is just a knockRequestsList remove this comment once generating the final file + +import Combine +import SwiftUI + +struct KnockRequestsListScreenCoordinatorParameters { + let roomProxy: JoinedRoomProxyProtocol + let mediaProvider: MediaProviderProtocol +} + +enum KnockRequestsListScreenCoordinatorAction { } + +final class KnockRequestsListScreenCoordinator: CoordinatorProtocol { + private let viewModel: KnockRequestsListScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: KnockRequestsListScreenCoordinatorParameters) { + viewModel = KnockRequestsListScreenViewModel(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider) + } + + func start() { } + + func toPresentable() -> AnyView { + AnyView(KnockRequestsListScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift new file mode 100644 index 0000000000..5747aff5f9 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift @@ -0,0 +1,45 @@ +// +// 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 + +enum KnockRequestsListScreenViewModelAction { } + +struct KnockRequestsListScreenViewState: BindableState { + // TODO: Not sure yet how we will fetch this, this is just for testing purposes + var requests: [KnockRequestCellInfo] = [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello")] + // If you are in this view one of these must have been true so by default we assume all of them to be true + var canAccept = true + var canDecline = true + var canBan = true + var isKnockableRoom = true + + // If all the permissions are denied or the join rule changes while we are in the view + // we want to stop displaying any request + var shouldDisplayRequests: Bool { + !requests.isEmpty && isKnockableRoom && (canAccept || canDecline || canBan) + } + + var bindings = KnockRequestsListStateBindings() +} + +struct KnockRequestsListStateBindings { + var alertInfo: AlertInfo? +} + +enum KnockRequestsListAlertType { + case acceptAllRequests + case declineRequest + case declineAndBan +} + +enum KnockRequestsListScreenViewAction { + case acceptAllRequests + case acceptRequest(userID: String) + case declineRequest(userID: String) + case ban(userID: String) +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift new file mode 100644 index 0000000000..391cc60a95 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift @@ -0,0 +1,107 @@ +// +// 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 SwiftUI + +typealias KnockRequestsListScreenViewModelType = StateStoreViewModel + +class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, KnockRequestsListScreenViewModelProtocol { + private let roomProxy: JoinedRoomProxyProtocol + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol) { + self.roomProxy = roomProxy + super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider) + + updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) + Task { + await updatePermissions() + } + + setupSubscriptions() + } + + // MARK: - Public + + override func process(viewAction: KnockRequestsListScreenViewAction) { + switch viewAction { + case .acceptAllRequests: + state.bindings.alertInfo = .init(id: .acceptAllRequests, + title: L10n.screenKnockRequestsListAcceptAllAlertTitle, + message: L10n.screenKnockRequestsListAcceptAllAlertDescription, + primaryButton: .init(title: L10n.screenKnockRequestsListAcceptAllAlertConfirmButtonTitle, + // TODO: Implement action + action: nil), + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + case .acceptRequest(let userID): + // TODO: Implement + break + case .declineRequest(let userID): + state.bindings.alertInfo = .init(id: .declineRequest, + title: L10n.screenKnockRequestsListDeclineAlertTitle, + message: L10n.screenKnockRequestsListDeclineAlertDescription(userID), + primaryButton: .init(title: L10n.screenKnockRequestsListDeclineAlertConfirmButtonTitle, + role: .destructive, + // TODO: Implement action + action: nil), + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + case .ban(let userID): + state.bindings.alertInfo = .init(id: .declineAndBan, + title: L10n.screenKnockRequestsListBanAlertTitle, + message: L10n.screenKnockRequestsListBanAlertDescription(userID), + // TODO: Implement action + primaryButton: .init(title: L10n.screenKnockRequestsListBanAlertConfirmButtonTitle, + role: .destructive, + action: nil), + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + } + + // MARK: - Private + + private func setupSubscriptions() { + roomProxy.infoPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] roomInfo in + self?.updateRoomInfo(roomInfo: roomInfo) + Task { await self?.updatePermissions() } + } + .store(in: &cancellables) + } + + private func updateRoomInfo(roomInfo: RoomInfoProxy) { + switch roomInfo.joinRule { + case .knock, .knockRestricted: + state.isKnockableRoom = true + default: + state.isKnockableRoom = false + } + } + + private func updatePermissions() async { + state.canAccept = await (try? roomProxy.canUserInvite(userID: roomProxy.ownUserID).get()) == true + state.canDecline = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canBan = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true + } + + // For testing purposes + private init(initialViewState: KnockRequestsListScreenViewState) { + roomProxy = JoinedRoomProxyMock(.init()) + super.init(initialViewState: initialViewState) + } +} + +extension KnockRequestsListScreenViewModel { + static func mockWithInitialState(_ initialViewState: KnockRequestsListScreenViewState) -> KnockRequestsListScreenViewModel { + .init(initialViewState: initialViewState) + } +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModelProtocol.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModelProtocol.swift new file mode 100644 index 0000000000..c6ca4a7119 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// 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 + +@MainActor +protocol KnockRequestsListScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: KnockRequestsListScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestCell.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestCell.swift new file mode 100644 index 0000000000..db6962d1f1 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestCell.swift @@ -0,0 +1,195 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct KnockRequestCellInfo: Identifiable { + /// user identifier of the usee that sent the request + let id: String + let displayName: String? + let avatarURL: URL? + let timestamp: String? + let reason: String? +} + +struct KnockRequestCell: View { + let cellInfo: KnockRequestCellInfo + var mediaProvider: MediaProviderProtocol? + let onAccept: ((String) -> Void)? + let onDecline: ((String) -> Void)? + let onDeclineAndBan: ((String) -> Void)? + + var body: some View { + HStack(alignment: .top, spacing: 16) { + LoadableAvatarImage(url: cellInfo.avatarURL, + name: cellInfo.displayName, + contentID: cellInfo.id, + avatarSize: .user(on: .knockingUserList), + mediaProvider: mediaProvider) + VStack(alignment: .leading, spacing: 12) { + header + if let reason = cellInfo.reason { + DisclosableText(text: reason) + } + actions + } + .padding(.trailing, 16) + .overlay(alignment: .bottom) { + // Custom separator that uses the same color from the compound one + Color.compound._borderInteractiveSecondaryAlpha + .frame(height: 0.5) + } + } + .padding(.top, 16) + .padding(.leading, 16) + .background(.compound.bgCanvasDefault) + } + + private var header: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .top, spacing: 0) { + Text(cellInfo.displayName ?? cellInfo.id) + .font(.compound.bodyLGSemibold) + .foregroundStyle(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + if let timestamp = cellInfo.timestamp { + Text(timestamp) + .font(.compound.bodySM) + .foregroundStyle(.compound.textSecondary) + } + } + if cellInfo.displayName != nil { + Text(cellInfo.id) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + } + } + } + + @ViewBuilder + private var actions: some View { + VStack(spacing: 0) { + if onDecline != nil || onAccept != nil { + HStack(spacing: 16) { + if let onDecline { + Button(L10n.actionDecline) { + onDecline(cellInfo.id) + } + .buttonStyle(.compound(.secondary, size: .medium)) + } + + if let onAccept { + Button(L10n.actionAccept) { + onAccept(cellInfo.id) + } + .buttonStyle(.compound(.primary, size: .medium)) + } + } + } + + if let onDeclineAndBan { + Button(role: .destructive) { + onDeclineAndBan(cellInfo.id) + } label: { + Text(L10n.screenKnockRequestsListDeclineAndBanActionTitle) + .padding(.top, 8) + .padding(.bottom, 4) + } + .frame(maxWidth: .infinity) + .buttonStyle(.compound(.plain)) + .padding(.top, 16) + } + } + .padding(.bottom, 16) + } +} + +private struct DisclosableText: View { + let text: String + @State private var collapsedHeight = CGFloat.zero + @State private var expandedHeight = CGFloat.zero + @State private var isExpanded = false + + var body: some View { + HStack(alignment: .top, spacing: 4) { + Text(text) + .multilineTextAlignment(.leading) + .lineLimit(isExpanded ? nil : 3) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .onGeometryChange(for: CGFloat.self) { geometry in + geometry.size.height + } action: { newValue in + if !isExpanded { + collapsedHeight = newValue + } + } + .background { + Text(text) + .multilineTextAlignment(.leading) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: CGFloat.self) { geometry in + geometry.size.height + } action: { newValue in + expandedHeight = newValue + } + .hidden() + } + Button { + withAnimation { + isExpanded.toggle() + } + } label: { + CompoundIcon(\.chevronDown, size: .medium, relativeTo: .compound.bodyMD) + .foregroundStyle(.compound.iconTertiary) + .rotationEffect(.degrees(isExpanded ? 180 : 0)) + } + .buttonStyle(.plain) + .opacity(collapsedHeight < expandedHeight ? 1 : 0) + .disabled(collapsedHeight >= expandedHeight) + } + } +} + +struct KnockRequestCell_Previews: PreviewProvider, TestablePreview { + // swiftlint:disable:next line_length + static let aliceWithLongReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello would like to join this room, also this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason") + + static let aliceWithShortReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello, I am Alice and would like to join this room, please") + + static let aliceWithNoReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: nil) + + static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil) + + static var previews: some View { + KnockRequestCell(cellInfo: aliceWithLongReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) + .previewDisplayName("Long reason") + KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) + .previewDisplayName("Short reason") + KnockRequestCell(cellInfo: aliceWithNoReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) + .previewDisplayName("No reason") + KnockRequestCell(cellInfo: aliceWithNoName, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: { _ in }) + .previewDisplayName("No name") + KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: nil, onDecline: { _ in }, onDeclineAndBan: { _ in }) + .previewDisplayName("No Accept") + KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: nil, onDecline: nil, onDeclineAndBan: { _ in }) + .previewDisplayName("No Accept and Decline") + KnockRequestCell(cellInfo: aliceWithShortReason, onAccept: { _ in }, onDecline: { _ in }, onDeclineAndBan: nil) + .previewDisplayName("No Ban") + } +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListEmptyStateView.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListEmptyStateView.swift new file mode 100644 index 0000000000..6f168e67c0 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListEmptyStateView.swift @@ -0,0 +1,38 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct KnockRequestsListEmptyStateView: View { + var body: some View { + VStack(spacing: 16) { + BigIcon(icon: \.askToJoin) + VStack(spacing: 8) { + Text(L10n.screenKnockRequestsListEmptyStateTitle) + .multilineTextAlignment(.center) + .font(.compound.headingMDBold) + .foregroundStyle(.compound.textPrimary) + Text(L10n.screenKnockRequestsListEmptyStateDescription) + .multilineTextAlignment(.center) + .foregroundStyle(.compound.textSecondary) + .font(.compound.bodyMD) + } + Spacer() + } + .padding(.top, 53) + .padding(.horizontal, 40) + } +} + +// MARK: - Previews + +struct KnockRequestsListEmptyStateView_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + KnockRequestsListEmptyStateView() + } +} diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift new file mode 100644 index 0000000000..d3bc5915a4 --- /dev/null +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/View/KnockRequestsListScreen.swift @@ -0,0 +1,96 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct KnockRequestsListScreen: View { + @ObservedObject var context: KnockRequestsListScreenViewModel.Context + + var body: some View { + mainContent + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(L10n.screenKnockRequestsListTitle) + .background(.compound.bgCanvasDefault) + .overlay { + if !context.viewState.shouldDisplayRequests { + KnockRequestsListEmptyStateView() + } + } + .safeAreaInset(edge: .bottom) { + if context.viewState.shouldDisplayRequests { + acceptAllButton + } + } + .alert(item: $context.alertInfo) + } + + @ViewBuilder + private var mainContent: some View { + ScrollView { + LazyVStack(spacing: 0) { + if context.viewState.shouldDisplayRequests { + ForEach(context.viewState.requests) { requestInfo in + ListRow(kind: .custom { + KnockRequestCell(cellInfo: requestInfo, + mediaProvider: context.mediaProvider, + onAccept: context.viewState.canAccept ? onAccept : nil, + onDecline: context.viewState.canDecline ? onDecline : nil, + onDeclineAndBan: context.viewState.canBan ? onDeclineAndBan : nil) + }) + } + } + } + .padding(.top, 40) + } + } + + private var acceptAllButton: some View { + Button(L10n.screenKnockRequestsListAcceptAllButtonTitle) { + context.send(viewAction: .acceptAllRequests) + } + .buttonStyle(.compound(.secondary)) + .padding(.horizontal, 16) + .padding(.top, 16) + .padding(.bottom, 4) + .background(.compound.bgCanvasDefault) + } + + private func onAccept(userID: String) { + context.send(viewAction: .acceptRequest(userID: userID)) + } + + private func onDecline(userID: String) { + context.send(viewAction: .declineRequest(userID: userID)) + } + + private func onDeclineAndBan(userID: String) { + context.send(viewAction: .ban(userID: userID)) + } +} + +// MARK: - Previews + +struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview { + static let emptyViewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: [])) + + static let viewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello"), + // swiftlint:disable:next line_length + .init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason"), + .init(id: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil, timestamp: "Now", reason: nil), + .init(id: "@dan:matrix.org", displayName: "Dan", avatarURL: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")])) + + static var previews: some View { + NavigationStack { + KnockRequestsListScreen(context: viewModel.context) + } + NavigationStack { + KnockRequestsListScreen(context: emptyViewModel.context) + } + .previewDisplayName("Empty state") + } +} diff --git a/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift b/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift index cdb9cace9f..c401dc2e4f 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift @@ -25,11 +25,7 @@ private struct PreviewView: UIViewControllerRepresentable { previewController.dataSource = context.coordinator previewController.delegate = context.coordinator - if ProcessInfo.processInfo.isiOSAppOnMac { - return previewController - } else { - return UINavigationController(rootViewController: previewController) - } + return UINavigationController(rootViewController: previewController) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } diff --git a/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift b/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift index c1b1a4ad34..377d43a490 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift @@ -69,7 +69,7 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable { photoLibraryPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) } - provider.loadFileRepresentation(forTypeIdentifier: contentType.identifier) { [weak self] url, error in + provider.loadFileRepresentation(forTypeIdentifier: contentType.type.identifier) { [weak self] url, error in guard let url else { Task { @MainActor in self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error))) diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift index 42633e9b54..4d2610314b 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift @@ -14,6 +14,7 @@ struct MediaUploadPreviewScreenCoordinatorParameters { let mediaUploadingPreprocessor: MediaUploadingPreprocessor let title: String? let url: URL + let shouldShowCaptionWarning: Bool } enum MediaUploadPreviewScreenCoordinatorAction { @@ -34,7 +35,8 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy, mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, title: parameters.title, - url: parameters.url) + url: parameters.url, + shouldShowCaptionWarning: parameters.shouldShowCaptionWarning) } func start() { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift index c471ab42a1..b3568c35c4 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift @@ -14,7 +14,17 @@ enum MediaUploadPreviewScreenViewModelAction { struct MediaUploadPreviewScreenViewState: BindableState { let url: URL let title: String? + let shouldShowCaptionWarning: Bool var shouldDisableInteraction = false + + var bindings = MediaUploadPreviewScreenBindings() +} + +struct MediaUploadPreviewScreenBindings: BindableState { + var caption = NSAttributedString() + var presendCallback: (() -> Void)? + + var isPresentingMediaCaptionWarning = false } enum MediaUploadPreviewScreenViewAction { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index d1610f673f..f3741f6e0b 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -32,26 +32,28 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, roomProxy: JoinedRoomProxyProtocol, mediaUploadingPreprocessor: MediaUploadingPreprocessor, title: String?, - url: URL) { + url: URL, + shouldShowCaptionWarning: Bool) { self.userIndicatorController = userIndicatorController self.roomProxy = roomProxy self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.url = url - super.init(initialViewState: MediaUploadPreviewScreenViewState(url: url, title: title)) + super.init(initialViewState: MediaUploadPreviewScreenViewState(url: url, title: title, shouldShowCaptionWarning: shouldShowCaptionWarning)) } override func process(viewAction: MediaUploadPreviewScreenViewAction) { + // Get the current caption before all the processing starts. + let caption = state.bindings.caption.nonBlankString + switch viewAction { case .send: Task { - let progressSubject = CurrentValueSubject(0.0) - - startLoading(progressPublisher: progressSubject.asCurrentValuePublisher()) + startLoading() switch await mediaUploadingPreprocessor.processMedia(at: url) { case .success(let mediaInfo): - switch await sendAttachment(mediaInfo: mediaInfo, progressSubject: progressSubject) { + switch await sendAttachment(mediaInfo: mediaInfo, caption: caption) { case .success: actionsSubject.send(.dismiss) case .failure(let error): @@ -75,29 +77,43 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, // MARK: - Private - private func sendAttachment(mediaInfo: MediaInfo, progressSubject: CurrentValueSubject) async -> Result { + private func sendAttachment(mediaInfo: MediaInfo, caption: String?) async -> Result { let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in self?.requestHandle = handle } switch mediaInfo { case let .image(imageURL, thumbnailURL, imageInfo): - return await roomProxy.timeline.sendImage(url: imageURL, thumbnailURL: thumbnailURL, imageInfo: imageInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendImage(url: imageURL, + thumbnailURL: thumbnailURL, + imageInfo: imageInfo, + caption: caption, + requestHandle: requestHandle) case let .video(videoURL, thumbnailURL, videoInfo): - return await roomProxy.timeline.sendVideo(url: videoURL, thumbnailURL: thumbnailURL, videoInfo: videoInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendVideo(url: videoURL, + thumbnailURL: thumbnailURL, + videoInfo: videoInfo, + caption: caption, + requestHandle: requestHandle) case let .audio(audioURL, audioInfo): - return await roomProxy.timeline.sendAudio(url: audioURL, audioInfo: audioInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendAudio(url: audioURL, + audioInfo: audioInfo, + caption: caption, + requestHandle: requestHandle) case let .file(fileURL, fileInfo): - return await roomProxy.timeline.sendFile(url: fileURL, fileInfo: fileInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendFile(url: fileURL, + fileInfo: fileInfo, + caption: caption, + requestHandle: requestHandle) } } private static let loadingIndicatorIdentifier = "\(MediaUploadPreviewScreenViewModel.self)-Loading" - private func startLoading(progressPublisher: CurrentValuePublisher) { + private func startLoading() { userIndicatorController.submitIndicator( UserIndicator(id: Self.loadingIndicatorIdentifier, - type: .modal(progress: .published(progressPublisher), interactiveDismissDisabled: false, allowsInteraction: true), + type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), title: L10n.commonSending, persistent: true) ) @@ -112,3 +128,10 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, userIndicatorController.submitIndicator(UserIndicator(title: label)) } } + +extension NSAttributedString { + var nonBlankString: String? { + guard !string.isBlank else { return nil } + return string + } +} diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index 30db52e56c..17ba7c9be4 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -5,25 +5,37 @@ // Please see LICENSE in the repository root for full details. // +import Compound import QuickLook import SwiftUI struct MediaUploadPreviewScreen: View { + @Environment(\.colorScheme) private var colorScheme + @ObservedObject var context: MediaUploadPreviewScreenViewModel.Context - var title: String { - ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" - } + @State private var captionWarningFrame: CGRect = .zero + + private var title: String { ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" } + private var colorSchemeOverride: ColorScheme { ProcessInfo.processInfo.isiOSAppOnMac ? colorScheme : .dark } var body: some View { mainContent - .id(UUID()) + .id(context.viewState.url) + .ignoresSafeArea(edges: [.horizontal]) + .safeAreaInset(edge: .bottom, spacing: 0) { + composer + .padding(.horizontal, 12) + .padding(.vertical, 16) + .background() // Don't use compound so we match the QLPreviewController. + } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) - .disabled(context.viewState.shouldDisableInteraction) - .ignoresSafeArea(edges: [.horizontal, .bottom]) .toolbar { toolbar } + .disabled(context.viewState.shouldDisableInteraction) .interactiveDismissDisabled() + .presentationBackground(.background) // Fix a bug introduced by the caption warning. + .preferredColorScheme(colorSchemeOverride) } @ViewBuilder @@ -32,24 +44,85 @@ struct MediaUploadPreviewScreen: View { Text(title) .font(.compound.headingMD) .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { PreviewView(fileURL: context.viewState.url, title: context.viewState.title) } } + private var composer: some View { + HStack(spacing: 12) { + HStack(spacing: 6) { + MessageComposerTextField(placeholder: L10n.richTextEditorComposerCaptionPlaceholder, + text: $context.caption, + presendCallback: $context.presendCallback, + maxHeight: ComposerConstant.maxHeight, + keyHandler: { _ in }, + pasteHandler: { _ in }) + + if context.viewState.shouldShowCaptionWarning { + captionWarningButton + } + } + .messageComposerStyle() + + SendButton { + context.send(viewAction: .send) + } + } + } + + private var captionWarningButton: some View { + Button { + context.isPresentingMediaCaptionWarning = true + } label: { + CompoundIcon(\.infoSolid, size: .xSmall, relativeTo: .compound.bodyLG) + } + .tint(.compound.iconCriticalPrimary) + .popover(isPresented: $context.isPresentingMediaCaptionWarning, arrowEdge: .bottom) { + captionWarningContent + .presentationDetents([.height(captionWarningFrame.height)]) + .presentationDragIndicator(.visible) + .padding(.top, 19) // For the drag indicator + .presentationBackground(.compound.bgCanvasDefault) + .preferredColorScheme(colorSchemeOverride) + } + } + + var captionWarningContent: some View { + VStack(spacing: 0) { + VStack(spacing: 16) { + BigIcon(icon: \.infoSolid, style: .alertSolid) + + Text(L10n.screenMediaUploadPreviewCaptionWarning) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + .padding(24) + .padding(.bottom, 8) + + Button(L10n.actionOk) { + context.isPresentingMediaCaptionWarning = false + } + .buttonStyle(.compound(.secondary)) + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .readFrame($captionWarningFrame) + } + @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { Button { context.send(viewAction: .cancel) } label: { Text(L10n.actionCancel) } - } - ToolbarItem(placement: .confirmationAction) { - Button { context.send(viewAction: .send) } label: { - Text(L10n.actionSend) - } - .disabled(context.viewState.shouldDisableInteraction) + // Fix a bug with the preferredColorScheme on iOS 18 where the button doesn't + // follow the dark colour scheme on devices running with dark mode disabled. + .tint(.compound.textActionPrimary) } } } @@ -111,29 +184,37 @@ private class PreviewItem: NSObject, QLPreviewItem { } } +private class PreviewViewController: QLPreviewController { + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + // Remove top file details bar + navigationController?.navigationBar.isHidden = true + + // Hide toolbar share button + toolbarItems?.first?.isHidden = true + } +} + // MARK: - Previews struct MediaUploadPreviewScreen_Previews: PreviewProvider, TestablePreview { + static let snapshotURL = URL.picturesDirectory + static let testURL = Bundle.main.url(forResource: "AppIcon60x60@2x", withExtension: "png") + static let viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: UserIndicatorControllerMock.default, roomProxy: JoinedRoomProxyMock(), mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), - title: "some random file name", - url: URL.picturesDirectory) + title: "App Icon.png", + url: snapshotURL, + shouldShowCaptionWarning: true) static var previews: some View { NavigationStack { MediaUploadPreviewScreen(context: viewModel.context) } - } -} - -private class PreviewViewController: QLPreviewController { - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - // Remove top file details bar - navigationController?.navigationBar.isHidden = true - - // Hide toolbar share button - toolbarItems?.first?.isHidden = true + MediaUploadPreviewScreen(context: viewModel.context) + .captionWarningContent + .previewDisplayName("Caption warning") } } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift index 0a459a3f6e..cf0280f96d 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift @@ -33,8 +33,8 @@ enum MessageForwardingScreenViewAction { struct MessageForwardingRoom: Identifiable, Equatable { let id: String - let name: String - let alias: String? + let title: String + let description: String let avatar: RoomAvatar } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index ef27e2a2ae..d9ed472c12 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -45,8 +45,11 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me .map(\.bindings.searchQuery) .removeDuplicates() .sink { [weak self] searchQuery in - guard let self else { return } - self.roomSummaryProvider.setFilter(.search(query: searchQuery)) + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } } .store(in: &cancellables) @@ -79,8 +82,10 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me continue } - let room = MessageForwardingRoom(id: summary.id, name: summary.name, alias: summary.canonicalAlias, avatar: summary.avatar) - rooms.append(room) + rooms.append(.init(id: summary.id, + title: summary.name, + description: summary.roomListDescription, + avatar: summary.avatar)) } state.rooms = rooms diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift index 2801f66ef2..552c6c8be7 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift @@ -68,8 +68,8 @@ private struct MessageForwardingListRow: View { let context: MessageForwardingScreenViewModel.Context var body: some View { - ListRow(label: .avatar(title: room.name, - description: room.alias ?? room.id, + ListRow(label: .avatar(title: room.title, + description: room.description, icon: avatar), kind: .selection(isSelected: isSelected) { context.send(viewAction: .selectRoom(roomID: room.id)) diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift index ec7d72b44f..12424e3dce 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift @@ -5,7 +5,8 @@ // Please see LICENSE in the repository root for full details. // -import Foundation +import Compound +import SwiftUI enum SessionVerificationScreenViewModelAction { case finished @@ -26,32 +27,32 @@ struct SessionVerificationScreenViewState: BindableState { let flow: SessionVerificationScreenFlow var verificationState: SessionVerificationScreenStateMachine.State - var headerImageName: String { + var headerIcon: (keyPath: KeyPath, style: BigIcon.Style) { switch verificationState { case .initial: - return "lock" + return (\.devices, .defaultSolid) case .acceptingVerificationRequest: - return "hourglass" + return (\.devices, .defaultSolid) case .requestingVerification: - return "hourglass" + return (\.devices, .defaultSolid) case .verificationRequestAccepted: - return "face.smiling" + return (\.reaction, .defaultSolid) case .startingSasVerification: - return "hourglass" + return (\.devices, .defaultSolid) case .sasVerificationStarted: - return "hourglass" + return (\.devices, .defaultSolid) case .cancelling: - return "hourglass" + return (\.lockSolid, .defaultSolid) + case .showingChallenge: + return (\.reaction, .defaultSolid) case .acceptingChallenge: - return "hourglass" + return (\.reaction, .defaultSolid) case .decliningChallenge: - return "hourglass" - case .showingChallenge: - return "face.smiling" + return (\.reaction, .defaultSolid) case .verified: - return "checkmark.shield" + return (\.checkCircleSolid, .successSolid) case .cancelled: - return "exclamationmark.shield" + return (\.infoSolid, .alertSolid) } } @@ -60,12 +61,12 @@ struct SessionVerificationScreenViewState: BindableState { case .initial: switch flow { case .initiator: - return L10n.screenSessionVerificationOpenExistingSessionTitle + return L10n.screenSessionVerificationUseAnotherDeviceTitle case .responder: return L10n.screenSessionVerificationRequestTitle } case .acceptingVerificationRequest: - return L10n.screenSessionVerificationRequestTitle + return L10n.screenSessionVerificationWaitingAnotherDeviceTitle case .requestingVerification: return L10n.screenSessionVerificationWaitingToAcceptTitle case .verificationRequestAccepted: @@ -98,12 +99,12 @@ struct SessionVerificationScreenViewState: BindableState { case .initial: switch flow { case .initiator: - return L10n.screenSessionVerificationOpenExistingSessionSubtitle + return L10n.screenSessionVerificationUseAnotherDeviceSubtitle case .responder: return L10n.screenSessionVerificationRequestSubtitle } case .acceptingVerificationRequest: - return L10n.screenSessionVerificationRequestSubtitle + return L10n.screenSessionVerificationWaitingAnotherDeviceSubtitle case .requestingVerification: return L10n.screenSessionVerificationWaitingToAcceptSubtitle case .verificationRequestAccepted: diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift index bbbcf94270..95f6cc6f28 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift @@ -31,15 +31,10 @@ struct SessionVerificationScreen: View { @ViewBuilder private var screenHeader: some View { VStack(spacing: 0) { - if context.viewState.verificationState == .initial { - BigIcon(icon: \.lockSolid) - .padding(.bottom, 16) - } else { - Image(systemName: context.viewState.headerImageName) - .bigIcon() - .padding(.bottom, 16) - } - + BigIcon(icon: context.viewState.headerIcon.keyPath, + style: context.viewState.headerIcon.style) + .padding(.bottom, 16) + Text(context.viewState.title ?? "") .font(.compound.headingMDBold) .multilineTextAlignment(.center) diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 8a18ec2744..80dcd267cd 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -37,6 +37,7 @@ struct PinnedEventsTimelineScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, + isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled, isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift index 6331383322..8b8073db4d 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift @@ -19,7 +19,7 @@ import SwiftUI struct ResolveVerifiedUserSendFailureScreenCoordinatorParameters { let failure: TimelineItemSendFailure.VerifiedUser - let itemID: TimelineItemIdentifier + let sendHandle: SendHandleProxy let roomProxy: JoinedRoomProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol } @@ -43,7 +43,7 @@ final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol self.parameters = parameters viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: parameters.failure, - itemID: parameters.itemID, + sendHandle: parameters.sendHandle, roomProxy: parameters.roomProxy, userIndicatorController: parameters.userIndicatorController) } diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift index 58b60a5aae..dd7b454f1d 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -22,7 +22,7 @@ typealias ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewMode class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFailureScreenViewModelType, ResolveVerifiedUserSendFailureScreenViewModelProtocol { private let iterator: VerifiedUserSendFailureIterator private let failure: TimelineItemSendFailure.VerifiedUser - private let itemID: TimelineItemIdentifier + private let sendHandle: SendHandleProxy private let roomProxy: JoinedRoomProxyProtocol private var members: [String: RoomMemberProxyProtocol] @@ -34,7 +34,7 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail } init(failure: TimelineItemSendFailure.VerifiedUser, - itemID: TimelineItemIdentifier, + sendHandle: SendHandleProxy, roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { iterator = switch failure { @@ -43,7 +43,7 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail } self.failure = failure - self.itemID = itemID + self.sendHandle = sendHandle self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController @@ -77,9 +77,9 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail private func resolveAndResend() async { let result = switch failure { case .hasUnsignedDevice(let devices): - await roomProxy.ignoreDeviceTrustAndResend(devices: devices, itemID: itemID) + await roomProxy.ignoreDeviceTrustAndResend(devices: devices, sendHandle: sendHandle) case .changedIdentity(let users): - await roomProxy.withdrawVerificationAndResend(userIDs: users, itemID: itemID) + await roomProxy.withdrawVerificationAndResend(userIDs: users, sendHandle: sendHandle) } if case let .failure(error) = result { @@ -98,7 +98,7 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail } private func resend() async { - switch await roomProxy.resend(itemID: itemID) { + switch await sendHandle.resend() { case .success: actionsSubject.send(.dismiss) case .failure(let error): diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift index b0ef698b15..4f35ae0955 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -94,7 +94,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, - itemID: .randomEvent, + sendHandle: .mock, 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: .randomEvent, + sendHandle: .mock, roomProxy: JoinedRoomProxyMock(.init()), userIndicatorController: UserIndicatorControllerMock()) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 92dbcc3b42..43e2f1b2aa 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -29,6 +29,7 @@ enum RoomDetailsScreenCoordinatorAction { case presentRolesAndPermissionsScreen case presentCall case presentPinnedEventsTimeline + case presentKnockingRequestsListScreen } final class RoomDetailsScreenCoordinator: CoordinatorProtocol { @@ -79,6 +80,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentCall) case .displayPinnedEventsTimeline: actionsSubject.send(.presentPinnedEventsTimeline) + case .displayKnockingRequests: + actionsSubject.send(.presentKnockingRequestsListScreen) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index e1854f8a41..cfd247cdc3 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -22,6 +22,7 @@ enum RoomDetailsScreenViewModelAction { case requestRolesAndPermissionsPresentation case startCall case displayPinnedEventsTimeline + case displayKnockingRequests } // MARK: View @@ -42,9 +43,17 @@ struct RoomDetailsScreenViewState: BindableState { var canEditRoomTopic = false var canEditRoomAvatar = false var canEditRolesOrPermissions = false + var canKickUsers = false + var canBanUsers = false var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading var canJoinCall = false var pinnedEventsActionState = RoomDetailsScreenPinnedEventsActionState.loading + var knockingEnabled = false + var isKnockableRoom = false + + var canSeeKnockingRequests: Bool { + knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers) + } var canEdit: Bool { !isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar) @@ -188,6 +197,7 @@ enum RoomDetailsScreenViewAction { case processTapRolesAndPermissions case processTapCall case processTapPinnedEvents + case processTapRequestsToJoin } enum RoomDetailsScreenViewShortcut { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index d56498ef80..a7a8cbd856 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -75,6 +75,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr bindings: .init()), mediaProvider: mediaProvider) + appSettings.$knockingEnabled + .weakAssign(to: \.state.knockingEnabled, on: self) + .store(in: &cancellables) + appMediator.networkMonitor.reachabilityPublisher .filter { $0 == .reachable } .receive(on: DispatchQueue.main) @@ -160,6 +164,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr case .processTapPinnedEvents: analyticsService.trackInteraction(name: .PinnedMessageRoomInfoButton) actionsSubject.send(.displayPinnedEventsTimeline) + case .processTapRequestsToJoin: + actionsSubject.send(.displayKnockingRequests) } } @@ -183,6 +189,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces() state.joinedMembersCount = roomInfo.joinedMembersCount state.bindings.isFavourite = roomInfo.isFavourite + switch roomInfo.joinRule { + case .knock, .knockRestricted: + state.isKnockableRoom = true + default: + state.isKnockableRoom = false + } } private func fetchMembersIfNeeded() async { @@ -212,6 +224,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.canEditRoomAvatar = await (try? roomProxy.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == true state.canEditRolesOrPermissions = await (try? roomProxy.suggestedRole(for: roomProxy.ownUserID).get()) == .administrator state.canInviteUsers = await (try? roomProxy.canUserInvite(userID: roomProxy.ownUserID).get()) == true + state.canKickUsers = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canBanUsers = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true } private func setupNotificationSettingsSubscription() { @@ -349,7 +363,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: url, mimeType: "image/jpeg")) { + if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"), + case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) { 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 207f10796c..6a8613d167 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -160,6 +160,24 @@ struct RoomDetailsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) } + + 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.canSeeKnockingRequests { + ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle, + icon: \.askToJoin), + // TODO: Display count if requests > 0 when an API for them is available + details: .counter(1), + kind: .navigationLink { + context.send(viewAction: .processTapRequestsToJoin) + }) + } ListRow(label: .default(title: L10n.screenPollsHistoryTitle, icon: \.polls), kind: .navigationLink { @@ -189,14 +207,6 @@ struct RoomDetailsScreen: View { context.send(viewAction: .toggleFavourite(isFavourite: newValue)) } - 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, icon: \.admin), @@ -294,6 +304,7 @@ struct RoomDetailsScreen: View { struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { static let genericRoomViewModel = { + ServiceLocator.shared.settings.knockingEnabled = true let members: [RoomMemberProxyMock] = [ .mockMeAdmin, .mockAlice, @@ -313,7 +324,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { isDirect: false, isEncrypted: true, canonicalAlias: "#alias:domain.com", - members: members)) + members: members, + joinRule: .knock)) var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration() notificationSettingsProxyMockConfiguration.roomMode.isDefault = false @@ -357,6 +369,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { }() static let simpleRoomViewModel = { + ServiceLocator.shared.settings.knockingEnabled = true let members: [RoomMemberProxyMock] = [ .mockMeAdmin, .mockAlice, @@ -367,7 +380,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { name: "Room A", isDirect: false, isEncrypted: false, - members: members)) + members: members, + joinRule: .knock)) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) return RoomDetailsScreenViewModel(roomProxy: roomProxy, diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift index 95a67e0305..81f9605e29 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectorySearchScreen.swift @@ -84,7 +84,7 @@ struct RoomDirectorySearchScreen_Previews: PreviewProvider, TestablePreview { topic: nil, avatar: .room(id: "test_2", name: "Test 2", - avatarURL: .documentsDirectory), + avatarURL: .mockMXCAvatar), canBeJoined: false)] let roomDirectorySearchProxy = RoomDirectorySearchProxyMock(configuration: .init(results: results)) diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 178183a853..ac550181f6 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -165,7 +165,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro 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: url, mimeType: "image/jpeg")) { + if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"), + case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index 68250d114d..2322ad4047 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -91,13 +91,13 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { .mockAdmin, .mockModerator, .init(with: .init(userID: "@nodisplayname:matrix.org", membership: .join)), - .init(with: .init(userID: "@avatar:matrix.org", displayName: "Avatar", avatarURL: .picturesDirectory, membership: .join)) + .init(with: .init(userID: "@avatar:matrix.org", displayName: "Avatar", avatarURL: .mockMXCAvatar, membership: .join)) ] static let bannedMembers: [RoomMemberProxyMock] = [ .init(with: .init(userID: "@nodisplayname:matrix.org", membership: .ban)), .init(with: .init(userID: "@fake:matrix.org", displayName: "President", membership: .ban)), - .init(with: .init(userID: "@badavatar:matrix.org", avatarURL: .picturesDirectory, membership: .ban)) + .init(with: .init(userID: "@badavatar:matrix.org", avatarURL: .mockMXCAvatar, membership: .ban)) ] static let viewModel = RoomMembersListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Some room", diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 3459fb1c81..bfc6767a2b 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -284,9 +284,11 @@ extension FormatType { } enum ComposerMode: Equatable { + enum EditType { case `default`, addCaption, editCaption } + case `default` case reply(eventID: String, replyDetails: TimelineItemReplyDetails, isThread: Bool) - case edit(originalEventOrTransactionID: EventOrTransactionId) + case edit(originalEventOrTransactionID: EventOrTransactionId, type: EditType) case recordVoiceMessage(state: AudioRecorderState) case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 36471da729..c8e77830c2 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -15,6 +15,7 @@ import WysiwygComposer typealias ComposerToolbarViewModelType = StateStoreViewModel final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerToolbarViewModelProtocol { + private var initialText: String? private let wysiwygViewModel: WysiwygComposerViewModel private let completionSuggestionService: CompletionSuggestionServiceProtocol private let analyticsService: AnalyticsService @@ -41,12 +42,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool private var replyLoadingTask: Task? - init(wysiwygViewModel: WysiwygComposerViewModel, + init(initialText: String? = nil, + wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, mentionDisplayHelper: MentionDisplayHelper, analyticsService: AnalyticsService, composerDraftService: ComposerDraftServiceProtocol) { + self.initialText = initialText self.wysiwygViewModel = wysiwygViewModel self.completionSuggestionService = completionSuggestionService self.analyticsService = analyticsService @@ -206,6 +209,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } else { set(text: plainText) } + case .setFocus: + state.bindings.composerFocused = true case .removeFocus: state.bindings.composerFocused = false case .clear: @@ -219,8 +224,12 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } } - func loadDraft() { - Task { + func loadDraft() async { + if let initialText { + set(text: initialText) + set(mode: .default) + state.bindings.composerFocused = true + } else { guard case let .success(draft) = await draftService.loadDraft(), let draft else { return @@ -258,7 +267,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool case .newMessage: set(mode: .default) case .edit(let eventID): - set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID))) + set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID), type: .default)) case .reply(let eventID): set(mode: .reply(eventID: eventID, replyDetails: .loading(eventID: eventID), isThread: false)) replyLoadingTask = Task { @@ -314,7 +323,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool switch state.composerMode { case .default: type = .newMessage - case .edit(.eventId(let originalEventID)): + case .edit(.eventId(let originalEventID), .default): type = .edit(eventID: originalEventID) case .reply(let eventID, _, _): type = .reply(eventID: eventID) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift index 88db5d5cba..f7d19700bd 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -15,6 +15,6 @@ protocol ComposerToolbarViewModelProtocol { var keyCommands: [WysiwygKeyCommand] { get } func process(timelineAction: TimelineComposerAction) - func loadDraft() + func loadDraft() async func saveDraft() } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift index 29b5ff4cc9..2599f522d1 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift @@ -118,7 +118,7 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview { VStack(spacing: 8) { 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 } + .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCAvatar, range: .init()))]) { _ in } } VStack(spacing: 8) { CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()), diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index 0cb580d0ab..dc87b4c31d 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -62,7 +62,7 @@ struct ComposerToolbar: View { if !context.composerFormattingEnabled { if context.viewState.isUploading { ProgressView() - .scaledFrame(size: 44, relativeTo: .title) + .scaledFrame(size: 44, relativeTo: .compound.headingLG) .padding(.leading, 3) } else if context.viewState.showSendButton { sendButton @@ -119,27 +119,29 @@ struct ComposerToolbar: View { Image(Asset.Images.closeRte.name) .resizable() .scaledToFit() - .scaledFrame(size: 30, relativeTo: .title) - .scaledPadding(7, relativeTo: .title) + .scaledFrame(size: 30, relativeTo: .compound.headingLG) + .scaledPadding(7, relativeTo: .compound.headingLG) } .accessibilityLabel(L10n.actionClose) .accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions) } private var sendButton: some View { - Button { - sendMessage() - } label: { - CompoundIcon(context.viewState.composerMode.isEdit ? \.check : \.sendSolid) - .scaledPadding(6, relativeTo: .title) - .accessibilityLabel(context.viewState.composerMode.isEdit ? L10n.actionConfirm : L10n.actionSend) - .foregroundColor(context.viewState.sendButtonDisabled ? .compound.iconDisabled : .white) - .background { - Circle() - .foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary) + Group { + if context.viewState.composerMode.isEdit { + Button(action: sendMessage) { + CompoundIcon(\.check, size: .medium, relativeTo: .compound.headingLG) + .foregroundColor(.white) + .scaledPadding(6, relativeTo: .compound.headingLG) + .background(.compound.iconAccentTertiary, in: Circle()) + .accessibilityLabel(L10n.actionConfirm) } - .scaledPadding(4, relativeTo: .title) + } else { + SendButton(action: sendMessage) + .accessibilityLabel(L10n.actionSend) + } } + .scaledPadding(4, relativeTo: .compound.headingLG) .disabled(context.viewState.sendButtonDisabled) .animation(.linear(duration: 0.1).disabledDuringTests(), value: context.viewState.sendButtonDisabled) .keyboardShortcut(.return, modifiers: [.command]) @@ -271,8 +273,8 @@ struct ComposerToolbar: View { } label: { CompoundIcon(\.delete) .scaledToFit() - .scaledFrame(size: 30, relativeTo: .title) - .scaledPadding(7, relativeTo: .title) + .scaledFrame(size: 30, relativeTo: .compound.headingLG) + .scaledPadding(7, relativeTo: .compound.headingLG) } .buttonStyle(.compound(.plain)) .accessibilityLabel(L10n.a11yDelete) @@ -291,6 +293,8 @@ struct ComposerToolbar: View { } } +// MARK: - Previews + struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { static let wysiwygViewModel = WysiwygComposerViewModel() static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, @@ -300,7 +304,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { analyticsService: ServiceLocator.shared.analytics, composerDraftService: ComposerDraftServiceMock()) static let suggestions: [SuggestionItem] = [.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()))] + .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCAvatar, range: .init()))] static var previews: some View { ComposerToolbar.mock(focused: true) @@ -330,8 +334,6 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { } } -// MARK: - Mock - extension ComposerToolbar { static func mock(focused: Bool = true) -> ComposerToolbar { let wysiwygViewModel = WysiwygComposerViewModel() diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift index 549dd5624b..cc4bef92be 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift @@ -41,7 +41,7 @@ struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview { 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())) + MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test", displayName: "Test", avatarURL: .mockMXCAvatar, range: .init())) MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test2", displayName: nil, avatarURL: nil, range: .init())) } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 0a142b37a1..649321db83 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -35,17 +35,8 @@ struct MessageComposer: View { resizeGrabber } - mainContent - .padding(.horizontal, 12.0) - .clipShape(composerShape) - .background { - ZStack { - composerShape - .fill(Color.compound.bgSubtleSecondary) - composerShape - .stroke(Color.compound._borderTextFieldFocused, lineWidth: 0.5) - } - } + composerTextField + .messageComposerStyle(header: header) // Explicitly disable all animations to fix weirdness with the header immediately // appearing whilst the text field and keyboard are still animating up to it. .animation(.noAnimation, value: mode) @@ -57,34 +48,27 @@ struct MessageComposer: View { @State private var composerFrame = CGRect.zero - private var mainContent: some View { - VStack(alignment: .leading, spacing: -6) { - header - - if composerFormattingEnabled { - Color.clear - .overlay(alignment: .top) { - composerView - .clipped() - .readFrame($composerFrame) - } - .frame(minHeight: ComposerConstant.minHeight, maxHeight: max(composerHeight, composerFrame.height), - alignment: .top) - .tint(.compound.iconAccentTertiary) - .padding(.vertical, 10) - .onAppear { - onAppearAction() - } - } else { - MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder, - text: $plainComposerText, - presendCallback: $presendCallback, - maxHeight: ComposerConstant.maxHeight, - keyHandler: { handleKeyPress($0) }, - pasteHandler: pasteAction) - .tint(.compound.iconAccentTertiary) - .padding(.vertical, 10) - } + @ViewBuilder + private var composerTextField: some View { + if composerFormattingEnabled { + Color.clear + .overlay(alignment: .top) { + composerView + .clipped() + .readFrame($composerFrame) + } + .frame(minHeight: ComposerConstant.minHeight, maxHeight: max(composerHeight, composerFrame.height), + alignment: .top) + .onAppear { + onAppearAction() + } + } else { + MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder, + text: $plainComposerText, + presendCallback: $presendCallback, + maxHeight: ComposerConstant.maxHeight, + keyHandler: { handleKeyPress($0) }, + pasteHandler: pasteAction) } } @@ -98,8 +82,8 @@ struct MessageComposer: View { switch mode { case .reply(_, let replyDetails, _): MessageComposerReplyHeader(replyDetails: replyDetails, action: cancellationAction) - case .edit: - MessageComposerEditHeader(action: cancellationAction) + case .edit(_, let editType): + MessageComposerEditHeader(editType: editType, action: cancellationAction) case .recordVoiceMessage, .previewVoiceMessage, .default: EmptyView() } @@ -168,14 +152,20 @@ private struct MessageComposerReplyHeader: View { } private struct MessageComposerEditHeader: View { + let editType: ComposerMode.EditType let action: () -> Void + private var title: String { + switch editType { + case .default: L10n.commonEditing + case .addCaption: L10n.commonAddingCaption + case .editCaption: L10n.commonEditingCaption + } + } + var body: some View { HStack(alignment: .center, spacing: 8) { - Label(L10n.commonEditing, - icon: \.editSolid, - iconSize: .xSmall, - relativeTo: .compound.bodySMSemibold) + Label(title, icon: \.editSolid, iconSize: .xSmall, relativeTo: .compound.bodySMSemibold) .labelStyle(MessageComposerHeaderLabelStyle()) Spacer() Button(action: action) { @@ -200,6 +190,42 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle { } } +// MARK: - Style + +extension View { + func messageComposerStyle(header: some View = EmptyView()) -> some View { + modifier(MessageComposerStyleModifier(header: header)) + } +} + +private struct MessageComposerStyleModifier: ViewModifier { + private let composerShape = RoundedRectangle(cornerRadius: 21, style: .circular) + + let header: Header + + func body(content: Content) -> some View { + VStack(alignment: .leading, spacing: -6) { + header + + content + .tint(.compound.iconAccentTertiary) + .padding(.vertical, 10) + } + .padding(.horizontal, 12.0) + .clipShape(composerShape) + .background { + ZStack { + composerShape + .fill(Color.compound.bgSubtleSecondary) + composerShape + .stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5) + } + } + } +} + +// MARK: - Previews + struct MessageComposer_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock @@ -211,6 +237,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { duration: 100, waveform: nil, source: nil, + fileSize: nil, contentType: nil)))), .loaded(sender: .init(id: "James"), eventID: "123", @@ -220,14 +247,15 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { eventContent: .message(.file(.init(filename: "brain-surgery.pdf", caption: "File: Crash course in brain surgery", source: nil, + fileSize: nil, thumbnailSource: nil, contentType: nil)))), .loaded(sender: .init(id: "Cliff"), eventID: "123", eventContent: .message(.image(.init(filename: "head.png", caption: "Image: Pushead", - source: .init(url: .picturesDirectory, mimeType: nil), - thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))), + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail)))), .loaded(sender: .init(id: "Jason"), eventID: "123", eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))), @@ -238,9 +266,8 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { eventID: "123", eventContent: .message(.video(.init(filename: "never.mov", caption: "Video: Through the never", - duration: 100, - source: nil, - thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))), + videoInfo: .mockVideo, + thumbnailInfo: .mockThumbnail)))), .loading(eventID: "") ] @@ -275,13 +302,20 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { messageComposer() messageComposer(.init(string: "Some message"), - mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString))) + mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .default)) 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")))), isThread: false)) + + Color.clear.frame(height: 20) + + messageComposer(.init(string: "Some new caption"), + mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .addCaption)) + messageComposer(.init(string: "Some updated caption"), + mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .editCaption)) } .padding(.horizontal) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift index 791ee8d548..b789445a71 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -18,8 +18,8 @@ struct RoomAttachmentPicker: View { Menu { menuContent } label: { - CompoundIcon(asset: Asset.Images.composerAttachment, size: .custom(30), relativeTo: .title) - .scaledPadding(7, relativeTo: .title) + CompoundIcon(asset: Asset.Images.composerAttachment, size: .custom(30), relativeTo: .compound.headingLG) + .scaledPadding(7, relativeTo: .compound.headingLG) } .buttonStyle(RoomAttachmentPickerButtonStyle()) .accessibilityLabel(L10n.actionAddToTimeline) @@ -32,7 +32,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .enableTextFormatting) } label: { Label(L10n.screenRoomAttachmentTextFormatting, icon: \.textFormatting) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerTextFormatting) @@ -40,7 +39,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .attach(.poll)) } label: { Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPoll) @@ -48,7 +46,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .attach(.location)) } label: { Label(L10n.screenRoomAttachmentSourceLocation, icon: \.locationPin) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerLocation) @@ -56,7 +53,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .attach(.file)) } label: { Label(L10n.screenRoomAttachmentSourceFiles, icon: \.attachment) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerDocuments) @@ -64,7 +60,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .attach(.photoLibrary)) } label: { Label(L10n.screenRoomAttachmentSourceGallery, icon: \.image) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerPhotoLibrary) @@ -72,7 +67,6 @@ struct RoomAttachmentPicker: View { context.send(viewAction: .attach(.camera)) } label: { Label(L10n.screenRoomAttachmentSourceCamera, icon: \.takePhoto) - .labelStyle(.menuSheet) } .accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerCamera) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingButton.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingButton.swift index 8688f0f6cf..c998209e53 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingButton.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingButton.swift @@ -32,18 +32,15 @@ struct VoiceMessageRecordingButton: View { } label: { switch mode { case .idle: - CompoundIcon(\.micOn, size: .medium, relativeTo: .title) + CompoundIcon(\.micOn, size: .medium, relativeTo: .compound.headingLG) .foregroundColor(.compound.iconSecondary) - .scaledPadding(10, relativeTo: .title) + .scaledPadding(10, relativeTo: .compound.headingLG) case .recording: - CompoundIcon(asset: Asset.Images.stopRecording, size: .medium, relativeTo: .title) + CompoundIcon(asset: Asset.Images.stopRecording, size: .medium, relativeTo: .compound.headingLG) .foregroundColor(.compound.iconOnSolidPrimary) - .scaledPadding(6, relativeTo: .title) - .background( - Circle() - .foregroundColor(.compound.bgActionPrimaryRest) - ) - .scaledPadding(4, relativeTo: .title) + .scaledPadding(6, relativeTo: .compound.headingLG) + .background(.compound.bgActionPrimaryRest, in: Circle()) + .scaledPadding(4, relativeTo: .compound.headingLG) } } .buttonStyle(VoiceMessageRecordingButtonStyle()) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 1f63793857..94b94d51a6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -15,6 +15,7 @@ struct RoomScreenCoordinatorParameters { let clientProxy: ClientProxyProtocol let roomProxy: JoinedRoomProxyProtocol var focussedEvent: FocusEvent? + var sharedText: String? let timelineController: RoomTimelineControllerProtocol let mediaProvider: MediaProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol @@ -40,7 +41,8 @@ enum RoomScreenCoordinatorAction { case presentMessageForwarding(forwardingItem: MessageForwardingItem) case presentCallScreen case presentPinnedEventsTimeline - case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) + case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) + case presentKnockRequestsList } final class RoomScreenCoordinator: CoordinatorProtocol { @@ -88,7 +90,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { maxCompressedHeight: ComposerConstant.maxHeight, maxExpandedHeight: ComposerConstant.maxHeight, parserStyle: .elementX) - let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, + let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText, + wysiwygViewModel: wysiwygViewModel, completionSuggestionService: parameters.completionSuggestionService, mediaProvider: parameters.mediaProvider, mentionDisplayHelper: ComposerMentionDisplayHelper(timelineContext: timelineViewModel.context), @@ -132,8 +135,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) case .displayLocation(let body, let geoURI, let description): actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) - case .displayResolveSendFailure(let failure, let itemID): - actionsSubject.send(.presentResolveSendFailure(failure: failure, itemID: itemID)) + case .displayResolveSendFailure(let failure, let sendHandle): + actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle)) case .composer(let action): composerViewModel.process(timelineAction: action) case .hasScrolled(direction: let direction): @@ -167,12 +170,14 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentCallScreen) case .removeComposerFocus: composerViewModel.process(timelineAction: .removeFocus) + case .displayKnockRequests: + actionsSubject.send(.presentKnockRequestsList) } } .store(in: &cancellables) // Loading the draft requires the subscriptions to be set up first otherwise the room won't be be able to propagate the information to the composer. - composerViewModel.loadDraft() + Task { await composerViewModel.loadDraft() } } func focusOnEvent(_ focussedEvent: FocusEvent) { @@ -183,6 +188,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol { Task { await timelineViewModel.focusOnEvent(eventID: eventID) } } + func shareText(_ string: String) { + composerViewModel.process(timelineAction: .setMode(mode: .default)) // Make sure we're not e.g. replying. + composerViewModel.process(timelineAction: .setText(plainText: string, htmlText: nil)) + composerViewModel.process(timelineAction: .setFocus) + } + func stop() { composerViewModel.saveDraft() timelineViewModel.stop() diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 92c9bfdbe2..e9b16fa378 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -14,6 +14,7 @@ enum RoomScreenViewModelAction { case displayRoomDetails case displayCall case removeComposerFocus + case displayKnockRequests } enum RoomScreenViewAction { @@ -22,6 +23,9 @@ enum RoomScreenViewAction { case displayRoomDetails case displayCall case footerViewAction(RoomScreenFooterViewAction) + case acceptKnock(userID: String) + case dismissKnockRequests + case viewKnockRequests } struct RoomScreenViewState: BindableState { @@ -39,6 +43,18 @@ struct RoomScreenViewState: BindableState { var hasOngoingCall: Bool var shouldShowCallButton = true + var isKnockingEnabled = false + var isKnockableRoom = false + var canAcceptKnocks = false + var canDeclineKnocks = false + var canBan = false + // TODO: We still don't know how to get these, but these will be the non already seen knock requests of the room, for now we are using this as a mock for testing purposes + var unseenKnockRequests: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Helloooo")] + + var shouldSeeKnockRequests: Bool { + isKnockingEnabled && isKnockableRoom && !unseenKnockRequests.isEmpty && (canAcceptKnocks || canDeclineKnocks || canBan) + } + var footerDetails: RoomScreenFooterViewDetails? var bindings: RoomScreenViewStateBindings diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 93c95e1a3f..c518a39e4a 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -103,6 +103,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .resolvePinViolation(let userID): Task { await resolveIdentityPinningViolation(userID) } } + case .acceptKnock(userID: let userID): + // TODO: API to accept a knock required + break + case .dismissKnockRequests: + // TODO: API to mark knocks as seen required + break + case .viewKnockRequests: + actionsSubject.send(.displayKnockRequests) } } @@ -117,6 +125,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Private private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher) { + appSettings.$knockingEnabled + .weakAssign(to: \.state.isKnockingEnabled, on: self) + .store(in: &cancellables) + let roomInfoSubscription = roomProxy .infoPublisher @@ -236,10 +248,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) } - let userID = roomProxy.ownUserID - if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { - state.canJoinCall = permission + switch (roomProxy.isEncryptedOneToOneRoom, roomInfo.joinRule) { + case (false, .knock), (false, .knockRestricted): + state.isKnockableRoom = true + default: + state.isKnockableRoom = false } + + let ownUserID = roomProxy.ownUserID + state.canJoinCall = await (try? roomProxy.canUserJoinCall(userID: ownUserID).get()) == true + state.canAcceptKnocks = await (try? roomProxy.canUserInvite(userID: ownUserID).get()) == true + state.canDeclineKnocks = await (try? roomProxy.canUserKick(userID: ownUserID).get()) == true + state.canBan = await (try? roomProxy.canUserBan(userID: ownUserID).get()) == true } private func setupPinnedEventsTimelineProviderIfNeeded() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/KnockRequestsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/KnockRequestsBannerView.swift new file mode 100644 index 0000000000..ae33410794 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/KnockRequestsBannerView.swift @@ -0,0 +1,202 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct KnockRequestInfo { + let displayName: String? + let avatarURL: URL? + let userID: String + let reason: String? +} + +struct KnockRequestsBannerView: View { + let requests: [KnockRequestInfo] + let onDismiss: () -> Void + let onAccept: ((String) -> Void)? + let onViewAll: () -> Void + var mediaProvider: MediaProviderProtocol? + + var body: some View { + mainContent + .padding(16) + .background(.compound.bgCanvasDefault, in: RoundedRectangle(cornerRadius: 12)) + .compositingGroup() + .shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4) + .padding(.horizontal, 16) + } + + @ViewBuilder + private var mainContent: some View { + if requests.count == 1 { + SingleKnockRequestBannerContent(request: requests[0], + onDismiss: onDismiss, + onAccept: onAccept, + onViewAll: onViewAll, + mediaProvider: mediaProvider) + } else if requests.count > 1 { + MultipleKnockRequestsBannerContent(requests: requests, + onDismiss: onDismiss, + onViewAll: onViewAll, + mediaProvider: mediaProvider) + } else { + EmptyView() + } + } +} + +private struct SingleKnockRequestBannerContent: View { + let request: KnockRequestInfo + let onDismiss: () -> Void + let onAccept: ((String) -> Void)? + let onViewAll: () -> Void + var mediaProvider: MediaProviderProtocol? + + var body: some View { + VStack(spacing: 14) { + header + if let reason = request.reason { + Text(reason) + .lineLimit(2) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + } + actions + } + } + + private var header: some View { + HStack(spacing: 10) { + LoadableAvatarImage(url: request.avatarURL, + name: request.displayName, + contentID: request.userID, + avatarSize: .user(on: .knockingUserBanner), mediaProvider: mediaProvider) + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 0) { + Text(L10n.screenRoomSingleKnockRequestTitle(request.displayName ?? request.userID)) + .lineLimit(2) + .font(.compound.bodyMDSemibold) + .foregroundStyle(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + KnockRequestsBannerDismissButton(onDismiss: onDismiss) + } + if request.displayName != nil { + Text(request.userID) + .lineLimit(2) + .font(.compound.bodySM) + .foregroundStyle(.compound.textSecondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + + private var actions: some View { + HStack(spacing: 12) { + Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll) + .buttonStyle(.compound(.secondary, size: .medium)) + if let onAccept { + Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle, action: { + onAccept(request.userID) + }) + .buttonStyle(.compound(.primary, size: .medium)) + } + } + .padding(.top, request.reason == nil ? 0 : 2) + .frame(maxWidth: .infinity) + } +} + +private struct MultipleKnockRequestsBannerContent: View { + let requests: [KnockRequestInfo] + let onDismiss: () -> Void + let onViewAll: () -> Void + var mediaProvider: MediaProviderProtocol? + + private var avatars: [StackedAvatarInfo] { + requests + .prefix(3) + .map { .init(url: $0.avatarURL, name: $0.displayName, contentID: $0.userID) } + .reversed() + } + + private var multipleKnockRequestsTitle: String { + guard let first = requests.first else { + return "" + } + + let string = first.displayName ?? first.userID + return L10n.tr("Localizable", "screen_room_multiple_knock_requests_title", string, avatars.count - 1) + } + + var body: some View { + VStack(spacing: 14) { + HStack(spacing: 10) { + StackedAvatarsView(overlap: 16, lineWidth: 2, shouldStackFromLast: true, avatars: avatars, avatarSize: .user(on: .knockingUsersBannerStack), mediaProvider: mediaProvider) + HStack(alignment: .top, spacing: 0) { + Text(multipleKnockRequestsTitle) + .lineLimit(2) + .font(.compound.bodyMDSemibold) + .foregroundStyle(.compound.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + KnockRequestsBannerDismissButton(onDismiss: onDismiss) + } + } + Button(L10n.screenRoomMultipleKnockRequestsViewAllButtonTitle) { + onViewAll() + } + .buttonStyle(.compound(.primary, size: .medium)) + } + } +} + +private struct KnockRequestsBannerDismissButton: View { + let onDismiss: () -> Void + + var body: some View { + Button { + onDismiss() + } label: { + CompoundIcon(\.close, size: .medium, relativeTo: .compound.bodySMSemibold) + .foregroundColor(.compound.iconTertiary) + } + .alignmentGuide(.top, computeValue: { _ in + 3 + }) + } +} + +struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview { + static let singleRequest: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil)] + + static let singleRequestWithReason: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hey, I’d like to join this room because of xyz topic and I’d like to participate in the room.")] + + static let singleRequestNoDisplayName: [KnockRequestInfo] = [.init(displayName: nil, avatarURL: nil, userID: "@alice:matrix.org", reason: nil)] + + static let multipleRequests: [KnockRequestInfo] = [ + .init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil), + .init(displayName: "Bob", avatarURL: nil, userID: "@bob:matrix.org", reason: nil), + .init(displayName: "Charlie", avatarURL: nil, userID: "@charlie:matrix.org", reason: nil), + .init(displayName: "Dan", avatarURL: nil, userID: "@dan:matrix.org", reason: nil), + .init(displayName: "Test", avatarURL: nil, userID: "@dan:matrix.org", reason: nil) + ] + + static var previews: some View { + KnockRequestsBannerView(requests: singleRequest, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) + .previewDisplayName("Single Request") + KnockRequestsBannerView(requests: singleRequest, onDismiss: { }, onAccept: nil, onViewAll: { }) + .previewDisplayName("Single Request, no accept action") + KnockRequestsBannerView(requests: singleRequestWithReason, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) + .previewDisplayName("Single Request with reason") + KnockRequestsBannerView(requests: singleRequestNoDisplayName, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) + .previewDisplayName("Single Request, No Display Name") + KnockRequestsBannerView(requests: multipleRequests, onDismiss: { }, onAccept: { _ in }, onViewAll: { }) + .previewDisplayName("Multiple Requests") + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 9e187d8ab3..c6f1792259 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -29,12 +29,11 @@ struct RoomScreen: View { timeline .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .overlay(alignment: .top) { - Group { - if roomContext.viewState.shouldShowPinnedEventsBanner { - pinnedItemsBanner - } - } - .animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner) + pinnedItemsBanner + } + // This can overlay on top of the pinnedItemsBanner + .overlay(alignment: .top) { + knockRequestsBanner } .safeAreaInset(edge: .bottom, spacing: 0) { VStack(spacing: 0) { @@ -76,6 +75,7 @@ struct RoomScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, + isCreateMediaCaptionsEnabled: timelineContext.viewState.isCreateMediaCaptionsEnabled, isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() @@ -119,11 +119,45 @@ struct RoomScreen: View { } } + @ViewBuilder private var pinnedItemsBanner: some View { - PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState, - onMainButtonTap: { roomContext.send(viewAction: .tappedPinnedEventsBanner) }, - onViewAllButtonTap: { roomContext.send(viewAction: .viewAllPins) }) - .transition(.move(edge: .top)) + Group { + if roomContext.viewState.shouldShowPinnedEventsBanner { + PinnedItemsBannerView(state: roomContext.viewState.pinnedEventsBannerState, + onMainButtonTap: { roomContext.send(viewAction: .tappedPinnedEventsBanner) }, + onViewAllButtonTap: { roomContext.send(viewAction: .viewAllPins) }) + .transition(.move(edge: .top)) + } + } + .animation(.elementDefault, value: roomContext.viewState.shouldShowPinnedEventsBanner) + } + + @ViewBuilder + private var knockRequestsBanner: some View { + Group { + if roomContext.viewState.shouldSeeKnockRequests { + KnockRequestsBannerView(requests: roomContext.viewState.unseenKnockRequests, + onDismiss: dismissKnockRequestsBanner, + onAccept: roomContext.viewState.canAcceptKnocks ? acceptKnockRequest : nil, + onViewAll: onViewAllKnockRequests, + mediaProvider: roomContext.mediaProvider) + .padding(.top, 16) + .transition(.move(edge: .top)) + } + } + .animation(.elementDefault, value: roomContext.viewState.shouldSeeKnockRequests) + } + + private func dismissKnockRequestsBanner() { + roomContext.send(viewAction: .dismissKnockRequests) + } + + private func acceptKnockRequest(userID: String) { + roomContext.send(viewAction: .acceptKnock(userID: userID)) + } + + private func onViewAllKnockRequests() { + roomContext.send(viewAction: .viewKnockRequests) } private var scrollToBottomButton: some View { diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift new file mode 100644 index 0000000000..a89a8b4cc4 --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift @@ -0,0 +1,52 @@ +// +// 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 SwiftUI + +struct RoomSelectionScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol + let roomSummaryProvider: RoomSummaryProviderProtocol + let mediaProvider: MediaProviderProtocol +} + +enum RoomSelectionScreenCoordinatorAction { + case dismiss + case confirm(roomID: String) +} + +final class RoomSelectionScreenCoordinator: CoordinatorProtocol { + private var viewModel: RoomSelectionScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: RoomSelectionScreenCoordinatorParameters) { + viewModel = RoomSelectionScreenViewModel(clientProxy: parameters.clientProxy, + roomSummaryProvider: parameters.roomSummaryProvider, + mediaProvider: parameters.mediaProvider) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + switch action { + case .dismiss: + self?.actionsSubject.send(.dismiss) + case .confirm(let roomID): + self?.actionsSubject.send(.confirm(roomID: roomID)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(RoomSelectionScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift new file mode 100644 index 0000000000..1d066b994d --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift @@ -0,0 +1,39 @@ +// +// 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 MatrixRustSDK + +enum RoomSelectionScreenViewModelAction { + case dismiss + case confirm(roomID: String) +} + +struct RoomSelectionScreenViewState: BindableState { + var rooms: [RoomSelectionRoom] = [] + var selectedRoomID: String? + var bindings = RoomSelectionScreenViewStateBindings() +} + +struct RoomSelectionScreenViewStateBindings { + var searchQuery = "" +} + +enum RoomSelectionScreenViewAction { + case cancel + case confirm + case selectRoom(roomID: String) + case reachedTop + case reachedBottom +} + +struct RoomSelectionRoom: Identifiable, Equatable { + let id: String + let title: String + let description: String + let avatar: RoomAvatar +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift new file mode 100644 index 0000000000..3bad9ac694 --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift @@ -0,0 +1,105 @@ +// +// 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 SwiftUI + +typealias RoomSelectionScreenViewModelType = StateStoreViewModel + +class RoomSelectionScreenViewModel: RoomSelectionScreenViewModelType, RoomSelectionScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol + private let roomSummaryProvider: RoomSummaryProviderProtocol + + private var actionsSubject: PassthroughSubject = .init() + + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(clientProxy: ClientProxyProtocol, + roomSummaryProvider: RoomSummaryProviderProtocol, + mediaProvider: MediaProviderProtocol) { + self.clientProxy = clientProxy + self.roomSummaryProvider = roomSummaryProvider + + super.init(initialViewState: RoomSelectionScreenViewState(), mediaProvider: mediaProvider) + + roomSummaryProvider.roomListPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateRooms() + } + .store(in: &cancellables) + + context.$viewState + .map(\.bindings.searchQuery) + .removeDuplicates() + .sink { [weak self] searchQuery in + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } + } + .store(in: &cancellables) + + updateRooms() + } + + override func process(viewAction: RoomSelectionScreenViewAction) { + switch viewAction { + case .cancel: + actionsSubject.send(.dismiss) + roomSummaryProvider.setFilter(.all(filters: [])) + case .confirm: + guard let selectedRoomID = state.selectedRoomID else { + return + } + + actionsSubject.send(.confirm(roomID: selectedRoomID)) + case .selectRoom(let roomID): + state.selectedRoomID = roomID + case .reachedTop: + updateVisibleRange(edge: .top) + case .reachedBottom: + updateVisibleRange(edge: .bottom) + } + } + + // MARK: - Private + + private func updateRooms() { + var rooms = [RoomSelectionRoom]() + + for summary in roomSummaryProvider.roomListPublisher.value { + rooms.append(.init(id: summary.id, + title: summary.name, + description: summary.roomListDescription, + avatar: summary.avatar)) + } + + state.rooms = rooms + } + + /// The actual range values don't matter as long as they contain the lower + /// or upper bounds. updateVisibleRange is a hybrid API that powers both + /// sliding sync visible range update and list paginations + /// For lists other than the home screen one we don't care about visible ranges, + /// we just need the respective bounds to be there to trigger a next page load or + /// a reset to just one page + private func updateVisibleRange(edge: UIRectEdge) { + switch edge { + case .top: + roomSummaryProvider.updateVisibleRange(0..<0) + case .bottom: + let roomCount = roomSummaryProvider.roomListPublisher.value.count + roomSummaryProvider.updateVisibleRange(roomCount.. { get } + var context: RoomSelectionScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift b/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift new file mode 100644 index 0000000000..8a1ea9113d --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift @@ -0,0 +1,104 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct RoomSelectionScreen: View { + @ObservedObject var context: RoomSelectionScreenViewModel.Context + + var body: some View { + Form { + Section { + ForEach(context.viewState.rooms) { room in + RoomSelectionListRow(room: room, + isSelected: context.viewState.selectedRoomID == room.id, + context: context) + } + // Replace these with ScrollView's `scrollPosition` when dropping iOS 16. + } header: { + emptyRectangle + .onAppear { + context.send(viewAction: .reachedTop) + } + } footer: { + emptyRectangle + .onAppear { + context.send(viewAction: .reachedBottom) + } + } + } + .compoundList() + .navigationTitle(L10n.screenRoomlistMainSpaceTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + ToolbarItem(placement: .confirmationAction) { + Button(L10n.actionShare) { + context.send(viewAction: .confirm) + } + .disabled(context.viewState.selectedRoomID == nil) + } + } + .searchController(query: $context.searchQuery, showsCancelButton: false) + .compoundSearchField() + .disableAutocorrection(true) + } + + /// The greedy size of Rectangle can create an issue with the navigation bar when the search is highlighted, so is best to use a fixed frame instead of hidden() or EmptyView() + private var emptyRectangle: some View { + Rectangle() + .frame(width: 0, height: 0) + } +} + +private struct RoomSelectionListRow: View { + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + let room: RoomSelectionRoom + let isSelected: Bool + let context: RoomSelectionScreenViewModel.Context + + var body: some View { + ListRow(label: .avatar(title: room.title, + description: room.description, + icon: avatar), + kind: .selection(isSelected: isSelected) { + context.send(viewAction: .selectRoom(roomID: room.id)) + }) + } + + @ViewBuilder @MainActor + var avatar: some View { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .roomSelection), + mediaProvider: context.mediaProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + } +} + +// MARK: - Previews + +struct RoomSelectionScreen_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) + let viewModel = RoomSelectionScreenViewModel(clientProxy: ClientProxyMock(.init()), + roomSummaryProvider: summaryProvider, + mediaProvider: MediaProviderMock(configuration: .init())) + + NavigationStack { + RoomSelectionScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift index acd39bd180..51314243eb 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -239,13 +239,13 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview SecureBackupRecoveryKeyScreen(context: generatingViewModel.context) } .previewDisplayName("Generating") - .snapshot(delay: 0.25) + .snapshotPreferences(delay: 0.25) NavigationStack { SecureBackupRecoveryKeyScreen(context: setupViewModel.context) } .previewDisplayName("Set up") - .snapshot(delay: 0.25) + .snapshotPreferences(delay: 0.25) NavigationStack { SecureBackupRecoveryKeyScreen(context: incompleteViewModel.context) diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index f8523ff3c1..d8a496c8f0 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -43,13 +43,14 @@ enum DeveloperOptionsScreenViewAction { protocol DeveloperOptionsProtocol: AnyObject { var logLevel: TracingConfiguration.LogLevel { get set } var slidingSyncDiscovery: AppSettings.SlidingSyncDiscovery { get set } + var publicSearchEnabled: Bool { get set } var hideUnreadMessagesBadge: Bool { get set } var fuzzyRoomListSearchEnabled: 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 } + var createMediaCaptionsEnabled: 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 d9e6f2a6cd..de6909cc5f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -36,6 +36,10 @@ struct DeveloperOptionsScreen: View { } Section("Room List") { + Toggle(isOn: $context.publicSearchEnabled) { + Text("Public search") + } + Toggle(isOn: $context.hideUnreadMessagesBadge) { Text("Hide grey dots") } @@ -50,8 +54,8 @@ struct DeveloperOptionsScreen: View { Text("Hide image & video previews") } - Toggle(isOn: $context.frequentEmojisEnabled) { - Text("Show frequently used emojis") + Toggle(isOn: $context.createMediaCaptionsEnabled) { + Text("Allow creation of media captions") } } diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 83b2bd23dd..2e610569ca 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -100,12 +100,15 @@ class TimelineInteractionHandler { switch action { case .copy: - guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { + guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { return } + UIPasteboard.general.string = messageTimelineItem.body + case .copyCaption: + guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol, + let caption = messageTimelineItem.mediaCaption else { return } - - UIPasteboard.general.string = messageTimelineItem.body - case .edit: + UIPasteboard.general.string = caption + case .edit, .addCaption, .editCaption, .editPoll: switch timelineItem { case let messageTimelineItem as EventBasedMessageTimelineItemProtocol: processEditMessageEvent(messageTimelineItem) @@ -118,6 +121,12 @@ class TimelineInteractionHandler { default: MXLog.error("Cannot edit item with id: \(timelineItem.id)") } + case .removeCaption: + guard case let .event(_, eventOrTransactionID) = timelineItem.id else { + MXLog.error("Failed removing caption, missing event ID") + return + } + Task { await timelineController.removeCaption(eventOrTransactionID) } case .copyPermalink: guard let eventID = eventTimelineItem.id.eventID else { actionsSubject.send(.displayErrorToast(L10n.errorFailedCreatingThePermalink)) @@ -133,17 +142,10 @@ class TimelineInteractionHandler { UIPasteboard.general.url = permalinkURL } case .redact: - guard case let .event(_, eventOrTransactionID) = itemID else { - fatalError() - } - - Task { - await timelineController.redact(eventOrTransactionID) - } + guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() } + Task { await timelineController.redact(eventOrTransactionID) } case .reply: - guard let eventID = eventTimelineItem.id.eventID else { - return - } + guard let eventID = eventTimelineItem.id.eventID else { return } let replyInfo = buildReplyInfo(for: eventTimelineItem) let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type) @@ -156,21 +158,14 @@ class TimelineInteractionHandler { MXLog.info("Showing debug info for \(eventTimelineItem.id)") actionsSubject.send(.showDebugInfo(debugInfo)) case .retryDecryption(let sessionID): - Task { - await timelineController.retryDecryption(for: sessionID) - } + Task { await timelineController.retryDecryption(for: sessionID) } case .report: actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id)) case .react: displayEmojiPicker(for: itemID) case .toggleReaction(let key): - Task { - guard case let .event(_, eventOrTransactionID) = itemID else { - fatalError() - } - - await timelineController.toggleReaction(key, to: eventOrTransactionID) - } + guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() } + Task { await timelineController.toggleReaction(key, to: eventOrTransactionID) } case .endPoll(let pollStartID): endPoll(pollStartID: pollStartID) case .pin: @@ -202,18 +197,35 @@ class TimelineInteractionHandler { let text: String var htmlText: String? + var editType = ComposerMode.EditType.default switch messageTimelineItem.contentType { case .text(let content): text = content.body htmlText = content.formattedBodyHTMLString case .emote(let content): text = "/me " + content.body + case .audio(let content): + text = content.caption ?? "" + htmlText = content.formattedCaptionHTMLString + editType = text.isEmpty ? .addCaption : .editCaption + case .file(let content): + text = content.caption ?? "" + htmlText = content.formattedCaptionHTMLString + editType = text.isEmpty ? .addCaption : .editCaption + case .image(let content): + text = content.caption ?? "" + htmlText = content.formattedCaptionHTMLString + editType = text.isEmpty ? .addCaption : .editCaption + case .video(let content): + text = content.caption ?? "" + htmlText = content.formattedCaptionHTMLString + editType = text.isEmpty ? .addCaption : .editCaption default: text = messageTimelineItem.body } // 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(originalEventOrTransactionID: eventOrTransactionID)))) + actionsSubject.send(.composer(action: .setMode(mode: .edit(originalEventOrTransactionID: eventOrTransactionID, type: editType)))) actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText))) } @@ -248,54 +260,20 @@ class TimelineInteractionHandler { // MARK: Pasting and dropping func handlePasteOrDrop(_ provider: NSItemProvider) { - guard let contentType = provider.preferredContentType, - let preferredExtension = contentType.preferredFilenameExtension else { - MXLog.error("Invalid NSItemProvider: \(provider)") - actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia)) - return - } - - let providerSuggestedName = provider.suggestedName - let providerDescription = provider.description - - _ = provider.loadDataRepresentation(for: contentType) { data, error in - Task { @MainActor in - let loadingIndicatorIdentifier = UUID().uuidString - self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) - defer { - self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) - } - - if let error { - self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia)) - MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)") - return - } - - guard let data else { - self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia)) - MXLog.error("Invalid NSItemProvider data: \(providerDescription)") - return - } - - do { - let url = try await Task.detached { - if let filename = providerSuggestedName { - let hasExtension = !(filename as NSString).pathExtension.isEmpty - let filename = hasExtension ? filename : "\(filename).\(preferredExtension)" - return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename) - } else { - let filename = "\(UUID().uuidString).\(preferredExtension)" - return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename) - } - }.value - - self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) - } catch { - self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia)) - MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)") - } + Task { + let loadingIndicatorIdentifier = UUID().uuidString + self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) + defer { + self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) + } + + guard let fileURL = await provider.storeData() else { + MXLog.error("Failed storing NSItemProvider data \(provider)") + self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia)) + return } + + self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: fileURL)) } } @@ -429,10 +407,7 @@ class TimelineInteractionHandler { return } - guard case .success(let mediaPlayer) = mediaPlayerProvider.player(for: source), let audioPlayer = mediaPlayer as? AudioPlayerProtocol else { - MXLog.error("Cannot play a voice message without an audio player") - return - } + let audioPlayer = mediaPlayerProvider.player // Stop any recording in progress if voiceMessageRecorder.isRecording { @@ -451,7 +426,7 @@ class TimelineInteractionHandler { // Detach all other states await mediaPlayerProvider.detachAllStates(except: audioPlayerState) - guard audioPlayer.mediaSource == source, audioPlayer.state != .error else { + guard audioPlayer.sourceURL == source.url, audioPlayer.state != .error else { // Load content do { MXLog.info("Loading voice message audio content from source for itemID \(itemID)") @@ -459,7 +434,7 @@ class TimelineInteractionHandler { // Make sure that the player is still attached, as it may have been detached while waiting for the voice message to be loaded. if audioPlayerState.isAttached { - audioPlayer.load(mediaSource: source, using: url, autoplay: true) + audioPlayer.load(sourceURL: source.url, playbackURL: url, autoplay: true) } } catch { MXLog.error("Failed to load voice message: \(error)") @@ -553,11 +528,11 @@ class TimelineInteractionHandler { switch timelineItem { case let item as ImageRoomTimelineItem: - source = item.content.source + source = item.content.imageInfo.source filename = item.content.filename caption = item.content.caption case let item as VideoRoomTimelineItem: - source = item.content.source + source = item.content.videoInfo.source filename = item.content.filename caption = item.content.caption case let item as FileRoomTimelineItem: diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 363ddb9fa1..2108e635a0 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -22,7 +22,7 @@ enum TimelineViewModelAction { case tappedOnSenderDetails(userID: String) case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayLocation(body: String, geoURI: GeoURI, description: String?) - case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) + case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) case composer(action: TimelineComposerAction) case hasScrolled(direction: ScrollDirection) case viewInRoomTimeline(eventID: String) @@ -43,7 +43,7 @@ enum TimelineViewAction { case itemAppeared(itemID: TimelineItemIdentifier) case itemDisappeared(itemID: TimelineItemIdentifier) - case itemTapped(itemID: TimelineItemIdentifier) + case mediaTapped(itemID: TimelineItemIdentifier) case itemSendInfoTapped(itemID: TimelineItemIdentifier) case toggleReaction(key: String, itemID: TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier) @@ -79,6 +79,7 @@ enum TimelineViewAction { enum TimelineComposerAction { case setMode(mode: ComposerMode) case setText(plainText: String, htmlText: String?) + case setFocus case removeFocus case clear } @@ -98,6 +99,7 @@ struct TimelineViewState: BindableState { var canCurrentUserRedactSelf = false var canCurrentUserPin = false var isViewSourceEnabled: Bool + var isCreateMediaCaptionsEnabled: Bool var hideTimelineMedia: Bool // The `pinnedEventIDs` are used only to determine if an item is already pinned or not. diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 7a7103499d..3777feed1d 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -81,6 +81,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), ownUserID: roomProxy.ownUserID, isViewSourceEnabled: appSettings.viewSourceEnabled, + isCreateMediaCaptionsEnabled: appSettings.createMediaCaptionsEnabled, hideTimelineMedia: appSettings.hideTimelineMedia, pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs, bindings: .init(reactionsCollapsed: [:]), @@ -128,8 +129,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { Task { await timelineController.processItemAppearance(id) } case .itemDisappeared(let id): Task { await timelineController.processItemDisappearance(id) } - case .itemTapped(let id): - Task { await handleItemTapped(with: id) } + case .mediaTapped(let id): + Task { await handleMediaTapped(with: id) } case .itemSendInfoTapped(let itemID): handleItemSendInfoTapped(itemID: itemID) case .toggleReaction(let emoji, let itemID): @@ -447,6 +448,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { .weakAssign(to: \.state.isViewSourceEnabled, on: self) .store(in: &cancellables) + appSettings.$createMediaCaptionsEnabled + .weakAssign(to: \.state.isCreateMediaCaptionsEnabled, on: self) + .store(in: &cancellables) + appSettings.$hideTimelineMedia .weakAssign(to: \.state.hideTimelineMedia, on: self) .store(in: &cancellables) @@ -533,7 +538,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { await timelineController.sendReadReceipt(for: lastVisibleItemID) } - private func handleItemTapped(with itemID: TimelineItemIdentifier) async { + private func handleMediaTapped(with itemID: TimelineItemIdentifier) async { state.showLoading = true let action = await timelineInteractionHandler.processItemTap(itemID) @@ -562,7 +567,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { if case .sendingFailed(.unknown) = eventTimelineItem.properties.deliveryStatus { displayAlert(.sendingFailed) } else if case let .sendingFailed(.verifiedUser(failure)) = eventTimelineItem.properties.deliveryStatus { - actionsSubject.send(.displayResolveSendFailure(failure: failure, itemID: itemID)) + guard let sendHandle = timelineController.sendHandle(for: itemID) else { + MXLog.error("Cannot find send handle for \(itemID).") + return + } + + actionsSubject.send(.displayResolveSendFailure(failure: failure, + sendHandle: sendHandle)) + } else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message { displayAlert(.encryptionAuthenticity(authenticityMessage)) } @@ -599,11 +611,17 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { html: html, inReplyToEventID: eventID, intentionalMentions: intentionalMentions) - case .edit(let originalEventOrTransactionID): + case .edit(let originalEventOrTransactionID, .default): await timelineController.edit(originalEventOrTransactionID, message: message, html: html, intentionalMentions: intentionalMentions) + case .edit(let originalEventOrTransactionID, .addCaption), + .edit(let originalEventOrTransactionID, .editCaption): + await timelineController.editCaption(originalEventOrTransactionID, + message: message, + html: html, + intentionalMentions: intentionalMentions) case .default: switch slashCommand(message: message) { case .join: diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift index f97ae80116..8cb4264d49 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift @@ -51,7 +51,7 @@ struct TimelineItemMacContextMenu: View { } Section { - ForEach(menuActions.debugActions) { action in + ForEach(menuActions.secondaryActions) { action in Button(role: action.isDestructive ? .destructive : nil) { send(action) } label: { diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index cc0b1e50be..0dbfb0670e 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -11,6 +11,7 @@ import SwiftUI struct TimelineItemMenu: View { @EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.dismiss) private var dismiss + @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var reactionsFrame = CGRect.zero @@ -46,7 +47,7 @@ struct TimelineItemMenu: View { .background(Color.compound.bgSubtlePrimary) } - viewsForActions(actions.debugActions) + viewsForActions(actions.secondaryActions) } } } @@ -109,31 +110,43 @@ struct TimelineItemMenu: View { } private var reactionsSection: some View { - ScrollView(.horizontal) { - HStack(alignment: .center, spacing: 8) { - ForEach(actions.reactions, id: \.key) { - reactionButton(for: $0.key) - } - - Button { - dismiss() - // Otherwise we get errors that a sheet is already presented - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - context.send(viewAction: .displayEmojiPicker(itemID: item.id)) + HStack(spacing: 8) { + ScrollView(.horizontal) { + HStack(alignment: .center, spacing: 8) { + ForEach(actions.reactions, id: \.key) { + reactionButton(for: $0.key) } - } label: { - CompoundIcon(\.reactionAdd, size: .medium, relativeTo: .compound.headingLG) - .foregroundColor(.compound.iconSecondary) - .padding(10) } - .accessibilityLabel(L10n.actionReact) + .padding(.horizontal) + .frame(minWidth: reactionsFrame.width, maxWidth: .infinity, alignment: .center) + } + .scrollIndicators(.hidden) + .scrollBounceBehavior(.basedOnSize, axes: .horizontal) + .readFrame($reactionsFrame) + .overlay { + if horizontalSizeClass == .compact { + LinearGradient(stops: [.init(color: .clear, location: 0.0), + .init(color: .clear, location: 0.9), + .init(color: .compound.bgCanvasDefault, location: 1.0)], + startPoint: .leading, + endPoint: .trailing) + .allowsHitTesting(false) + } } - .padding(.horizontal) - .frame(minWidth: reactionsFrame.width, maxWidth: .infinity, alignment: .center) + + Button { + dismiss() + // Otherwise we get errors that a sheet is already presented + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + context.send(viewAction: .displayEmojiPicker(itemID: item.id)) + } + } label: { + CompoundIcon(\.reactionAdd, size: .medium, relativeTo: .compound.headingLG) + .foregroundColor(.compound.iconSecondary) + .padding(10) + } + .accessibilityLabel(L10n.actionReact) } - .scrollIndicators(.hidden) - .scrollBounceBehavior(.basedOnSize, axes: .horizontal) - .readFrame($reactionsFrame) } private func reactionButton(for emoji: String) -> some View { @@ -166,8 +179,8 @@ struct TimelineItemMenu: View { send(action) } label: { action.label - .labelStyle(.menuSheet) } + .buttonStyle(.menuSheet) } } @@ -249,21 +262,28 @@ private extension View { // MARK: - Previews struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { + enum ItemType { case incomingText, outgoingMedia, outgoingMediaWithCaption } + static let viewModel = TimelineViewModel.mock - static let (item, actions) = makeItem() - static let (backupItem, _) = makeItem(authenticity: .notGuaranteed(color: .gray)) - static let (unsignedItem, _) = makeItem(authenticity: .unsignedDevice(color: .red)) - static let (unencryptedItem, _) = makeItem(authenticity: .sentInClear(color: .red)) - static let (unknownFailureItem, _) = makeItem(deliveryStatus: .sendingFailed(.unknown)) - static let (identityChangedItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.changedIdentity(users: [ + static let (item, actions) = makeActions() + static let (backupItem, _) = makeActions(authenticity: .notGuaranteed(color: .gray)) + static let (unsignedItem, _) = makeActions(authenticity: .unsignedDevice(color: .red)) + static let (unencryptedItem, _) = makeActions(authenticity: .sentInClear(color: .red)) + static let (unknownFailureItem, _) = makeActions(deliveryStatus: .sendingFailed(.unknown)) + static let (identityChangedItem, _) = makeActions(deliveryStatus: .sendingFailed(.verifiedUser(.changedIdentity(users: [ "@alice:matrix.org" ])))) - static let (unsignedDevicesItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [ + static let (unsignedDevicesItem, _) = makeActions(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [ "@alice:matrix.org": ["DEVICE1", "DEVICE2"] ])))) - static let (ownUnsignedDevicesItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [ + static let (ownUnsignedDevicesItem, _) = makeActions(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [ RoomMemberProxyMock.mockMe.userID: ["DEVICE1"] ])))) + + // Media + + static let (mediaItem, mediaItemActions) = makeActions(itemType: .outgoingMedia) + static let (mediaItemWithCaption, mediaItemWithCaptionActions) = makeActions(itemType: .outgoingMediaWithCaption) static var previews: some View { TimelineItemMenu(item: item, actions: actions) @@ -302,26 +322,56 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { TimelineItemMenu(item: identityChangedItem, actions: actions) .environmentObject(viewModel.context) .previewDisplayName("Identity Changed") + + // Media + + TimelineItemMenu(item: mediaItem, actions: mediaItemActions) + .environmentObject(viewModel.context) + .previewDisplayName("Media") + + TimelineItemMenu(item: mediaItemWithCaption, actions: mediaItemWithCaptionActions) + .environmentObject(viewModel.context) + .previewDisplayName("Media with Caption") } - static func makeItem(authenticity: EncryptionAuthenticity? = nil, - deliveryStatus: TimelineItemDeliveryStatus? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! { - guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem, - let actions = TimelineItemMenuActions(isReactable: true, - actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], - debugActions: [.viewSource], - emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) else { - return nil - } - - if let authenticity { - item.properties.encryptionAuthenticity = authenticity - } + static func makeActions(itemType: ItemType = .incomingText, + authenticity: EncryptionAuthenticity? = nil, + deliveryStatus: TimelineItemDeliveryStatus? = nil) -> (EventBasedTimelineItemProtocol, TimelineItemMenuActions)! { + guard var item = makeItem(itemType: itemType) else { return nil } + let provider = TimelineItemMenuActionProvider(timelineItem: item, + canCurrentUserRedactSelf: true, + canCurrentUserRedactOthers: false, + canCurrentUserPin: true, + pinnedEventIDs: [], + isDM: true, + isViewSourceEnabled: true, + isCreateMediaCaptionsEnabled: true, + isPinnedEventsTimeline: false, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) + guard let actions = provider.makeActions() else { return nil } - if let deliveryStatus { - item.properties.deliveryStatus = deliveryStatus + if var textItem = item as? TextRoomTimelineItem { + if let authenticity { + textItem.properties.encryptionAuthenticity = authenticity + } + + if let deliveryStatus { + textItem.properties.deliveryStatus = deliveryStatus + } + item = textItem } return (item, actions) } + + static func makeItem(itemType: ItemType) -> EventBasedTimelineItemProtocol? { + switch itemType { + case .incomingText: + RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol + case .outgoingMedia: + RoomTimelineItemFixtures.mediaChunk[1] as? EventBasedTimelineItemProtocol + case .outgoingMediaWithCaption: + RoomTimelineItemFixtures.mediaChunk[5] as? EventBasedTimelineItemProtocol + } + } } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index a2e7de4f20..3a0e88b792 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. // +import OrderedCollections import SFSafeSymbols import SwiftUI @@ -12,48 +13,55 @@ import SwiftUI struct TimelineItemMenuActions { let reactions: [TimelineItemMenuReaction] let actions: [TimelineItemMenuAction] - let debugActions: [TimelineItemMenuAction] + let secondaryActions: [TimelineItemMenuAction] init?(isReactable: Bool, actions: [TimelineItemMenuAction], - debugActions: [TimelineItemMenuAction], + secondaryActions: [TimelineItemMenuAction], emojiProvider: EmojiProviderProtocol) { - if !isReactable, actions.isEmpty, debugActions.isEmpty { + if !isReactable, actions.isEmpty, secondaryActions.isEmpty { return nil } self.actions = actions - self.debugActions = debugActions + self.secondaryActions = secondaryActions - // 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 += [ + var frequentlyUsed: OrderedSet = [ .init(key: "👍️", symbol: .handThumbsup), .init(key: "👎️", symbol: .handThumbsdown), - .init(key: "🔥", symbol: .flame), - .init(key: "❤️", symbol: .heart), - .init(key: "👏", symbol: .handsClap) + .init(key: "🎉", symbol: .partyPopper), + .init(key: "❤️", symbol: .heart) ] - frequentlyUsed = Array(frequentlyUsed.prefix(5)) + frequentlyUsed.append(contentsOf: emojiProvider.frequentlyUsedSystemEmojis().map { TimelineItemMenuReaction(key: $0, symbol: .heart) }) reactions = if isReactable { - frequentlyUsed + Array(frequentlyUsed.elements.prefix(10)) } else { [] } } } -struct TimelineItemMenuReaction { +struct TimelineItemMenuReaction: Hashable { let key: String let symbol: SFSymbol + + // Frequently used emojis on the all use the same .heart SFSymbol. + // Override equatable so we can remove duplicates. + static func == (lhs: TimelineItemMenuReaction, rhs: TimelineItemMenuReaction) -> Bool { + lhs.key == rhs.key + } } enum TimelineItemMenuAction: Identifiable, Hashable { case copy + case copyCaption case edit + case addCaption + case editCaption + case removeCaption + case editPoll case copyPermalink case redact case reply(isThread: Bool) @@ -73,7 +81,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { /// Whether the item should cancel a reply/edit occurring in the composer. var switchToDefaultComposer: Bool { switch self { - case .reply, .edit: + case .reply, .edit, .addCaption, .editCaption, .editPoll: return false default: return true @@ -83,7 +91,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { /// Whether the action should be shown for an item that failed to send. var canAppearInFailedEcho: Bool { switch self { - case .copy, .edit, .redact, .viewSource: + case .copy, .edit, .redact, .viewSource, .editPoll: return true default: return false @@ -103,7 +111,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { /// Whether or not the action is destructive. var isDestructive: Bool { switch self { - case .redact, .report: + case .redact, .report, .removeCaption: return true default: return false @@ -124,9 +132,19 @@ enum TimelineItemMenuAction: Identifiable, Hashable { var label: some View { switch self { case .copy: - Label(L10n.actionCopy, icon: \.copy) + Label(L10n.actionCopyText, icon: \.copy) + case .copyCaption: + Label(L10n.actionCopyCaption, icon: \.copy) case .edit: Label(L10n.actionEdit, icon: \.edit) + case .addCaption: + Label(L10n.actionAddCaption, icon: \.edit) + case .editCaption: + Label(L10n.actionEditCaption, icon: \.edit) + case .removeCaption: + Label(L10n.actionRemoveCaption, icon: \.close) + case .editPoll: + Label(L10n.actionEditPoll, icon: \.edit) case .copyPermalink: Label(L10n.actionCopyLinkToMessage, icon: \.link) case .reply(let isThread): @@ -134,7 +152,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { case .forward: Label(L10n.actionForward, icon: \.forward) case .redact: - Label(L10n.actionRemove, icon: \.delete) + Label(L10n.actionRemoveMessage, icon: \.delete) case .viewSource: Label(L10n.actionViewSource, icon: \.code) case .retryDecryption: diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 9fcc47575a..4d624fb3f7 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -16,6 +16,7 @@ struct TimelineItemMenuActionProvider { let pinnedEventIDs: Set let isDM: Bool let isViewSourceEnabled: Bool + let isCreateMediaCaptionsEnabled: Bool let isPinnedEventsTimeline: Bool let emojiProvider: EmojiProviderProtocol @@ -31,26 +32,12 @@ struct TimelineItemMenuActionProvider { return nil } - var debugActions: [TimelineItemMenuAction] = [] - if isViewSourceEnabled { - debugActions.append(.viewSource) - } - if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem { - switch encryptedItem.encryptionType { - case .megolmV1AesSha2(let sessionID, _): - debugActions.append(.retryDecryption(sessionID: sessionID)) - default: - break - } - - return .init(isReactable: false, - actions: [.copyPermalink], - debugActions: debugActions, - emojiProvider: emojiProvider) + return makeEncryptedItemActions(encryptedItem) } var actions: [TimelineItemMenuAction] = [] + var secondaryActions: [TimelineItemMenuAction] = [] if item.canBeRepliedTo { if let messageItem = item as? EventBasedMessageTimelineItemProtocol { @@ -63,51 +50,95 @@ struct TimelineItemMenuActionProvider { if item.isForwardable { actions.append(.forward(itemID: item.id)) } - - if item.isEditable { - actions.append(.edit) - } if canCurrentUserPin, let eventID = item.id.eventID { actions.append(pinnedEventIDs.contains(eventID) ? .unpin : .pin) } + + if item.isRemoteMessage { + actions.append(.copyPermalink) + } + + if item.isEditable { + if item.supportsMediaCaption { + if item.hasMediaCaption { + actions.append(.editCaption) + } else if isCreateMediaCaptionsEnabled { + actions.append(.addCaption) + } + } else if item is PollRoomTimelineItem { + actions.append(.editPoll) + } else if !(item is VoiceMessageRoomTimelineItem) { + actions.append(.edit) + } + } if item.isCopyable { actions.append(.copy) + } else if item.hasMediaCaption { + actions.append(.copyCaption) } - if item.isRemoteMessage { - actions.append(.copyPermalink) + if item.hasMediaCaption { + actions.append(.removeCaption) } - + if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = item.id.eventID { actions.append(.endPoll(pollStartID: eventID)) } - if canRedactItem(item) { - actions.append(.redact) + if isViewSourceEnabled { + actions.append(.viewSource) } - + if !item.isOutgoing { - actions.append(.report) - } - - if item.hasFailedToSend { - actions = actions.filter(\.canAppearInFailedEcho) + secondaryActions.append(.report) } - - if item.isRedacted { - actions = actions.filter(\.canAppearInRedacted) + + if canRedactItem(item) { + secondaryActions.append(.redact) } if isPinnedEventsTimeline { actions.insert(.viewInRoomTimeline, at: 0) actions = actions.filter(\.canAppearInPinnedEventsTimeline) + secondaryActions = secondaryActions.filter(\.canAppearInPinnedEventsTimeline) + } + + if item.hasFailedToSend { + actions = actions.filter(\.canAppearInFailedEcho) + secondaryActions = secondaryActions.filter(\.canAppearInFailedEcho) + } + + if item.isRedacted { + actions = actions.filter(\.canAppearInRedacted) + secondaryActions = secondaryActions.filter(\.canAppearInRedacted) } return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, actions: actions, - debugActions: debugActions, + secondaryActions: secondaryActions, + emojiProvider: emojiProvider) + } + + private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? { + var actions: [TimelineItemMenuAction] = [.copyPermalink] + var secondaryActions: [TimelineItemMenuAction] = [] + + if isViewSourceEnabled { + actions.append(.viewSource) + } + + switch encryptedItem.encryptionType { + case .megolmV1AesSha2(let sessionID, _): + secondaryActions.append(.retryDecryption(sessionID: sessionID)) + default: + break + } + + return .init(isReactable: false, + actions: actions, + secondaryActions: secondaryActions, emojiProvider: emojiProvider) } diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift index dc4e1e16e0..4e6996e9c2 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift @@ -69,7 +69,7 @@ struct ReadReceiptCell_Previews: PreviewProvider, TestablePreview { ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", formattedTimestamp: "10:00"), memberState: .init(displayName: "Test", - avatarURL: URL.documentsDirectory), + avatarURL: .mockMXCAvatar), mediaProvider: MediaProviderMock(configuration: .init())) .previewDisplayName("With Image") ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", diff --git a/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift index 6326e84a32..48810a9023 100644 --- a/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift @@ -42,7 +42,7 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: content.caption ?? content.filename, formattedBody: content.formattedCaption, - icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii)) + icon: .init(kind: .mediaSource(content.thumbnailInfo?.source ?? content.imageInfo.source), cornerRadii: iconCornerRadii)) case .notice(let content): ReplyView(sender: sender, plainBody: content.body, @@ -55,7 +55,7 @@ struct TimelineReplyView: View { ReplyView(sender: sender, plainBody: content.caption ?? content.filename, formattedBody: content.formattedCaption, - icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) }) + icon: content.thumbnailInfo.map { .init(kind: .mediaSource($0.source), cornerRadii: iconCornerRadii) }) case .voice: ReplyView(sender: sender, plainBody: L10n.commonVoiceMessage, @@ -222,9 +222,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { }() static var previewItems: [TimelineReplyView] { - let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png") - - return [ + [ TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: "")), @@ -252,6 +250,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { duration: 0, waveform: nil, source: nil, + fileSize: nil, contentType: nil))))), TimelineReplyView(placement: .timeline, @@ -260,6 +259,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { eventContent: .message(.file(.init(filename: "file.txt", caption: "Some file", source: nil, + fileSize: nil, thumbnailSource: nil, contentType: nil))))), @@ -268,17 +268,16 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { eventID: "123", eventContent: .message(.image(.init(filename: "image.jpg", caption: "Some image", - source: imageSource, - thumbnailSource: imageSource))))), + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail))))), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", eventContent: .message(.video(.init(filename: "video.mp4", caption: "Some video", - duration: 0, - source: nil, - thumbnailSource: imageSource))))), + videoInfo: .mockVideo, + thumbnailInfo: .mockThumbnail))))), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), eventID: "123", @@ -292,6 +291,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { duration: 0, waveform: nil, source: nil, + fileSize: nil, contentType: nil))))), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), @@ -321,7 +321,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { } .padding() .environmentObject(viewModel.context) - // Allow member names to load. Reduce precission as the `imageSource` randomly renders slightly differently + // Allow member names to load. Reduce precission as the `mockThumbnail` randomly renders slightly differently .snapshotPreferences(delay: 0.2, precision: 0.98) .previewLayout(.sizeThatFits) } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 61ed78ee82..fcfd2160d8 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -125,10 +125,11 @@ struct TimelineItemBubbledStylerView: View { var messageBubbleWithActions: some View { messageBubble .onTapGesture { - context.send(viewAction: .itemTapped(itemID: timelineItem.id)) + // We need a tap gesture before the long press gesture below, otherwise something + // on iOS 17 hijacks the long press and you can't bring up the context menu. This + // is no longer an issue on iOS 18. Note: it's fine for this to be empty, we handle + // specific taps within the timeline views themselves. } - // We need a tap gesture before this long one so that it doesn't - // steal away the gestures from the scroll view .longPressWithFeedback { context.send(viewAction: .displayTimelineItemMenu(itemID: timelineItem.id)) } @@ -148,6 +149,7 @@ struct TimelineItemBubbledStylerView: View { pinnedEventIDs: context.viewState.pinnedEventIDs, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled, + isCreateMediaCaptionsEnabled: context.viewState.isCreateMediaCaptionsEnabled, isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline, emojiProvider: context.viewState.emojiProvider) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in @@ -396,6 +398,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview } } .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } static var replies: some View { @@ -427,6 +430,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview groupStyle: .single)) } .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } static var threads: some View { @@ -434,6 +438,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview MockTimelineContent(isThreaded: true) } .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } static var pinned: some View { @@ -441,6 +446,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview MockTimelineContent(isPinned: true) } .environmentObject(viewModelWithPins.context) + .environment(\.timelineContext, viewModel.context) } static var encryptionAuthenticity: some View { @@ -498,8 +504,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "other.png", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), - thumbnailSource: nil), + imageInfo: .mockImage, + thumbnailInfo: nil), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray)))) @@ -514,6 +520,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview duration: 100, waveform: EstimatedWaveform.mockWaveform, source: nil, + fileSize: nil, contentType: nil), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), @@ -522,6 +529,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview waveform: EstimatedWaveform.mockWaveform)) } .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } } @@ -552,6 +560,7 @@ private struct MockTimelineContent: View { duration: 100, waveform: EstimatedWaveform.mockWaveform, source: nil, + fileSize: nil, contentType: nil), replyDetails: replyDetails)) @@ -565,6 +574,7 @@ private struct MockTimelineContent: View { content: .init(filename: "file.txt", caption: "File", source: nil, + fileSize: nil, thumbnailSource: nil, contentType: nil), replyDetails: replyDetails)) @@ -577,8 +587,8 @@ private struct MockTimelineContent: View { isThreaded: isThreaded, sender: .init(id: ""), content: .init(filename: "image.jpg", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), - thumbnailSource: nil), + imageInfo: .mockImage, + thumbnailInfo: nil), replyDetails: replyDetails)) LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), @@ -616,6 +626,7 @@ private struct MockTimelineContent: View { duration: 100, waveform: EstimatedWaveform.mockWaveform, source: nil, + fileSize: nil, contentType: nil), replyDetails: replyDetails), playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index b350a9b2e7..aef73a383f 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -152,12 +152,20 @@ private extension TimelineItemSendInfo { layoutType = switch timelineItem { case is TextBasedRoomTimelineItem: .overlay(capsuleStyle: false) - case let message as EventBasedMessageTimelineItemProtocol where message is ImageRoomTimelineItem || message is VideoRoomTimelineItem: - .overlay(capsuleStyle: !message.hasMediaCaption) + case let message as EventBasedMessageTimelineItemProtocol: + switch message { + case is ImageRoomTimelineItem, is VideoRoomTimelineItem: + .overlay(capsuleStyle: !message.hasMediaCaption) + case is AudioRoomTimelineItem, is FileRoomTimelineItem: + // swiftlint:disable:next void_function_in_ternary + message.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal(spacing: 0) // No spacing as the content already contains it. + case let locationTimelineItem as LocationRoomTimelineItem: + .overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil) + default: + .horizontal() + } case is StickerRoomTimelineItem: .overlay(capsuleStyle: true) - case let locationTimelineItem as LocationRoomTimelineItem: - .overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil) case is PollRoomTimelineItem: .vertical(spacing: 16) default: diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index 6f37e7f56e..cb82e40ae5 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -11,25 +11,22 @@ struct TimelineReadReceiptsView: View { let displayNumber = 3 let timelineItem: EventBasedTimelineItemProtocol @EnvironmentObject private var context: TimelineViewModel.Context + + var avatars: [StackedAvatarInfo] { + timelineItem.properties.orderedReadReceipts.prefix(displayNumber).map { receipt in + StackedAvatarInfo(url: context.viewState.members[receipt.userID]?.avatarURL, + name: context.viewState.members[receipt.userID]?.displayName, + contentID: receipt.userID) + } + } var body: some View { HStack(spacing: 2) { - HStack(spacing: -4) { - let receiptsToDisplay = timelineItem.properties.orderedReadReceipts.prefix(displayNumber) - ForEach(0.. displayNumber { Text("+\(remaining)") .font(.compound.bodySM) diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift index b89ec72f76..dd0ef08829 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift @@ -10,18 +10,17 @@ import SwiftUI struct AudioRoomTimelineView: View { let timelineItem: AudioRoomTimelineItem - + var body: some View { TimelineStyler(timelineItem: timelineItem) { - Label(title: { Text(timelineItem.body) }, - icon: { Image(systemName: "waveform") - .foregroundColor(.compound.iconPrimary) - }) - .labelStyle(RoomTimelineViewLabelStyle()) - .font(.compound.bodyLG) - .padding(.vertical, 12) - .padding(.horizontal, 6) - .accessibilityLabel(L10n.commonAudio) + MediaFileRoomTimelineContent(timelineItemID: timelineItem.id, + filename: timelineItem.content.filename, + fileSize: timelineItem.content.fileSize, + caption: timelineItem.content.caption, + formattedCaption: timelineItem.content.formattedCaption, + additionalWhitespaces: timelineItem.additionalWhitespaces(), + isAudioFile: true) + .accessibilityLabel(L10n.commonAudio) } } } @@ -30,21 +29,31 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock static var previews: some View { - body.environmentObject(viewModel.context) + VStack(spacing: 20) { + AudioRoomTimelineView(timelineItem: makeItem(filename: "audio.ogg", + fileSize: 2 * 1024 * 1024)) + + AudioRoomTimelineView(timelineItem: makeItem(filename: "Best Song Ever.mp3", + fileSize: 7 * 1024 * 1024, + caption: "This song rocks!")) + } + .environmentObject(viewModel.context) } - static var body: some View { - AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .randomEvent, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(filename: "audio.ogg", - duration: 300, - waveform: nil, - source: nil, - contentType: nil))) + static func makeItem(filename: String, fileSize: UInt, caption: String? = nil) -> AudioRoomTimelineItem { + .init(id: .randomEvent, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "Bob"), + content: .init(filename: filename, + caption: caption, + duration: 300, + waveform: nil, + source: nil, + fileSize: fileSize, + contentType: nil)) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift index 3beab6197c..05d57448c8 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift @@ -13,12 +13,15 @@ struct CallInviteRoomTimelineView: View { let timelineItem: CallInviteRoomTimelineItem var body: some View { - Label(title: { Text(L10n.commonCallInvite) }, - icon: { CompoundIcon(\.voiceCall, size: .medium, relativeTo: .compound.bodyMD) }) - .font(.compound.bodyMD) - .foregroundColor(.compound.textSecondary) - .frame(maxWidth: .infinity, alignment: .center) - .padding() + Label { + Text(L10n.screenRoomTimelineLegacyCall) + } icon: { + CompoundIcon(\.voiceCall, size: .medium, relativeTo: .compound.bodyMD) + } + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding() } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift index c584b8a34f..e4a8c93b0a 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift @@ -10,7 +10,7 @@ import Foundation import SwiftUI struct CallNotificationRoomTimelineView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context let timelineItem: CallNotificationRoomTimelineItem @@ -20,7 +20,7 @@ struct CallNotificationRoomTimelineView: View { name: timelineItem.sender.displayName ?? timelineItem.sender.id, contentID: timelineItem.sender.id, avatarSize: .user(on: .timeline), - mediaProvider: context.mediaProvider) + mediaProvider: context?.mediaProvider) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 0) { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift index 4febbbdc3a..ff43aedda4 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift @@ -18,6 +18,7 @@ struct EncryptedRoomTimelineView: View { case .unknown: return \.time case .sentBeforeWeJoined, + .historicalMessage, .verificationViolation, .insecureDevice: return \.block diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift index d9b3ccd4d5..3bb13c6585 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift @@ -13,63 +13,117 @@ struct FileRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { - Label { Text(timelineItem.body) } icon: { - CompoundIcon(\.document) - .foregroundColor(.compound.iconPrimary) + MediaFileRoomTimelineContent(timelineItemID: timelineItem.id, + filename: timelineItem.content.filename, + fileSize: timelineItem.content.fileSize, + caption: timelineItem.content.caption, + formattedCaption: timelineItem.content.formattedCaption, + additionalWhitespaces: timelineItem.additionalWhitespaces()) + .accessibilityLabel(L10n.commonFile) + } + } +} + +// MARK: Content + +struct MediaFileRoomTimelineContent: View { + @Environment(\.timelineContext) private var context + + let timelineItemID: TimelineItemIdentifier + let filename: String + let fileSize: UInt? + let caption: String? + let formattedCaption: AttributedString? + let additionalWhitespaces: Int + var isAudioFile = false + + var icon: KeyPath { + isAudioFile ? \.audio : \.attachment + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + filePreview + .onTapGesture { + context?.send(viewAction: .mediaTapped(itemID: timelineItemID)) + } + + if let formattedCaption { + FormattedBodyText(attributedString: formattedCaption, + additionalWhitespacesCount: additionalWhitespaces) + } else if let caption { + FormattedBodyText(text: caption, + additionalWhitespacesCount: additionalWhitespaces) + } + } + } + + var filePreview: some View { + Label { + HStack(spacing: 4) { + Text(filename) + .truncationMode(.middle) + + if let fileSize { + Text("(\(fileSize.formatted(.byteCount(style: .file))))") + .layoutPriority(1) // We want the filename to truncate rather than the size. + } } - .labelStyle(RoomTimelineViewLabelStyle()) .font(.compound.bodyLG) - .padding(.vertical, 8) - .padding(.horizontal, 6) - .accessibilityLabel(L10n.commonFile) + .foregroundStyle(.compound.textPrimary) + .lineLimit(1) + } icon: { + CompoundIcon(icon, size: .xSmall, relativeTo: .body) + .foregroundColor(.compound.iconPrimary) + .scaledPadding(8) + .background(.compound.iconOnSolidPrimary, in: Circle()) } + .labelStyle(.custom(spacing: 8, alignment: .center)) + .padding(.horizontal, 4) // Add to the styler's padding of 8, as we use the default insets for the caption. } } +// MARK: - Previews + struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock static var previews: some View { - body.environmentObject(viewModel.context) - } - - static var body: some View { VStack(spacing: 20.0) { - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(filename: "document.pdf", - source: nil, - thumbnailSource: nil, - contentType: nil))) - - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(filename: "document.docx", - source: nil, - thumbnailSource: nil, - contentType: nil))) + FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf")) + + FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf", + fileSize: 3 * 1024 * 1024)) + + FileRoomTimelineView(timelineItem: makeItem(filename: "spreadsheet.xlsx", + fileSize: 17 * 1024, + caption: "The important figures you asked me to send over.")) - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent, - timestamp: "Now", - isOutgoing: false, - isEditable: false, - canBeRepliedTo: true, - isThreaded: false, - sender: .init(id: "Bob"), - content: .init(filename: "document.txt", - source: nil, - thumbnailSource: nil, - contentType: nil))) + FileRoomTimelineView(timelineItem: makeItem(filename: "document.txt", + fileSize: 456, + caption: "Plain caption", + formattedCaption: "Formatted caption")) } + .environmentObject(viewModel.context) + } + + static func makeItem(filename: String, + fileSize: UInt? = nil, + caption: String? = nil, + formattedCaption: AttributedString? = nil) -> FileRoomTimelineItem { + .init(id: .randomEvent, + timestamp: "Now", + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: "Bob"), + content: .init(filename: filename, + caption: caption, + formattedCaption: formattedCaption, + source: nil, + fileSize: fileSize, + thumbnailSource: nil, + contentType: nil)) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift index 63cb9b04ef..86ad073d08 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI struct ImageRoomTimelineView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context let timelineItem: ImageRoomTimelineItem var hasMediaCaption: Bool { timelineItem.content.caption != nil } @@ -17,19 +17,15 @@ struct ImageRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { 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)) + loadableImage + .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)) + .onTapGesture { + context?.send(viewAction: .mediaTapped(itemID: timelineItem.id)) + } if let attributedCaption = timelineItem.content.formattedCaption { FormattedBodyText(attributedString: attributedCaption, @@ -44,15 +40,30 @@ struct ImageRoomTimelineView: View { } } - var source: MediaSourceProxy { - guard timelineItem.content.contentType != .gif, let thumbnailSource = timelineItem.content.thumbnailSource else { - return timelineItem.content.source + @ViewBuilder + private var loadableImage: some View { + if timelineItem.content.contentType == .gif { + LoadableImage(mediaSource: timelineItem.content.imageInfo.source, + mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), + blurhash: timelineItem.content.blurhash, + size: timelineItem.content.imageInfo.size, + mediaProvider: context?.mediaProvider) { + placeholder + } + .timelineMediaFrame(imageInfo: timelineItem.content.imageInfo) + } else { + LoadableImage(mediaSource: timelineItem.content.thumbnailInfo?.source ?? timelineItem.content.imageInfo.source, + mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), + blurhash: timelineItem.content.blurhash, + size: timelineItem.content.thumbnailInfo?.size ?? timelineItem.content.imageInfo.size, + mediaProvider: context?.mediaProvider) { + placeholder + } + .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo) } - - return thumbnailSource } - - var placeholder: some View { + + private var placeholder: some View { Rectangle() .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) .opacity(0.3) @@ -65,6 +76,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var previews: some View { body .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } static var body: some View { @@ -77,8 +89,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "image.jpg", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"), - thumbnailSource: nil))) + imageInfo: .mockImage, + thumbnailInfo: nil))) ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", @@ -88,8 +100,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "other.png", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), - thumbnailSource: nil))) + imageInfo: .mockImage, + thumbnailInfo: nil))) ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent, timestamp: "Now", @@ -99,9 +111,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "Blurhashed.jpg", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), - thumbnailSource: nil, - aspectRatio: 0.7, + imageInfo: .mockImage, + thumbnailInfo: nil, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW", contentType: .gif))) @@ -114,11 +125,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { 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, + imageInfo: .mockImage, + thumbnailInfo: .mockThumbnail, 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 6d3752dbd2..6c6b70189f 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -8,6 +8,7 @@ import SwiftUI struct LocationRoomTimelineView: View { + @Environment(\.timelineContext) private var context let timelineItem: LocationRoomTimelineItem var body: some View { @@ -15,6 +16,9 @@ struct LocationRoomTimelineView: View { mainContent .accessibilityElement(children: .ignore) .accessibilityLabel(accessibilityLabel) + .onTapGesture { + context?.send(viewAction: .mediaTapped(itemID: timelineItem.id)) + } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift index 65f155f53a..043d68834b 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift @@ -9,21 +9,24 @@ import Foundation import SwiftUI struct StickerRoomTimelineView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context let timelineItem: StickerRoomTimelineItem var body: some View { TimelineStyler(timelineItem: timelineItem) { - LoadableImage(url: timelineItem.imageURL, - mediaType: .timelineItem, + LoadableImage(mediaSource: timelineItem.imageInfo.source, + mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), blurhash: timelineItem.blurhash, - mediaProvider: context.mediaProvider) { + size: timelineItem.imageInfo.size, + mediaProvider: context?.mediaProvider) { placeholder } - .timelineMediaFrame(height: timelineItem.height, - aspectRatio: timelineItem.aspectRatio) + .timelineMediaFrame(imageInfo: timelineItem.imageInfo) .accessibilityElement(children: .ignore) .accessibilityLabel("\(L10n.commonSticker), \(timelineItem.body)") + .onTapGesture { + context?.send(viewAction: .mediaTapped(itemID: timelineItem.id)) + } } } @@ -38,7 +41,9 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock static var previews: some View { - body.environmentObject(viewModel.context) + body + .environmentObject(viewModel.context) + .environment(\.timelineContext, viewModel.context) } static var body: some View { @@ -50,7 +55,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { isEditable: false, canBeRepliedTo: true, sender: .init(id: "Bob"), - imageURL: URL.picturesDirectory)) + imageInfo: .mockImage)) StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, body: "Some other image", @@ -59,7 +64,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { isEditable: false, canBeRepliedTo: true, sender: .init(id: "Bob"), - imageURL: URL.picturesDirectory)) + imageInfo: .mockImage)) StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent, body: "Blurhashed image", @@ -68,8 +73,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { isEditable: false, canBeRepliedTo: true, sender: .init(id: "Bob"), - imageURL: URL.picturesDirectory, - aspectRatio: 0.7, + imageInfo: .mockImage, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift index 27b0ddd69e..88337eb78f 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI struct VideoRoomTimelineView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context let timelineItem: VideoRoomTimelineItem private var hasMediaCaption: Bool { timelineItem.content.caption != nil } @@ -18,13 +18,15 @@ struct VideoRoomTimelineView: View { TimelineStyler(timelineItem: timelineItem) { VStack(alignment: .leading, spacing: 4) { thumbnail - .timelineMediaFrame(height: timelineItem.content.height, - aspectRatio: timelineItem.content.aspectRatio) + .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo) .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)) + .onTapGesture { + context?.send(viewAction: .mediaTapped(itemID: timelineItem.id)) + } if let attributedCaption = timelineItem.content.formattedCaption { FormattedBodyText(attributedString: attributedCaption, @@ -41,11 +43,12 @@ struct VideoRoomTimelineView: View { @ViewBuilder var thumbnail: some View { - if let thumbnailSource = timelineItem.content.thumbnailSource { + if let thumbnailSource = timelineItem.content.thumbnailInfo?.source { LoadableImage(mediaSource: thumbnailSource, - mediaType: .timelineItem, + mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), blurhash: timelineItem.content.blurhash, - mediaProvider: context.mediaProvider) { imageView in + size: timelineItem.content.thumbnailInfo?.size, + mediaProvider: context?.mediaProvider) { imageView in imageView .overlay { playIcon } } placeholder: { @@ -88,9 +91,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "video.mp4", - duration: 21, - source: nil, - thumbnailSource: nil))) + videoInfo: .mockVideo, + thumbnailInfo: nil))) VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, timestamp: "Now", @@ -100,9 +102,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "other.mp4", - duration: 22, - source: nil, - thumbnailSource: nil))) + videoInfo: .mockVideo, + thumbnailInfo: nil))) VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, timestamp: "Now", @@ -112,10 +113,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { isThreaded: false, sender: .init(id: "Bob"), content: .init(filename: "Blurhashed.mp4", - duration: 23, - source: nil, - thumbnailSource: nil, - aspectRatio: 0.7, + videoInfo: .mockVideo, + thumbnailInfo: nil, blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))) VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent, @@ -127,9 +126,8 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { sender: .init(id: "Bob"), content: .init(filename: "video.mp4", caption: "This is a caption", - duration: 21, - source: nil, - thumbnailSource: nil))) + videoInfo: .mockVideo, + thumbnailInfo: nil))) } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift index 4dd222bcec..72fb09764b 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI struct TimelineSenderAvatarView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context let timelineItem: EventBasedTimelineItemProtocol @@ -18,7 +18,7 @@ struct TimelineSenderAvatarView: View { name: timelineItem.sender.displayName, contentID: timelineItem.sender.id, avatarSize: .user(on: .timeline), - mediaProvider: context.mediaProvider) + mediaProvider: context?.mediaProvider) .overlay { Circle().stroke(Color.compound.bgCanvasDefault, lineWidth: 3) } diff --git a/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift b/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift index e927cda08d..458b9c8ebf 100644 --- a/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift @@ -14,7 +14,7 @@ struct TypingIndicatorView: View { var body: some View { content .font(.compound.bodySM) - .foregroundColor(.compound.textPlaceholder) + .foregroundColor(.compound.textSecondary) .lineLimit(1) .truncationMode(.middle) .padding(.horizontal, 4) diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index 01a0f10337..bbf1f2eff7 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -104,7 +104,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr 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: url, mimeType: "image/jpeg")) { + if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"), + case let .success(file) = await mediaProvider.loadFileFromSource(mediaSource) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName) } } diff --git a/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift b/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift index 79a1a3a5a9..d7cd59c57d 100644 --- a/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift +++ b/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift @@ -22,7 +22,7 @@ private enum InternalAudioPlayerState { } class AudioPlayer: NSObject, AudioPlayerProtocol { - var mediaSource: MediaSourceProxy? + var sourceURL: URL? private var playerItem: AVPlayerItem? private var internalAudioPlayer: AVQueuePlayer? @@ -46,7 +46,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { private let releaseAudioSessionTimeoutInterval = 5.0 - private(set) var url: URL? + private(set) var playbackURL: URL? private var deinitInProgress = false @@ -86,13 +86,13 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { unloadContent() } - func load(mediaSource: MediaSourceProxy, using url: URL, autoplay: Bool) { + func load(sourceURL: URL, playbackURL: URL, autoplay: Bool) { unloadContent() setInternalState(.loading) - self.mediaSource = mediaSource - self.url = url + self.sourceURL = sourceURL + self.playbackURL = playbackURL self.autoplay = autoplay - playerItem = AVPlayerItem(url: url) + playerItem = AVPlayerItem(url: playbackURL) internalAudioPlayer = AVQueuePlayer(playerItem: playerItem) addObservers() } @@ -162,8 +162,8 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { } private func unloadContent() { - mediaSource = nil - url = nil + sourceURL = nil + playbackURL = nil internalAudioPlayer?.replaceCurrentItem(with: nil) internalAudioPlayer = nil playerItem = nil diff --git a/ElementX/Sources/Services/Audio/Player/AudioPlayerProtocol.swift b/ElementX/Sources/Services/Audio/Player/AudioPlayerProtocol.swift index 7addac3f3a..257713cc76 100644 --- a/ElementX/Sources/Services/Audio/Player/AudioPlayerProtocol.swift +++ b/ElementX/Sources/Services/Audio/Player/AudioPlayerProtocol.swift @@ -12,6 +12,17 @@ enum AudioPlayerError: Error { case genericError } +// There used to be a MediaPlayerProtocol that AudioPlayerProtocol inherited from. +// This should be called something else but we already have an AudioPlayerState, +// AudioPlayerPlaybackState and InternalAudioPlayerState so who knows what to call this. +enum MediaPlayerState { + case loading + case playing + case paused + case stopped + case error +} + enum AudioPlayerAction { case didStartLoading case didFinishLoading @@ -22,8 +33,21 @@ enum AudioPlayerAction { case didFailWithError(error: Error) } -protocol AudioPlayerProtocol: MediaPlayerProtocol { +protocol AudioPlayerProtocol: AnyObject { + var sourceURL: URL? { get } + var duration: TimeInterval { get } + var currentTime: TimeInterval { get } + var playbackURL: URL? { get } + var state: MediaPlayerState { get } + var actions: AnyPublisher { get } + + func load(sourceURL: URL, playbackURL: URL, autoplay: Bool) + func reset() + func play() + func pause() + func stop() + func seek(to progress: Double) async } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift b/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift index b0ab2abc31..2b468e28de 100644 --- a/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift +++ b/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift @@ -134,7 +134,7 @@ class AudioPlayerState: ObservableObject, Identifiable { MXLog.info("updating duration: \(duration) -> \(audioPlayerDuration)") duration = audioPlayerDuration } - fileURL = audioPlayer?.url + fileURL = audioPlayer?.playbackURL playbackState = .readyToPlay case .didStartPlaying: if let audioPlayer { diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 801ce4e8cb..41d264eda0 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -67,6 +67,22 @@ class ClientProxy: ClientProxyProtocol { "org.matrix.msc3401.call.member": Int32(0) ]) } + + private static var knockingRoomCreationPowerLevelOverrides: PowerLevels { + .init(usersDefault: nil, + eventsDefault: nil, + stateDefault: nil, + ban: nil, + kick: nil, + redact: nil, + invite: Int32(50), + notifications: nil, + users: [:], + events: [ + "m.call.member": Int32(0), + "org.matrix.msc3401.call.member": Int32(0) + ]) + } private var loadCachedAvatarURLTask: Task? private let userAvatarURLSubject = CurrentValueSubject(nil) @@ -289,7 +305,7 @@ class ClientProxy: ClientProxyProtocol { stopSync(completion: nil) } - private func stopSync(completion: (() -> Void)?) { + func stopSync(completion: (() -> Void)?) { MXLog.info("Stopping sync") if restartTask != nil { @@ -377,9 +393,14 @@ class ClientProxy: ClientProxyProtocol { } // swiftlint:disable:next function_parameter_count - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result { + func createRoom(name: String, + topic: String?, + isRoomPrivate: Bool, + isKnockingOnly: Bool, + userIDs: [String], + avatarURL: URL?, + aliasLocalPart: String?) async -> Result { do { - // TODO: Revisit once the SDK supports the knocking API let parameters = CreateRoomParameters(name: name, topic: topic, isEncrypted: isRoomPrivate, @@ -388,7 +409,10 @@ class ClientProxy: ClientProxyProtocol { preset: isRoomPrivate ? .privateChat : .publicChat, invite: userIDs, avatar: avatarURL?.absoluteString, - powerLevelContentOverride: Self.roomCreationPowerLevelOverrides) + powerLevelContentOverride: isKnockingOnly ? Self.knockingRoomCreationPowerLevelOverrides : Self.roomCreationPowerLevelOverrides, + joinRuleOverride: isKnockingOnly ? .knock : nil, + // This is an FFI naming mistake, what is required is the `aliasLocalPart` not the whole alias + canonicalAlias: aliasLocalPart) let roomID = try await client.createRoom(request: parameters) await waitForRoomToSync(roomID: roomID) @@ -626,6 +650,7 @@ class ClientProxy: ClientProxyProtocol { func resolveRoomAlias(_ alias: String) async -> Result { do { guard let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) else { + MXLog.error("Failed resolving room alias, is nil") return .failure(.failedResolvingRoomAlias) } @@ -641,6 +666,16 @@ class ClientProxy: ClientProxyProtocol { } } + func isAliasAvailable(_ alias: String) async -> Result { + do { + let result = try await client.isRoomAliasAvailable(alias: alias) + return .success(result) + } catch { + MXLog.error("Failed checking if alias: \(alias) is available with error: \(error)") + return .failure(.sdkError(error)) + } + } + func getElementWellKnown() async -> Result { await client.getElementWellKnown().map(ElementWellKnown.init) } @@ -776,13 +811,14 @@ class ClientProxy: ClientProxyProtocol { do { let syncService = try await client .syncService() - .withCrossProcessLock(appIdentifier: "MainApp") + .withCrossProcessLock() .withUtdHook(delegate: ClientDecryptionErrorDelegate(actionsSubject: actionsSubject)) .finish() + let roomListService = syncService.roomListService() let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList", - mentionBuilder: PlainMentionBuilder()), prefix: .senderName) + mentionBuilder: PlainMentionBuilder()), destination: .roomList) let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false), messageEventStringBuilder: roomMessageEventStringBuilder, shouldDisambiguateDisplayNames: false, @@ -798,7 +834,7 @@ class ClientProxy: ClientProxyProtocol { alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, - name: "MessageForwarding", + name: "AlternateAllRooms", notificationSettings: notificationSettings, appSettings: appSettings) try await alternateRoomSummaryProvider?.setRoomList(roomListService.allRooms()) @@ -894,15 +930,15 @@ class ClientProxy: ClientProxyProtocol { switch roomListItem.membership() { case .invited: return try await .invited(InvitedRoomProxy(roomListItem: roomListItem, - room: roomListItem.invitedRoom())) + roomPreview: roomListItem.previewRoom(via: []), + ownUserID: userID)) 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())) + roomPreview: roomListItem.previewRoom(via: []), + ownUserID: userID)) } + return nil case .joined: if roomListItem.isTimelineInitialized() == false { try await roomListItem.initTimeline(eventTypeFilter: eventFilters, internalIdPrefix: nil) diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 85bd4f7adf..ac06415980 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -68,7 +68,7 @@ struct RoomPreviewDetails { let topic: String? let avatarURL: URL? let memberCount: UInt - let isHistoryWorldReadable: Bool + let isHistoryWorldReadable: Bool? let isJoined: Bool let isInvited: Bool let isPublic: Bool @@ -88,7 +88,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var deviceID: String? { get } var homeserver: String { get } - + var slidingSyncVersion: SlidingSyncVersion { get } var availableSlidingSyncVersions: [SlidingSyncVersion] { get async } @@ -123,6 +123,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func startSync() func stopSync() + func stopSync(completion: (() -> Void)?) // Hopefully this will become async once we get SE-0371. func accountURL(action: AccountManagementAction) async -> URL? @@ -133,7 +134,13 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func createDirectRoom(with userID: String, expectedRoomName: String?) 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 createRoom(name: String, + topic: String?, + isRoomPrivate: Bool, + isKnockingOnly: Bool, + userIDs: [String], + avatarURL: URL?, + aliasLocalPart: String?) async -> Result func joinRoom(_ roomID: String, via: [String]) async -> Result @@ -173,6 +180,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func resolveRoomAlias(_ alias: String) async -> Result + func isAliasAvailable(_ alias: String) async -> Result + func getElementWellKnown() async -> Result // MARK: - Ignored users diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index d2e03d96dd..736e99c4b3 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -14,4 +14,5 @@ struct CreateRoomFlowParameters { var isRoomPrivate = true var isKnockingOnly = false var avatarImageMedia: MediaInfo? + var aliasLocalPart: String? } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallConfiguration.swift b/ElementX/Sources/Services/ElementCall/ElementCallConfiguration.swift index 31da3bb16f..29bbdf3cd3 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallConfiguration.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallConfiguration.swift @@ -21,7 +21,8 @@ struct ElementCallConfiguration { clientID: String, elementCallBaseURL: URL, elementCallBaseURLOverride: URL?, - colorScheme: ColorScheme) + colorScheme: ColorScheme, + notifyOtherParticipants: Bool) } /// The type of call being configured i.e. whether it's an external URL or an internal room call. @@ -58,13 +59,15 @@ struct ElementCallConfiguration { clientID: String, elementCallBaseURL: URL, elementCallBaseURLOverride: URL?, - colorScheme: ColorScheme) { + colorScheme: ColorScheme, + notifyOtherParticipants: Bool) { kind = .roomCall(roomProxy: roomProxy, clientProxy: clientProxy, clientID: clientID, elementCallBaseURL: elementCallBaseURL, elementCallBaseURLOverride: elementCallBaseURLOverride, - colorScheme: colorScheme) + colorScheme: colorScheme, + notifyOtherParticipants: notifyOtherParticipants) } /// A string representing the call being configured. @@ -72,7 +75,7 @@ struct ElementCallConfiguration { switch kind { case .genericCallLink(let url): url.absoluteString - case .roomCall(let roomProxy, _, _, _, _, _): + case .roomCall(let roomProxy, _, _, _, _, _, _): roomProxy.id } } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 1ecf1dfa20..9dffddc981 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -99,26 +99,18 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe incomingCallID = nil ongoingCallID = callID - let handle = CXHandle(type: .generic, value: roomDisplayName) - let startCallAction = CXStartCallAction(call: callID.callKitID, handle: handle) - startCallAction.isVideo = true + // Don't bother starting another CallKit session as it won't work properly + // https://developer.apple.com/forums//thread/767949?answerId=812951022#812951022 - do { - try await callController.request(CXTransaction(action: startCallAction)) - } catch { - MXLog.error("Failed requesting start call action with error: \(error)") - } + // let handle = CXHandle(type: .generic, value: roomDisplayName) + // let startCallAction = CXStartCallAction(call: callID.callKitID, handle: handle) + // startCallAction.isVideo = true - 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)") - } + // do { + // try await callController.request(CXTransaction(action: startCallAction)) + // } catch { + // MXLog.error("Failed requesting start call action with error: \(error)") + // } } func tearDownCallSession() { @@ -222,6 +214,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe // Reporting the call as ended imediately after answering it works around that // as EC gets access to media again and EX builds the right UI in `setupCallSession` // + // https://developer.apple.com/forums//thread/767949?answerId=812951022#812951022 + // // https://github.com/element-hq/element-x-ios/issues/3041 // https://forums.developer.apple.com/forums/thread/685268 // https://stackoverflow.com/questions/71483732/webrtc-running-from-wkwebview-avaudiosession-development-roadblock diff --git a/ElementX/Sources/Services/Emojis/EmojiProvider.swift b/ElementX/Sources/Services/Emojis/EmojiProvider.swift index 80c31ed0d5..104f65ce23 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProvider.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProvider.swift @@ -57,7 +57,7 @@ class EmojiProvider: EmojiProviderProtocol { } func frequentlyUsedSystemEmojis() -> [String] { - guard appSettings.frequentEmojisEnabled, !ProcessInfo.processInfo.isiOSAppOnMac else { + guard !ProcessInfo.processInfo.isiOSAppOnMac else { return [] } @@ -72,7 +72,7 @@ class EmojiProvider: EmojiProviderProtocol { } func markEmojiAsFrequentlyUsed(_ emoji: String) { - guard appSettings.frequentEmojisEnabled else { + guard !ProcessInfo.processInfo.isiOSAppOnMac else { return } diff --git a/ElementX/Sources/Services/Media/Provider/MediaSourceProxy.swift b/ElementX/Sources/Services/Media/Provider/MediaSourceProxy.swift index abfb3b2fee..2f1a85a826 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaSourceProxy.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaSourceProxy.swift @@ -23,8 +23,8 @@ struct MediaSourceProxy: Hashable { self.mimeType = mimeType } - init(url: URL, mimeType: String?) { - underlyingSource = mediaSourceFromUrl(url: url.absoluteString) + init(url: URL, mimeType: String?) throws { + underlyingSource = try MediaSource.fromUrl(url: url.absoluteString) self.url = URL(string: underlyingSource.url()) self.mimeType = mimeType } diff --git a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProtocol.swift b/ElementX/Sources/Services/MediaPlayer/MediaPlayerProtocol.swift deleted file mode 100644 index e955061139..0000000000 --- a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProtocol.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright 2023, 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. -// - -import Foundation - -enum MediaPlayerState { - case loading - case playing - case paused - case stopped - case error -} - -protocol MediaPlayerProtocol: AnyObject { - var mediaSource: MediaSourceProxy? { get } - var duration: TimeInterval { get } - var currentTime: TimeInterval { get } - var url: URL? { get } - var state: MediaPlayerState { get } - - func load(mediaSource: MediaSourceProxy, using url: URL, autoplay: Bool) - func reset() - func play() - func pause() - func stop() - func seek(to progress: Double) async -} - -// sourcery: AutoMockable -extension MediaPlayerProtocol { } diff --git a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProvider.swift b/ElementX/Sources/Services/MediaPlayer/MediaPlayerProvider.swift index 894269cc56..e6399259e2 100644 --- a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProvider.swift +++ b/ElementX/Sources/Services/MediaPlayer/MediaPlayerProvider.swift @@ -11,24 +11,12 @@ class MediaPlayerProvider: MediaPlayerProviderProtocol { private lazy var audioPlayer = AudioPlayer() private var audioPlayerStates: [String: AudioPlayerState] = [:] + var player: AudioPlayerProtocol { audioPlayer } + deinit { audioPlayerStates = [:] } - func player(for mediaSource: MediaSourceProxy) -> Result { - guard let mimeType = mediaSource.mimeType else { - MXLog.error("Unknown mime type") - return .failure(.unsupportedMediaType) - } - - if mimeType.starts(with: "audio/") { - return .success(audioPlayer) - } else { - MXLog.error("Unsupported media type: \(mediaSource.mimeType ?? "unknown")") - return .failure(.unsupportedMediaType) - } - } - // MARK: - AudioPlayer func playerState(for id: AudioPlayerStateIdentifier) -> AudioPlayerState? { diff --git a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProviderProtocol.swift b/ElementX/Sources/Services/MediaPlayer/MediaPlayerProviderProtocol.swift index c1cb2b74ee..479e3796af 100644 --- a/ElementX/Sources/Services/MediaPlayer/MediaPlayerProviderProtocol.swift +++ b/ElementX/Sources/Services/MediaPlayer/MediaPlayerProviderProtocol.swift @@ -13,7 +13,7 @@ enum MediaPlayerProviderError: Error { @MainActor protocol MediaPlayerProviderProtocol { - func player(for mediaSource: MediaSourceProxy) -> Result + var player: AudioPlayerProtocol { get } func playerState(for id: AudioPlayerStateIdentifier) -> AudioPlayerState? func register(audioPlayerState: AudioPlayerState) diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index d5fd7f7eb0..11fbda7147 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -29,16 +29,16 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { // MARK: NotificationManagerProtocol weak var delegate: NotificationManagerDelegate? - + func start() { - // Not implemented yet - // let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, - // title: L10n.actionQuickReply, - // options: []) + let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, + title: L10n.actionQuickReply, + options: []) let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message, - actions: [], + actions: [replyAction], intentIdentifiers: [], options: []) + let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite, actions: [], intentIdentifiers: [], diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index 40cbf1e8a9..ab181df3d3 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -55,7 +55,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { var senderAvatarMediaSource: MediaSourceProxy? { if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl, let senderAvatarURL = URL(string: senderAvatarURLString) { - return MediaSourceProxy(url: senderAvatarURL, mimeType: nil) + return try? MediaSourceProxy(url: senderAvatarURL, mimeType: nil) } return nil } @@ -63,7 +63,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { var roomAvatarMediaSource: MediaSourceProxy? { if let roomAvatarURLString = notificationItem.roomInfo.avatarUrl, let roomAvatarURL = URL(string: roomAvatarURLString) { - return MediaSourceProxy(url: roomAvatarURL, mimeType: nil) + return try? MediaSourceProxy(url: roomAvatarURL, mimeType: nil) } return nil } diff --git a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift index 4e2be76cc0..33bf851da8 100644 --- a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift @@ -11,36 +11,28 @@ import UIKit class InvitedRoomProxy: InvitedRoomProxyProtocol { private let roomListItem: RoomListItemProtocol - private let room: RoomProtocol + private let roomPreview: RoomPreviewProtocol + let info: BaseRoomInfoProxyProtocol + let ownUserID: String + let inviter: RoomMemberProxyProtocol? // 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 - + lazy var id: String = info.id + init(roomListItem: RoomListItemProtocol, - room: RoomProtocol) async throws { + roomPreview: RoomPreviewProtocol, + ownUserID: String) async throws { self.roomListItem = roomListItem - self.room = room - info = try await RoomInfoProxy(roomInfo: room.roomInfo()) - } - - func acceptInvitation() async -> Result { - do { - try await room.join() - return .success(()) - } catch { - MXLog.error("Failed accepting invitation with error: \(error)") - return .failure(.sdkError(error)) - } + self.roomPreview = roomPreview + self.ownUserID = ownUserID + info = try RoomPreviewInfoProxy(roomPreviewInfo: roomPreview.info()) + inviter = await roomPreview.inviter().map(RoomMemberProxy.init) } func rejectInvitation() async -> Result { do { - return try await .success(room.leave()) + return try await .success(roomPreview.leave()) } catch { MXLog.error("Failed rejecting invitiation with error: \(error)") return .failure(.sdkError(error)) diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 30211cc367..021d5df53d 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -315,47 +315,22 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } - func resend(itemID: TimelineItemIdentifier) async -> Result { - guard let transactionID = itemID.transactionID else { - MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") - return .failure(.missingTransactionID) - } - + func ignoreDeviceTrustAndResend(devices: [String: [String]], sendHandle: SendHandleProxy) async -> Result { do { - try await room.tryResend(transactionId: transactionID) + try await room.ignoreDeviceTrustAndResend(devices: devices, sendHandle: sendHandle.underlyingHandle) return .success(()) } catch { - MXLog.error("Failed resending \(transactionID) with error: \(error)") + MXLog.error("Failed trusting devices \(devices) and resending \(sendHandle.itemID) with error: \(error)") return .failure(.sdkError(error)) } } - func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result { - guard let transactionID = itemID.transactionID else { - MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") - return .failure(.missingTransactionID) - } - - do { - try await room.ignoreDeviceTrustAndResend(devices: devices, transactionId: transactionID) - return .success(()) - } catch { - MXLog.error("Failed trusting devices \(devices) and resending \(transactionID) with error: \(error)") - return .failure(.sdkError(error)) - } - } - - func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result { - guard let transactionID = itemID.transactionID else { - MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") - return .failure(.missingTransactionID) - } - + func withdrawVerificationAndResend(userIDs: [String], sendHandle: SendHandleProxy) async -> Result { do { - try await room.withdrawVerificationAndResend(userIds: userIDs, transactionId: transactionID) + try await room.withdrawVerificationAndResend(userIds: userIDs, sendHandle: sendHandle.underlyingHandle) return .success(()) } catch { - MXLog.error("Failed withdrawing verification of \(userIDs) and resending \(transactionID) with error: \(error)") + MXLog.error("Failed withdrawing verification of \(userIDs) and resending \(sendHandle.itemID) with error: \(error)") return .failure(.sdkError(error)) } } diff --git a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift index 74b640dc54..00321bf696 100644 --- a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift @@ -11,26 +11,26 @@ import UIKit class KnockedRoomProxy: KnockedRoomProxyProtocol { private let roomListItem: RoomListItemProtocol - private let room: RoomProtocol + private let roomPreview: RoomPreviewProtocol + let info: BaseRoomInfoProxyProtocol + let ownUserID: String // 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 - + lazy var id = info.id + init(roomListItem: RoomListItemProtocol, - room: RoomProtocol) async throws { + roomPreview: RoomPreviewProtocol, + ownUserID: String) throws { self.roomListItem = roomListItem - self.room = room - info = try await RoomInfoProxy(roomInfo: room.roomInfo()) + self.roomPreview = roomPreview + self.ownUserID = ownUserID + info = try RoomPreviewInfoProxy(roomPreviewInfo: roomPreview.info()) } func cancelKnock() async -> Result { do { - return try await .success(room.leave()) + return try await .success(roomPreview.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 index e402031310..5e9da29e3a 100644 --- a/ElementX/Sources/Services/Room/RoomInfoProxy.swift +++ b/ElementX/Sources/Services/Room/RoomInfoProxy.swift @@ -8,7 +8,19 @@ import Foundation import MatrixRustSDK -struct RoomInfoProxy { +protocol BaseRoomInfoProxyProtocol { + var id: String { get } + var displayName: String? { get } + var avatar: RoomAvatar { get } + var topic: String? { get } + var canonicalAlias: String? { get } + var avatarURL: URL? { get } + var activeMembersCount: Int { get } + var isDirect: Bool { get } + var isSpace: Bool { get } +} + +struct RoomInfoProxy: BaseRoomInfoProxyProtocol { let roomInfo: RoomInfo var id: String { roomInfo.id } @@ -53,4 +65,30 @@ struct RoomInfoProxy { var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) } var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) } var pinnedEventIDs: Set { Set(roomInfo.pinnedEventIds) } + var joinRule: JoinRule? { roomInfo.joinRule } +} + +struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { + let roomPreviewInfo: RoomPreviewInfo + + var id: String { roomPreviewInfo.roomId } + var displayName: String? { roomPreviewInfo.name } + var heroes: [RoomHero] { roomPreviewInfo.heroes ?? [] } + var topic: String? { roomPreviewInfo.topic } + var canonicalAlias: String? { roomPreviewInfo.canonicalAlias } + var avatarURL: URL? { roomPreviewInfo.avatarUrl.flatMap(URL.init) } + var isDirect: Bool { roomPreviewInfo.isDirect ?? false } + var isSpace: Bool { roomPreviewInfo.roomType == .space } + var activeMembersCount: Int { Int(roomPreviewInfo.numActiveMembers ?? roomPreviewInfo.numJoinedMembers) } + + /// 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) + } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index cf9c01fd8e..a19a735e79 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -33,14 +33,14 @@ protocol RoomProxyProtocol { // sourcery: AutoMockable protocol InvitedRoomProxyProtocol: RoomProxyProtocol { - var info: RoomInfoProxy { get } + var info: BaseRoomInfoProxyProtocol { get } + var inviter: RoomMemberProxyProtocol? { get } func rejectInvitation() async -> Result - func acceptInvitation() async -> Result } // sourcery: AutoMockable protocol KnockedRoomProxyProtocol: RoomProxyProtocol { - var info: RoomInfoProxy { get } + var info: BaseRoomInfoProxyProtocol { get } func cancelKnock() async -> Result } @@ -97,11 +97,9 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { /// https://spec.matrix.org/v1.9/client-server-api/#typing-notifications @discardableResult func sendTypingNotification(isTyping: Bool) async -> Result - func resend(itemID: TimelineItemIdentifier) async -> Result + func ignoreDeviceTrustAndResend(devices: [String: [String]], sendHandle: SendHandleProxy) async -> Result - func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result - - func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result + func withdrawVerificationAndResend(userIDs: [String], sendHandle: SendHandleProxy) async -> Result // MARK: - Room Flags diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index a5948fc4e7..70728678ba 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -35,7 +35,7 @@ struct RoomEventStringBuilder { case .redactedMessage: return prefix(L10n.commonMessageRemoved, with: displayName) case .sticker: - if messageEventStringBuilder.prefix == .messageType { + if messageEventStringBuilder.destination == .pinnedEvent { var string = AttributedString(L10n.commonSticker) string.bold() return string @@ -49,7 +49,7 @@ struct RoomEventStringBuilder { return stateEventStringBuilder .buildString(for: state, sender: sender, isOutgoing: isOutgoing) .map(AttributedString.init) - case .roomMembership(let userID, let displayName, let change): + case .roomMembership(let userID, let displayName, let change, _): return stateEventStringBuilder .buildString(for: change, memberUserID: userID, memberDisplayName: displayName, sender: sender, isOutgoing: isOutgoing) .map(AttributedString.init) @@ -63,7 +63,7 @@ struct RoomEventStringBuilder { memberIsYou: isOutgoing) .map(AttributedString.init) case .poll(let question, _, _, _, _, _, _): - if messageEventStringBuilder.prefix == .messageType { + if messageEventStringBuilder.destination == .pinnedEvent { let questionPlaceholder = "{question}" var finalString = AttributedString(L10n.commonPollSummary(questionPlaceholder)) finalString.bold() @@ -73,7 +73,7 @@ struct RoomEventStringBuilder { } return prefix(L10n.commonPollSummary(question), with: displayName) case .callInvite: - return prefix(L10n.commonCallInvite, with: displayName) + return prefix(L10n.commonUnsupportedCall, with: displayName) case .callNotify: return prefix(L10n.commonCallStarted, with: displayName) } @@ -96,7 +96,7 @@ struct RoomEventStringBuilder { RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID, shouldDisambiguateDisplayNames: false), messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(cacheKey: "pinnedEvents", mentionBuilder: PlainMentionBuilder()), - prefix: .messageType), + destination: .pinnedEvent), shouldDisambiguateDisplayNames: false, shouldPrefixSenderName: false) } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift index 2c7a1e07e2..444bba60ee 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift @@ -9,42 +9,46 @@ import Foundation import MatrixRustSDK struct RoomMessageEventStringBuilder { - enum Prefix { - case senderName - case messageType - case none + enum Destination { + /// Strings show on the room list as the last message + /// The sender will be prefixed in bold + case roomList + /// Events pinned to the banner on the top of the timeline + /// The message type will be prefixed in bold + case pinnedEvent + /// Shown in push notifications + /// No prefix + case notification } let attributedStringBuilder: AttributedStringBuilderProtocol - let prefix: Prefix + let destination: Destination func buildAttributedString(for messageType: MessageType, senderDisplayName: String) -> AttributedString { let message: AttributedString switch messageType { - // Message types that don't need a prefix. case .emote(content: let content): if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { return AttributedString(L10n.commonEmote(senderDisplayName, String(attributedMessage.characters))) } else { return AttributedString(L10n.commonEmote(senderDisplayName, content.body)) } - // Message types that should be prefixed with the sender's name. case .audio(content: let content): let isVoiceMessage = content.voice != nil var content = AttributedString(isVoiceMessage ? L10n.commonVoiceMessage : L10n.commonAudio) - if prefix == .messageType { + if destination == .pinnedEvent { content.bold() } message = content case .image(let content): - message = prefix == .messageType ? prefix(AttributedString(content.body), with: L10n.commonImage) : AttributedString("\(L10n.commonImage) - \(content.body)") + message = buildMessage(for: destination, caption: content.caption, type: L10n.commonImage) case .video(let content): - message = prefix == .messageType ? prefix(AttributedString(content.body), with: L10n.commonVideo) : AttributedString("\(L10n.commonVideo) - \(content.body)") + message = buildMessage(for: destination, caption: content.caption, type: L10n.commonVideo) case .file(let content): - message = prefix == .messageType ? prefix(AttributedString(content.body), with: L10n.commonFile) : AttributedString("\(L10n.commonFile) - \(content.body)") + message = buildMessage(for: destination, caption: content.caption, type: L10n.commonFile) case .location: var content = AttributedString(L10n.commonSharedLocation) - if prefix == .messageType { + if destination == .pinnedEvent { content.bold() } message = content @@ -64,13 +68,25 @@ struct RoomMessageEventStringBuilder { message = AttributedString(body) } - if prefix == .senderName { + if destination == .roomList { return prefix(message, with: senderDisplayName) } else { return message } } + private func buildMessage(for destination: Destination, caption: String?, type: String) -> AttributedString { + guard let caption else { + return AttributedString(type) + } + + if destination == .pinnedEvent { + return prefix(AttributedString(caption), with: type) + } else { + return AttributedString("\(type) - \(caption)") + } + } + private func prefix(_ eventSummary: AttributedString, with textToBold: String) -> AttributedString { let attributedEventSummary = AttributedString(eventSummary.string.trimmingCharacters(in: .whitespacesAndNewlines)) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 429cba58ad..7b9b3a5ed7 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -69,6 +69,20 @@ extension RoomSummary: CustomStringConvertible { - notificationMode: \(notificationMode?.rawValue ?? "nil") """ } + + /// Used where summaries are shown in a list e.g. message forwarding, + /// global search, share destination list etc. + var roomListDescription: String { + if isDirect { + return canonicalAlias ?? "" + } + + if let alias = canonicalAlias { + return alias + } + + return heroes.compactMap(\.displayName).formatted(.list(type: .and)) + } } extension RoomSummary { diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index cf77fa6da9..da1aef5116 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -250,35 +250,16 @@ enum RoomTimelineItemFixtures { 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")) + AudioRoomTimelineItem(isOutgoing: false, caption: "Listen to this!"), + AudioRoomTimelineItem(isOutgoing: true), + FileRoomTimelineItem(isOutgoing: false), + FileRoomTimelineItem(isOutgoing: true, caption: "Please check this ASAP!"), + ImageRoomTimelineItem(isOutgoing: false), + ImageRoomTimelineItem(isOutgoing: true, caption: "Isn't this pretty!"), + VideoRoomTimelineItem(isOutgoing: false, caption: "Woah, it was incredible!"), + VideoRoomTimelineItem(isOutgoing: true), + VoiceMessageRoomTimelineItem(isOutgoing: false), + VoiceMessageRoomTimelineItem(isOutgoing: true) ] } } @@ -301,3 +282,92 @@ private extension TextRoomTimelineItem { return newSelf } } + +private extension AudioRoomTimelineItem { + init(isOutgoing: Bool, caption: String? = nil) { + self.init(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: isOutgoing, + isEditable: isOutgoing, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"), + content: .init(filename: "audio.mp3", + caption: caption, + duration: 60, + waveform: nil, + source: try? .init(url: .mockMXCAudio, mimeType: nil), + fileSize: nil, + contentType: .mp3)) + } +} + +private extension FileRoomTimelineItem { + init(isOutgoing: Bool, caption: String? = nil) { + self.init(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: isOutgoing, + isEditable: isOutgoing, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"), + content: .init(filename: "file.pdf", + caption: caption, + source: try? .init(url: .mockMXCFile, mimeType: nil), + fileSize: nil, + thumbnailSource: nil, + contentType: .pdf)) + } +} + +private extension ImageRoomTimelineItem { + init(isOutgoing: Bool, caption: String? = nil) { + self.init(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: isOutgoing, + isEditable: isOutgoing, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"), + content: .init(filename: "image.jpg", + caption: caption, + imageInfo: .mockImage, + thumbnailInfo: nil, + blurhash: "KpE4oyayR5|GbHb];3j@of")) + } +} + +private extension VideoRoomTimelineItem { + init(isOutgoing: Bool, caption: String? = nil) { + self.init(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: isOutgoing, + isEditable: isOutgoing, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"), + content: .init(filename: "video.mp4", + caption: caption, + videoInfo: .mockVideo, + thumbnailInfo: .mockThumbnail, + blurhash: "KtI~70X5V?yss9oyrYs:t6")) + } +} + +private extension VoiceMessageRoomTimelineItem { + init(isOutgoing: Bool) { + self.init(id: .randomEvent, + timestamp: "10:47 am", + isOutgoing: isOutgoing, + isEditable: isOutgoing, + canBeRepliedTo: true, + isThreaded: false, + sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"), + content: .init(filename: "message.ogg", + duration: 10, + waveform: .mockWaveform, + source: try? .init(url: .mockMXCAudio, mimeType: nil), + fileSize: nil, + contentType: .audio)) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index e615165936..b8cf4387e2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -91,6 +91,13 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { html: String?, intentionalMentions: IntentionalMentions) async { } + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async { } + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async { } + func redact(_ eventOrTransactionID: EventOrTransactionId) async { } func pin(eventID: String) async { } @@ -104,6 +111,10 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo { .init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil) } + + func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? { + nil + } func retryDecryption(for sessionID: String) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 09681aa418..bdf6a5a6e1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import IntentsUI import MatrixRustSDK import UIKit @@ -14,7 +15,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private let roomProxy: JoinedRoomProxyProtocol private let liveTimelineProvider: RoomTimelineProviderProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol + private let mediaProvider: MediaProviderProtocol private let appSettings: AppSettings + private let serialDispatchQueue: DispatchQueue let callbacks = PassthroughSubject() @@ -40,11 +43,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol { timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy liveTimelineProvider = timelineProxy.timelineProvider self.timelineItemFactory = timelineItemFactory + self.mediaProvider = mediaProvider self.appSettings = appSettings + serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) activeTimeline = timelineProxy @@ -153,11 +159,61 @@ class RoomTimelineController: RoomTimelineControllerProtocol { intentionalMentions: intentionalMentions) { case .success: MXLog.info("Finished sending message") + await donateSendMessageIntent() case .failure(let error): MXLog.error("Failed sending message with error: \(error)") } } + private func donateSendMessageIntent() async { + guard let displayName = roomProxy.details.name ?? roomProxy.details.canonicalAlias, !displayName.isEmpty else { + MXLog.error("Failed donating send message intent, room missing name or alias.") + return + } + + let groupName = INSpeakableString(spokenPhrase: displayName) + + let sendMessageIntent = INSendMessageIntent(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: nil, + speakableGroupName: groupName, + conversationIdentifier: roomProxy.id, + serviceName: nil, + sender: nil, + attachments: nil) + + let avatarURL = switch roomProxy.details.avatar { + case .room(_, _, let avatarURL): + avatarURL + case .heroes(let userProfiles): + userProfiles.first?.avatarURL + } + + func addPlacehoder() { + if let imageData = Avatars.generatePlaceholderAvatarImageData(name: displayName, id: roomProxy.id, size: .init(width: 100, height: 100)) { + sendMessageIntent.setImage(INImage(imageData: imageData), forParameterNamed: \.speakableGroupName) + } + } + + if let avatarURL, let mediaSource = try? MediaSourceProxy(url: avatarURL, mimeType: nil) { + if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { + sendMessageIntent.setImage(INImage(imageData: avatarData), forParameterNamed: \.speakableGroupName) + } else { + addPlacehoder() + } + } else { + addPlacehoder() + } + + let interaction = INInteraction(intent: sendMessageIntent, response: nil) + + do { + try await interaction.donate() + } catch { + MXLog.error("Failed donating send message intent with error: \(error)") + } + } + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async { MXLog.info("Toggle reaction \(reaction) to \(eventOrTransactionID)") @@ -180,7 +236,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { html: html, intentionalMentions: intentionalMentions.toRustMentions()) - switch await activeTimeline.edit(eventOrTransactionID, newContent: messageContent) { + switch await activeTimeline.edit(eventOrTransactionID, newContent: .roomMessage(content: messageContent)) { case .success: MXLog.info("Finished editing message by event") case let .failure(error): @@ -188,6 +244,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async { + // We're waiting on an API for including mentions: https://github.com/matrix-org/matrix-rust-sdk/issues/4302 + MXLog.info("Editing timeline item caption: \(eventOrTransactionID) in \(roomID)") + + // When formattedCaption is nil, caption will be parsed as markdown and generate the HTML for us. + let newContent = createCaptionEdit(caption: message, formattedCaption: html.map { .init(format: .html, body: $0) }) + switch await activeTimeline.edit(eventOrTransactionID, newContent: newContent) { + case .success: + MXLog.info("Finished editing caption") + case let .failure(error): + MXLog.error("Failed editing caption with error: \(error)") + } + } + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async { + // Set a `nil` caption to remove it from the event. + let newContent = createCaptionEdit(caption: nil, formattedCaption: nil) + switch await activeTimeline.edit(eventOrTransactionID, newContent: newContent) { + case .success: + MXLog.info("Finished removing caption.") + case let .failure(error): + MXLog.error("Failed removing caption with error: \(error)") + } + } + func redact(_ eventOrTransactionID: EventOrTransactionId) async { MXLog.info("Send redaction in \(roomID)") @@ -250,6 +334,21 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .init(model: "Unknown item", originalJSON: nil, latestEditJSON: nil) } + func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? { + for timelineItemProxy in activeTimelineProvider.itemProxies { + switch timelineItemProxy { + case .event(let item): + if item.id == itemID { + return item.sendHandle.map { .init(itemID: itemID, underlyingHandle: $0) } + } + default: + continue + } + } + + return nil + } + func retryDecryption(for sessionID: String) async { await activeTimeline.retryDecryption(for: sessionID) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index d090f7d90e..4e86953c25 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -10,16 +10,19 @@ import Foundation struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, - timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol { RoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.timeline, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, + mediaProvider: mediaProvider, appSettings: ServiceLocator.shared.settings) } func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { return nil } @@ -27,6 +30,7 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { timelineProxy: pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory, + mediaProvider: mediaProvider, appSettings: ServiceLocator.shared.settings) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index 71746fd849..0f1093c8f5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -11,9 +11,11 @@ import Foundation protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, - timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 125454d3cc..e32356e25a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -56,6 +56,13 @@ protocol RoomTimelineControllerProtocol { html: String?, intentionalMentions: IntentionalMentions) async + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async func redact(_ eventOrTransactionID: EventOrTransactionId) async @@ -68,6 +75,8 @@ protocol RoomTimelineControllerProtocol { func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo + func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? + func retryDecryption(for sessionID: String) async func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 346fad5845..bd024e4967 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -135,6 +135,8 @@ class EventTimelineItemProxy { lazy var shieldState = item.lazyProvider.getShields(strict: false) + lazy var sendHandle = item.lazyProvider.getSendHandle() + lazy var readReceipts = item.readReceipts } @@ -179,11 +181,153 @@ struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible { } } -extension Receipt { - var dateTimestamp: Date? { - guard let timestamp else { +struct SendHandleProxy: Hashable { + enum Error: Swift.Error { + case sdkError(Swift.Error) + } + + let itemID: TimelineItemIdentifier + let underlyingHandle: SendHandle + + func resend() async -> Result { + do { + try await underlyingHandle.tryResend() + return .success(()) + } catch { + return .failure(.sdkError(error)) + } + } + + // MARK: - Hashable + + static func == (lhs: SendHandleProxy, rhs: SendHandleProxy) -> Bool { + lhs.itemID == rhs.itemID + } + + func hash(into hasher: inout Hasher) { + hasher.combine(itemID) + } + + static var mock: SendHandleProxy { + .init(itemID: .event(uniqueID: .init(id: UUID().uuidString), + eventOrTransactionID: .eventId(eventId: UUID().uuidString)), + underlyingHandle: .init(noPointer: .init())) + } +} + +struct VideoInfoProxy: Hashable { + let source: MediaSourceProxy + private(set) var duration: TimeInterval + private(set) var size: CGSize? + private(set) var aspectRatio: CGFloat? + private(set) var mimeType: String? + + init(source: MediaSource, duration: TimeInterval, width: UInt64?, height: UInt64?, mimeType: String?) { + self.source = MediaSourceProxy(source: source, mimeType: mimeType) + self.duration = duration + + let mediaInfo = MediaInfoProxy(width: width, height: height, mimeType: mimeType) + size = mediaInfo.size + aspectRatio = mediaInfo.aspectRatio + self.mimeType = mediaInfo.mimeType + } + + // MARK: - Mocks + + private init(source: MediaSourceProxy, duration: TimeInterval, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?) { + self.source = source + self.duration = duration + self.size = size + self.aspectRatio = aspectRatio + self.mimeType = mimeType + } + + static var mockVideo: VideoInfoProxy { + guard let mediaSource = try? MediaSourceProxy(url: .mockMXCVideo, mimeType: nil) else { + fatalError("Invalid mock media source URL") + } + + return .init(source: mediaSource, + duration: 100, + size: .init(width: 1920, height: 1080), + aspectRatio: 1.78, + mimeType: nil) + } +} + +struct ImageInfoProxy: Hashable { + let source: MediaSourceProxy + private(set) var size: CGSize? + private(set) var aspectRatio: CGFloat? + private(set) var mimeType: String? + + init?(source: MediaSource?, width: UInt64?, height: UInt64?, mimeType: String?) { + guard let source else { return nil } - return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) + + self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType) + } + + init(source: MediaSource, width: UInt64?, height: UInt64?, mimeType: String?) { + self.init(source: .init(source: source, mimeType: mimeType), width: width, height: height, mimeType: mimeType) + } + + init(source: MediaSourceProxy, width: UInt64?, height: UInt64?, mimeType: String?) { + self.source = source + + let mediaInfo = MediaInfoProxy(width: width, height: height, mimeType: mimeType) + size = mediaInfo.size + aspectRatio = mediaInfo.aspectRatio + self.mimeType = mediaInfo.mimeType + } + + // MARK: - Mocks + + private init(source: MediaSourceProxy, size: CGSize?, aspectRatio: CGFloat?, mimeType: String?) { + self.source = source + self.size = size + self.aspectRatio = aspectRatio + self.mimeType = mimeType + } + + static var mockImage: ImageInfoProxy { + guard let mediaSource = try? MediaSourceProxy(url: .mockMXCImage, mimeType: "image/png") else { + fatalError("Invalid mock media source URL") + } + + return .init(source: mediaSource, + size: .init(width: 100, height: 100), + aspectRatio: 1, + mimeType: "image/png") + } + + static var mockThumbnail: ImageInfoProxy { + guard let mediaSource = try? MediaSourceProxy(url: .mockMXCImage, mimeType: "image/png") else { + fatalError("Invalid mock media source URL") + } + + return .init(source: mediaSource, + size: nil, + aspectRatio: nil, + mimeType: nil) + } +} + +struct MediaInfoProxy: Hashable { + private(set) var size: CGSize? + private(set) var mimeType: String? + private(set) var aspectRatio: CGFloat? + + init(width: UInt64?, height: UInt64?, mimeType: String?) { + if let width, let height { + size = .init(width: CGFloat(width), height: CGFloat(height)) + + if width > 0, height > 0 { + aspectRatio = CGFloat(width) / CGFloat(height) + } + } + + self.mimeType = mimeType } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index 6b4ae9f5ea..7340377662 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -26,18 +26,31 @@ protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { } extension EventBasedMessageTimelineItemProtocol { - var hasMediaCaption: Bool { + var supportsMediaCaption: Bool { + switch contentType { + case .audio, .file, .image, .video: + true + case .emote, .notice, .text, .location, .voice: + false + } + } + + var mediaCaption: String? { switch contentType { case .audio(let content): - content.caption != nil + content.caption case .file(let content): - content.caption != nil + content.caption case .image(let content): - content.caption != nil + content.caption case .video(let content): - content.caption != nil + content.caption case .emote, .notice, .text, .location, .voice: - false + nil } } + + var hasMediaCaption: Bool { + mediaCaption != nil + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index aa99847b79..0150401ff9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -99,4 +99,14 @@ extension EventBasedTimelineItemProtocol { return true } } + + var supportsMediaCaption: Bool { + guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else { return false } + return messageBasedItem.supportsMediaCaption + } + + var hasMediaCaption: Bool { + guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else { return false } + return messageBasedItem.hasMediaCaption + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift index 4ca8d5c1f1..0691ed5dec 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItemContent.swift @@ -17,5 +17,6 @@ struct AudioRoomTimelineItemContent: Hashable { let duration: TimeInterval let waveform: EstimatedWaveform? let source: MediaSourceProxy? + let fileSize: UInt? let contentType: UTType? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift index 67f00ee1ba..3a2f52cf56 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItemContent.swift @@ -15,6 +15,7 @@ struct FileRoomTimelineItemContent: Hashable { /// The original textual representation of the formatted caption directly from the event (usually HTML code) var formattedCaptionHTMLString: String? let source: MediaSourceProxy? + let fileSize: UInt? let thumbnailSource: MediaSourceProxy? let contentType: UTType? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift index 01eb841510..f4c601d054 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItemContent.swift @@ -14,11 +14,10 @@ struct ImageRoomTimelineItemContent: Hashable { 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? - var height: CGFloat? - var aspectRatio: CGFloat? + + let imageInfo: ImageInfoProxy + let thumbnailInfo: ImageInfoProxy? + var blurhash: String? var contentType: UTType? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift index 0790970edf..ea50f20781 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItemContent.swift @@ -14,12 +14,10 @@ struct VideoRoomTimelineItemContent: Hashable { 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? - var width: CGFloat? - var height: CGFloat? - var aspectRatio: CGFloat? + + let videoInfo: VideoInfoProxy + let thumbnailInfo: ImageInfoProxy? + var blurhash: String? var contentType: UTType? } 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 0e70287ac5..df55cf05df 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI struct VoiceMessageRoomTimelineView: View { - @EnvironmentObject private var context: TimelineViewModel.Context + @Environment(\.timelineContext) private var context private let timelineItem: VoiceMessageRoomTimelineItem private let playerState: AudioPlayerState @State private var resumePlaybackAfterScrubbing = false @@ -31,22 +31,22 @@ struct VoiceMessageRoomTimelineView: View { } private func onPlaybackPlayPause() { - context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) + context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) } private func onPlaybackSeek(_ progress: Double) { - context.send(viewAction: .handleAudioPlayerAction(.seek(itemID: timelineItem.id, progress: progress))) + context?.send(viewAction: .handleAudioPlayerAction(.seek(itemID: timelineItem.id, progress: progress))) } private func onPlaybackScrubbing(_ dragging: Bool) { if dragging { if playerState.playbackState == .playing { resumePlaybackAfterScrubbing = true - context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) + context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) } } else { if resumePlaybackAfterScrubbing { - context.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) + context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) resumePlaybackAfterScrubbing = false } } @@ -67,6 +67,7 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview { duration: 300, waveform: EstimatedWaveform.mockWaveform, source: nil, + fileSize: nil, contentType: nil)) static let playerState = AudioPlayerState(id: .timelineItemIdentifier(timelineItemIdentifier), diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift index eaed672901..f6d5e7e0d5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift @@ -19,6 +19,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { case verificationViolation case insecureDevice case unknown + case historicalMessage } let id: TimelineItemIdentifier diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift index 8c64723186..466ba2a0f4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift @@ -17,11 +17,8 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { let sender: TimelineItemSender - let imageURL: URL + let imageInfo: ImageInfoProxy - var width: CGFloat? - var height: CGFloat? - var aspectRatio: CGFloat? var blurhash: String? var properties = RoomTimelineItemProperties() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 7a3d1e9ba3..c26fc8d2b8 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -33,12 +33,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .redactedMessage: return buildRedactedTimelineItem(eventItemProxy, isOutgoing) case .sticker(let body, let imageInfo, let mediaSource): - guard let url = URL(string: mediaSource.url()) else { - MXLog.error("Invalid sticker url string: \(mediaSource.url())") - return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing) - } - - return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing) + return buildStickerTimelineItem(eventItemProxy, body, imageInfo, mediaSource, isOutgoing) case .failedToParseMessageLike(let eventType, let error): return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing) case .failedToParseState(let eventType, _, let error): @@ -50,7 +45,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return nil } return buildStateTimelineItem(for: eventItemProxy, state: content, isOutgoing: isOutgoing) - case .roomMembership(userId: let userID, let displayName, change: let change): + case .roomMembership(userId: let userID, let displayName, change: let change, _): if isDM, change == .joined, userID == self.userID { return nil } @@ -118,15 +113,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ body: String, - _ imageInfo: ImageInfo, - _ imageURL: URL, + _ info: MatrixRustSDK.ImageInfo, + _ mediaSource: MediaSource, _ isOutgoing: Bool) -> RoomTimelineItemProtocol { - var aspectRatio: CGFloat? - let width = imageInfo.width.map(CGFloat.init) - let height = imageInfo.height.map(CGFloat.init) - if let width, let height, width > 0, height > 0 { - aspectRatio = width / height - } + let imageInfo = ImageInfoProxy(source: mediaSource, width: info.width, height: info.height, mimeType: info.mimetype) return StickerRoomTimelineItem(id: eventItemProxy.id, body: body, @@ -135,11 +125,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { isEditable: eventItemProxy.isEditable, canBeRepliedTo: eventItemProxy.canBeRepliedTo, sender: eventItemProxy.sender, - imageURL: imageURL, - width: width, - height: height, - aspectRatio: aspectRatio, - blurhash: imageInfo.blurhash, + imageInfo: imageInfo, + blurhash: info.blurhash, properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), @@ -166,6 +153,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .sentBeforeWeJoined: encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .sentBeforeWeJoined) errorLabel = L10n.commonUnableToDecryptNoAccess + case .historicalMessage: + encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .historicalMessage) + errorLabel = L10n.commonUnableToDecryptNoAccess } case .olmV1Curve25519AesSha2(let senderKey): encryptionType = .olmV1Curve25519AesSha2(senderKey: senderKey) @@ -511,60 +501,57 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { duration: messageContent.audio?.duration ?? 0, waveform: waveform, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), - contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) + fileSize: messageContent.info?.size.map(UInt.init), + contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) } - + 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) + let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource, + width: messageContent.info?.thumbnailInfo?.width, + height: messageContent.info?.thumbnailInfo?.height, + mimeType: messageContent.info?.thumbnailInfo?.mimetype) - var aspectRatio: CGFloat? - if let width, let height, width > 0, height > 0 { - aspectRatio = width / height - } + let imageInfo = ImageInfoProxy(source: messageContent.source, + width: messageContent.info?.width, + height: messageContent.info?.height, + mimeType: messageContent.info?.mimetype) 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, - height: height, - aspectRatio: aspectRatio, + imageInfo: imageInfo, + thumbnailInfo: thumbnailInfo, blurhash: messageContent.info?.blurhash, - contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) + contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) } - + 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) + let thumbnailInfo = ImageInfoProxy(source: messageContent.info?.thumbnailSource, + width: messageContent.info?.thumbnailInfo?.width, + height: messageContent.info?.thumbnailInfo?.height, + mimeType: messageContent.info?.thumbnailInfo?.mimetype) - var aspectRatio: CGFloat? - if let width, let height, width > 0, height > 0 { - aspectRatio = width / height - } + let videoInfo = VideoInfoProxy(source: messageContent.source, + duration: messageContent.info?.duration ?? 0, + width: messageContent.info?.width, + height: messageContent.info?.height, + mimeType: messageContent.info?.mimetype) 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, - width: width, - height: height, - aspectRatio: aspectRatio, + videoInfo: videoInfo, + thumbnailInfo: thumbnailInfo, blurhash: messageContent.info?.blurhash, - contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) + contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) } private func buildLocationTimelineItemContent(_ locationContent: LocationContent) -> LocationRoomTimelineItemContent { @@ -584,8 +571,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { formattedCaption: formattedCaption, formattedCaptionHTMLString: htmlCaption, source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype), + fileSize: messageContent.info?.size.map(UInt.init), thumbnailSource: thumbnailSource, - contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body)) + contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) } private func buildNoticeTimelineItemContent(_ messageContent: NoticeMessageContent) -> NoticeRoomTimelineItemContent { @@ -777,3 +765,12 @@ private extension RepliedToEventDetails { } } } + +private extension Receipt { + var dateTimestamp: Date? { + guard let timestamp else { + return nil + } + return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 64b5152d69..93238d41ea 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -164,9 +164,9 @@ final class TimelineProxy: TimelineProxyProtocol { } } - func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: EditedContent) async -> Result { do { - try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: .roomMessage(content: newContent)) + try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: newContent) MXLog.info("Finished editing timeline item: \(eventOrTransactionID)") @@ -223,18 +223,16 @@ final class TimelineProxy: TimelineProxyProtocol { func sendAudio(url: URL, audioInfo: AudioInfo, - progressSubject: CurrentValueSubject?, + caption: String?, 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) - }, - useSendQueue: false) + caption: caption, + formattedCaption: nil, // Rust will build this from the caption's markdown. + progressWatcher: nil, + useSendQueue: true) await requestHandle(handle) @@ -251,16 +249,16 @@ final class TimelineProxy: TimelineProxyProtocol { func sendFile(url: URL, fileInfo: FileInfo, - progressSubject: CurrentValueSubject?, + caption: String?, 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) - }, - useSendQueue: false) + caption: caption, + formattedCaption: nil, // Rust will build this from the caption's markdown. + progressWatcher: nil, + useSendQueue: true) await requestHandle(handle) @@ -278,19 +276,17 @@ final class TimelineProxy: TimelineProxyProtocol { func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, - progressSubject: CurrentValueSubject?, + caption: String?, 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) - }, - useSendQueue: false) + caption: caption, + formattedCaption: nil, // Rust will build this from the caption's markdown. + progressWatcher: nil, + useSendQueue: true) await requestHandle(handle) @@ -323,22 +319,21 @@ final class TimelineProxy: TimelineProxyProtocol { return .success(()) } + // swiftlint:disable:next function_parameter_count func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, - progressSubject: CurrentValueSubject?, + caption: String?, 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) - }, - useSendQueue: false) + caption: caption, + formattedCaption: nil, // Rust will build this from the caption's markdown. + progressWatcher: nil, + useSendQueue: true) await requestHandle(handle) @@ -356,7 +351,6 @@ final class TimelineProxy: TimelineProxyProtocol { func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], - progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { MXLog.info("Sending voice message") @@ -365,10 +359,8 @@ final class TimelineProxy: TimelineProxyProtocol { waveform: waveform, caption: nil, formattedCaption: nil, - progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }, - useSendQueue: false) + progressWatcher: nil, + useSendQueue: true) await requestHandle(handle) diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index e1c8b5f6fe..d1520cbbeb 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -38,7 +38,7 @@ protocol TimelineProxyProtocol { func paginateForwards(requestSize: UInt16) async -> Result func edit(_ eventOrTransactionID: EventOrTransactionId, - newContent: RoomMessageEventContentWithoutRelation) async -> Result + newContent: EditedContent) async -> Result func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result @@ -51,18 +51,18 @@ protocol TimelineProxyProtocol { func sendAudio(url: URL, audioInfo: AudioInfo, - progressSubject: CurrentValueSubject?, + caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result func sendFile(url: URL, fileInfo: FileInfo, - progressSubject: CurrentValueSubject?, + caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, - progressSubject: CurrentValueSubject?, + caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result func sendLocation(body: String, @@ -74,13 +74,12 @@ protocol TimelineProxyProtocol { func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, - progressSubject: CurrentValueSubject?, + caption: String?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], - progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result diff --git a/ElementX/Sources/Services/UserSession/SessionDirectories.swift b/ElementX/Sources/Services/UserSession/SessionDirectories.swift index 8a93aed129..ae768f30ea 100644 --- a/ElementX/Sources/Services/UserSession/SessionDirectories.swift +++ b/ElementX/Sources/Services/UserSession/SessionDirectories.swift @@ -51,6 +51,14 @@ struct SessionDirectories: Hashable, Codable { } } + /// Check that mission critical files (the crypto db) are still in the right place when restoring a session + /// iOS might decide to move the app with its user defaults and keychain but without + /// some of the files stored in the shared container e.g. after a device transfer, offloading etc. + /// If that happens we should fail the session restoration. + func isNonTransientUserDataValid() -> Bool { + FileManager.default.fileExists(atPath: dataPath.appending("/matrix-sdk-crypto.sqlite3")) + } + private func deleteFiles(at url: URL, with prefix: String) throws { let sessionDirectoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) for url in sessionDirectoryContents where url.lastPathComponent.hasPrefix(prefix) { @@ -64,19 +72,19 @@ extension SessionDirectories { init() { let sessionDirectoryName = UUID().uuidString dataDirectory = .sessionsBaseDirectory.appending(component: sessionDirectoryName) - cacheDirectory = .cachesBaseDirectory.appending(component: sessionDirectoryName) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: sessionDirectoryName) } /// Creates the session directories for a user who signed in before the data directory was stored. init(userID: String) { dataDirectory = .legacySessionDirectory(for: userID) - cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) } /// Creates the session directories for a user who has a single session directory stored without a separate caches directory. init(dataDirectory: URL) { self.dataDirectory = dataDirectory - cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) } } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 310475032a..1f4ca5e5b5 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -118,6 +118,11 @@ class UserSessionStore: UserSessionStoreProtocol { MXLog.info("Restoring client with encrypted store.") } + guard credentials.restorationToken.sessionDirectories.isNonTransientUserDataValid() else { + MXLog.error("Failed restoring login, missing non-transient user data") + return .failure(.failedRestoringLogin) + } + let homeserverURL = credentials.restorationToken.session.homeserverUrl let builder = ClientBuilder diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift index e907aed2c0..cd9e14f329 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift @@ -101,13 +101,12 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { await previewAudioPlayerState.attachAudioPlayer(audioPlayer) } - if audioPlayer.url == url { + if audioPlayer.playbackURL == url { audioPlayer.play() return .success(()) } - let pendingMediaSource = MediaSourceProxy(url: url, mimeType: mp4accMimeType) - audioPlayer.load(mediaSource: pendingMediaSource, using: url, autoplay: true) + audioPlayer.load(sourceURL: url, playbackURL: url, autoplay: true) return .success(()) } @@ -179,8 +178,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile, audioInfo: audioInfo, - waveform: waveform, - progressSubject: nil) { _ in } + waveform: waveform) { _ in } if case .failure(let error) = result { MXLog.error("Failed to send the voice message. \(error)") @@ -242,10 +240,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { previewAudioPlayerState = await AudioPlayerState(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: recordingDuration, waveform: EstimatedWaveform(data: [])) // Build the preview audio player - let mediaSource = MediaSourceProxy(url: url, mimeType: mp4accMimeType) - guard case .success(let mediaPlayer) = await mediaPlayerProvider.player(for: mediaSource), let audioPlayer = mediaPlayer as? AudioPlayerProtocol else { - return .failure(.previewNotAvailable) - } + let audioPlayer = await mediaPlayerProvider.player previewAudioPlayer = audioPlayer return .success(()) diff --git a/ElementX/Sources/ShareExtension/ShareExtensionModels.swift b/ElementX/Sources/ShareExtension/ShareExtensionModels.swift new file mode 100644 index 0000000000..a5147b2c93 --- /dev/null +++ b/ElementX/Sources/ShareExtension/ShareExtensionModels.swift @@ -0,0 +1,30 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation + +enum ShareExtensionConstants { + static let urlPath = "share" +} + +enum ShareExtensionPayload: Hashable, Codable { + case mediaFile(roomID: String?, mediaFile: ShareExtensionMediaFile) + case text(roomID: String?, text: String) + + var roomID: String? { + switch self { + case .mediaFile(let roomID, _), + .text(let roomID, _): + roomID + } + } +} + +struct ShareExtensionMediaFile: Hashable, Codable { + let url: URL + let suggestedName: String? +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 01bea2b808..56669b93f3 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -253,7 +253,7 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -272,7 +272,7 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.default let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -291,7 +291,7 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -313,7 +313,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -335,7 +335,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -357,7 +357,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -380,7 +380,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -402,7 +402,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -423,7 +423,7 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -458,7 +458,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -480,7 +480,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -502,7 +502,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), - roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: .mockMXCAvatar)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -684,6 +684,7 @@ class MockScreen: Identifiable { timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), + mediaProvider: MediaProviderMock(configuration: .init()), appSettings: ServiceLocator.shared.settings) let flowCoordinator = UserSessionFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)), diff --git a/ElementX/Sources/UITests/UITestsNotificationCenter.swift b/ElementX/Sources/UITests/UITestsNotificationCenter.swift index 32c2d8dde4..8e2c594a38 100644 --- a/ElementX/Sources/UITests/UITestsNotificationCenter.swift +++ b/ElementX/Sources/UITests/UITestsNotificationCenter.swift @@ -8,14 +8,14 @@ import Combine import SwiftUI -@MainActor /// A notification center that can be injected in the app to post notifications /// that are sent from the UI tests runner. Usage: /// - Create an instance of the center in the screen you want to test and call `startListening`. /// - Create a `UITestSignalling.Client` in the `.tests` mode in your tests. /// - Start the app from the tests and call `client.waitForApp()` to establish communication. /// - Send the notification from the tests you would like posted in the app. -class UITestsNotificationCenter: NotificationCenter { +@MainActor +class UITestsNotificationCenter: NotificationCenter, @unchecked Sendable { // periphery:ignore - retaining purpose private var client: UITestsSignalling.Client? private var signalCancellable: AnyCancellable? diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index fb51c555a9..39c81a8d9c 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -54,7 +54,7 @@ Application CFBundleURLSchemes - io.element + $(BASE_BUNDLE_IDENTIFIER) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 33e3b1b850..bb92ccae3d 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -66,7 +66,7 @@ targets: CFBundleTypeRole: Editor, CFBundleURLName: "Application", CFBundleURLSchemes: [ - io.element + $(BASE_BUNDLE_IDENTIFIER) ] } ] @@ -124,6 +124,7 @@ targets: SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h PILLS_UT_TYPE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).pills + DEVELOPMENT_ASSET_PATHS: DevelopmentAssets/Media OTHER_SWIFT_FLAGS: - "-DIS_MAIN_APP" @@ -189,9 +190,11 @@ targets: dependencies: - target: NSE + - target: ShareExtension # not used yet # - target: NCE - package: MatrixRustSDK + embed: true - package: Compound - package: Algorithms - package: AnalyticsEvents @@ -225,3 +228,4 @@ targets: - path: ../Resources - path: ../SupportingFiles - path: ../../Tools/Scripts/Templates/SimpleScreenExample/ElementX + - path: ../../DevelopmentAssets/Media diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index f767a9538d..383098d176 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -131,7 +131,7 @@ struct NotificationContentBuilder { private func processCallInviteEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider) - notification.body = L10n.commonCallInvite + notification.body = L10n.commonUnsupportedCall return notification } diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 3ea1ed8a6c..0df8b17c13 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -33,7 +33,7 @@ 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()), destination: .notification), settings: settings) private let keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) @@ -66,10 +66,10 @@ class NotificationServiceExtension: UNNotificationServiceExtension { handler = contentHandler modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent - NSELogger.configure(logLevel: settings.logLevel) + ExtensionLogger.configure(currentTarget: "nse", logLevel: settings.logLevel) MXLog.info("\(tag) #########################################") - NSELogger.logMemory(with: tag) + ExtensionLogger.logMemory(with: tag) MXLog.info("\(tag) Payload came: \(request.content.userInfo)") Self.serialQueue.sync { @@ -201,7 +201,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension { deinit { cleanUp() - NSELogger.logMemory(with: tag) + ExtensionLogger.logMemory(with: tag) MXLog.info("\(tag) deinit") } diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 417469aa13..dea122a13e 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -77,7 +77,7 @@ targets: - path: ../../ElementX/Sources/Application/AppSettings.swift - path: ../../ElementX/Sources/Generated/Assets.swift - path: ../../ElementX/Sources/Generated/Strings.swift - - path: ../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../ElementX/Sources/Other/Avatars.swift - path: ../../ElementX/Sources/Other/CurrentValuePublisher.swift - path: ../../ElementX/Sources/Other/Extensions/AttributedString.swift - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index c42a3a3024..33d08949e3 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -305,6 +305,30 @@ extension PreviewTests { } } + func test_knockRequestCell() { + for preview in KnockRequestCell_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + + func test_knockRequestsBannerView() { + for preview in KnockRequestsBannerView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + + func test_knockRequestsListEmptyStateView() { + for preview in KnockRequestsListEmptyStateView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + + func test_knockRequestsListScreen() { + for preview in KnockRequestsListScreen_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_legalInformationScreen() { for preview in LegalInformationScreen_Previews._allPreviews { assertSnapshots(matching: preview) @@ -701,6 +725,12 @@ extension PreviewTests { } } + func test_roomSelectionScreen() { + for preview in RoomSelectionScreen_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_sFNumberedListView() { for preview in SFNumberedListView_Previews._allPreviews { assertSnapshots(matching: preview) @@ -785,6 +815,12 @@ extension PreviewTests { } } + func test_stackedAvatarsView() { + for preview in StackedAvatarsView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_startChatScreen() { for preview in StartChatScreen_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/PreviewTests.swift b/PreviewTests/Sources/PreviewTests.swift index 5fd769481a..5558714caf 100644 --- a/PreviewTests/Sources/PreviewTests.swift +++ b/PreviewTests/Sources/PreviewTests.swift @@ -15,7 +15,7 @@ import XCTest class PreviewTests: XCTestCase { private let deviceConfig: ViewImageConfig = .iPhoneX private let simulatorDevice: String? = "iPhone14,6" // iPhone SE 3rd Generation - private let requiredOSVersion = (major: 18, minor: 0) + private let requiredOSVersion = (major: 18, minor: 1) private let snapshotDevices = ["iPhone 16", "iPad"] private var recordMode: SnapshotTestingConfiguration.Record = .missing diff --git a/PreviewTests/SupportingFiles/target.yml b/PreviewTests/SupportingFiles/target.yml index d0620f3343..560bb0c262 100644 --- a/PreviewTests/SupportingFiles/target.yml +++ b/PreviewTests/SupportingFiles/target.yml @@ -31,6 +31,7 @@ targets: dependencies: - target: ElementX + - package: MatrixRustSDK - package: SnapshotTesting info: diff --git a/ShareExtension/Sources/ShareExtensionViewController.swift b/ShareExtension/Sources/ShareExtensionViewController.swift new file mode 100644 index 0000000000..96bfe173ac --- /dev/null +++ b/ShareExtension/Sources/ShareExtensionViewController.swift @@ -0,0 +1,105 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import IntentsUI +import SwiftUI + +class ShareExtensionViewController: UIViewController { + private let appSettings: CommonSettingsProtocol = AppSettings() + private let hostingController = UIHostingController(rootView: ShareExtensionView()) + + override func viewDidLoad() { + super.viewDidLoad() + + addChild(hostingController) + view.addMatchedSubview(hostingController.view) + hostingController.didMove(toParent: self) + + MXLog.configure(currentTarget: "shareextension", filePrefix: "shareextension", logLevel: appSettings.logLevel) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + Task { + if let payload = await prepareSharePayload() { + await self.openMainApp(payload: payload) + } + + self.dismiss() + } + } + + // MARK: - Private + + private func prepareSharePayload() async -> ShareExtensionPayload? { + guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem, + let itemProvider = extensionItem.attachments?.first else { + return nil + } + + let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier + + if let fileURL = await itemProvider.storeData() { + return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent)) + } else if let url = await itemProvider.loadTransferable(type: URL.self) { + return .text(roomID: roomID, text: url.absoluteString) + } else if let string = await itemProvider.loadString() { + return .text(roomID: roomID, text: string) + } else { + MXLog.error("Failed loading NSItemProvider data: \(itemProvider)") + return nil + } + } + + private func openMainApp(payload: ShareExtensionPayload) async { + guard let payload = urlEncodeSharePayload(payload) else { + MXLog.error("Failed preparing share payload") + return + } + + guard let url = URL(string: "\(InfoPlistReader.main.baseBundleIdentifier):/\(ShareExtensionConstants.urlPath)?\(payload)") else { + MXLog.error("Failed retrieving main application scheme") + return + } + + await openURL(url) + } + + private func urlEncodeSharePayload(_ payload: ShareExtensionPayload) -> String? { + let data: Data + do { + data = try JSONEncoder().encode(payload) + } catch { + MXLog.error("Failed encoding share payload with error: \(error)") + return nil + } + + guard let jsonString = String(data: data, encoding: .utf8) else { + MXLog.error("Invalid payload data") + return nil + } + + return jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + } + + private func dismiss() { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + private func openURL(_ url: URL) async { + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + await application.open(url) + return + } + + responder = responder?.next + } + } +} diff --git a/ShareExtension/Sources/View/ShareExtensionView.swift b/ShareExtension/Sources/View/ShareExtensionView.swift new file mode 100644 index 0000000000..9329e3c1ec --- /dev/null +++ b/ShareExtension/Sources/View/ShareExtensionView.swift @@ -0,0 +1,23 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct ShareExtensionView: View { + var body: some View { + ZStack { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(.compound.bgCanvasDefault) + } +} + +#Preview { + ShareExtensionView() +} diff --git a/ShareExtension/SupportingFiles/Info.plist b/ShareExtension/SupportingFiles/Info.plist new file mode 100644 index 0000000000..d8ad392ed8 --- /dev/null +++ b/ShareExtension/SupportingFiles/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareExtensionViewController + + appGroupIdentifier + $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier + $(BASE_BUNDLE_IDENTIFIER) + keychainAccessGroupIdentifier + $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) + productionAppName + $(PRODUCTION_APP_NAME) + + diff --git a/ShareExtension/SupportingFiles/ShareExtension.entitlements b/ShareExtension/SupportingFiles/ShareExtension.entitlements new file mode 100644 index 0000000000..d9849a816d --- /dev/null +++ b/ShareExtension/SupportingFiles/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + + diff --git a/ShareExtension/SupportingFiles/target.yml b/ShareExtension/SupportingFiles/target.yml new file mode 100644 index 0000000000..cca24f0522 --- /dev/null +++ b/ShareExtension/SupportingFiles/target.yml @@ -0,0 +1,87 @@ +name: ShareExtension + +schemes: + ShareExtension: + analyze: + config: Debug + archive: + config: Release + build: + targets: + ShareExtension: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + askForAppToLaunch: true + config: Debug + debugEnabled: false + disableMainThreadChecker: false + launchAutomaticallySubstyle: 2 + test: + config: Debug + disableMainThreadChecker: false + +targets: + ShareExtension: + type: app-extension + platform: iOS + + dependencies: + - package: MatrixRustSDK + - package: Collections + - package: Compound + + info: + path: ../SupportingFiles/Info.plist + properties: + CFBundleDisplayName: $(PRODUCT_DISPLAY_NAME) + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + appGroupIdentifier: $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER) + keychainAccessGroupIdentifier: $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) + productionAppName: $(PRODUCTION_APP_NAME) + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareExtensionViewController + NSExtensionAttributes: + IntentsSupported: [ + INSendMessageIntent, + ] + NSExtensionActivationRule: + NSExtensionActivationSupportsFileWithMaxCount: 1 + NSExtensionActivationSupportsImageWithMaxCount: 1 + NSExtensionActivationSupportsMovieWithMaxCount: 1 + NSExtensionActivationSupportsText: true + NSExtensionActivationSupportsWebURLWithMaxCount: 1 + + settings: + base: + PRODUCT_NAME: ShareExtension + PRODUCT_DISPLAY_NAME: $(APP_DISPLAY_NAME) + PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.shareextension + MARKETING_VERSION: $(MARKETING_VERSION) + CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION) + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) + CODE_SIGN_ENTITLEMENTS: ShareExtension/SupportingFiles/ShareExtension.entitlements + + sources: + - path: ../Sources + - path: ../SupportingFiles + - path: ../../ElementX/Sources/ShareExtension + - path: ../../ElementX/Sources/Application/AppSettings.swift + - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift + - path: ../../ElementX/Sources/Other/Extensions/FileManager.swift + - path: ../../ElementX/Sources/Other/Extensions/NSItemProvider.swift + - path: ../../ElementX/Sources/Other/Extensions/ProcessInfo.swift + - path: ../../ElementX/Sources/Other/Extensions/UIView.swift + - path: ../../ElementX/Sources/Other/Extensions/URL.swift + - path: ../../ElementX/Sources/Other/InfoPlistReader.swift + - path: ../../ElementX/Sources/Other/Logging + - path: ../../ElementX/Sources/Other/UserPreference.swift + - path: ../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift diff --git a/TchapX/development/SupportingFiles/NSE/target.yml b/TchapX/development/SupportingFiles/NSE/target.yml index 07184f6654..0144733838 100644 --- a/TchapX/development/SupportingFiles/NSE/target.yml +++ b/TchapX/development/SupportingFiles/NSE/target.yml @@ -78,7 +78,7 @@ targets: - path: ../../../../NSE/Sources - path: ../../../../ElementX/Sources/Generated/Assets.swift - path: ../../../../ElementX/Sources/Generated/Strings.swift - - path: ../../../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../../../ElementX/Sources/Other/Avatars.swift - path: ../../../../ElementX/Sources/Other/Extensions/AttributedString.swift - path: ../../../../ElementX/Sources/Other/Extensions/Bundle.swift - path: ../../../../ElementX/Sources/Other/Extensions/ClientBuilder.swift diff --git a/TchapX/main/Sources/Generated/TchapAssets.swift b/TchapX/main/Sources/Generated/TchapAssets.swift index 016b5164a5..fdc5d22e14 100644 --- a/TchapX/main/Sources/Generated/TchapAssets.swift +++ b/TchapX/main/Sources/Generated/TchapAssets.swift @@ -2,14 +2,14 @@ // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen #if os(macOS) - import AppKit +import AppKit #elseif os(iOS) - import UIKit +import UIKit #elseif os(tvOS) || os(watchOS) - import UIKit +import UIKit #endif #if canImport(SwiftUI) - import SwiftUI +import SwiftUI #endif // Deprecated typealiases @@ -19,20 +19,21 @@ // MARK: - Asset Catalogs // swiftlint:disable identifier_name line_length nesting type_body_length type_name -internal enum TchapAssets { -} +enum TchapAssets { } + // swiftlint:enable identifier_name line_length nesting type_body_length type_name // MARK: - Implementation Details // swiftlint:disable convenience_type private final class BundleToken { - static let bundle: Bundle = { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: BundleToken.self) - #endif - }() + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() } + // swiftlint:enable convenience_type diff --git a/TchapX/main/Sources/Generated/TchapStrings+Untranslated.swift b/TchapX/main/Sources/Generated/TchapStrings+Untranslated.swift index d4bb6ab5bb..e8ce0a700c 100644 --- a/TchapX/main/Sources/Generated/TchapStrings+Untranslated.swift +++ b/TchapX/main/Sources/Generated/TchapStrings+Untranslated.swift @@ -9,20 +9,20 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces -internal enum TchapUntranslatedL10n { -} +enum TchapUntranslatedL10n { } + // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces // MARK: - Implementation Details extension TchapUntranslatedL10n { - static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - // No need to check languages, we always default to en for untranslated strings - guard let bundle = Bundle.lprojBundle(for: "en") else { return key } - let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") - return String(format: format, locale: Locale(identifier: "en"), arguments: args) - } + static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // No need to check languages, we always default to en for untranslated strings + guard let bundle = Bundle.lprojBundle(for: "en") else { return key } + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + return String(format: format, locale: Locale(identifier: "en"), arguments: args) + } } // swiftlint:enable all diff --git a/TchapX/main/Sources/Generated/TchapStrings.swift b/TchapX/main/Sources/Generated/TchapStrings.swift index e7acda9157..c2aaedec7b 100644 --- a/TchapX/main/Sources/Generated/TchapStrings.swift +++ b/TchapX/main/Sources/Generated/TchapStrings.swift @@ -9,33 +9,33 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces -internal enum TchapL10n { -} +enum TchapL10n { } + // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces // MARK: - Implementation Details extension TchapL10n { - static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - // Use preferredLocalizations to get a language that is in the bundle and the user's preferred list of languages. - let languages = Bundle.overrideLocalizations ?? Bundle.app.preferredLocalizations + static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // Use preferredLocalizations to get a language that is in the bundle and the user's preferred list of languages. + let languages = Bundle.overrideLocalizations ?? Bundle.app.preferredLocalizations + + for language in languages { + if let translation = trIn(language, table, key, args) { + return translation + } + } + return Bundle.app.developmentLocalization.flatMap { trIn($0, table, key, args) } ?? key + } - for language in languages { - if let translation = trIn(language, table, key, args) { + private static func trIn(_ language: String, _ table: String, _ key: String, _ args: CVarArg...) -> String? { + guard let bundle = Bundle.lprojBundle(for: language) else { return nil } + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let translation = String(format: format, locale: Locale(identifier: language), arguments: args) + guard translation != key else { return nil } return translation - } } - return Bundle.app.developmentLocalization.flatMap { trIn($0, table, key, args) } ?? key - } - - private static func trIn(_ language: String, _ table: String, _ key: String, _ args: CVarArg...) -> String? { - guard let bundle = Bundle.lprojBundle(for: language) else { return nil } - let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") - let translation = String(format: format, locale: Locale(identifier: language), arguments: args) - guard translation != key else { return nil } - return translation - } } // swiftlint:enable all diff --git a/TchapX/production/SupportingFiles/NSE/target.yml b/TchapX/production/SupportingFiles/NSE/target.yml index a70a54accc..d822999aa2 100644 --- a/TchapX/production/SupportingFiles/NSE/target.yml +++ b/TchapX/production/SupportingFiles/NSE/target.yml @@ -78,7 +78,7 @@ targets: - path: ../../../../NSE/Sources - path: ../../../../ElementX/Sources/Generated/Assets.swift - path: ../../../../ElementX/Sources/Generated/Strings.swift - - path: ../../../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../../../ElementX/Sources/Other/Avatars.swift - path: ../../../../ElementX/Sources/Other/Extensions/AttributedString.swift - path: ../../../../ElementX/Sources/Other/Extensions/Bundle.swift - path: ../../../../ElementX/Sources/Other/Extensions/ClientBuilder.swift diff --git a/TchapX/staging/SupportingFiles/NSE/target.yml b/TchapX/staging/SupportingFiles/NSE/target.yml index ae1c484ff0..a4a937a852 100644 --- a/TchapX/staging/SupportingFiles/NSE/target.yml +++ b/TchapX/staging/SupportingFiles/NSE/target.yml @@ -78,7 +78,7 @@ targets: - path: ../../../../NSE/Sources - path: ../../../../ElementX/Sources/Generated/Assets.swift - path: ../../../../ElementX/Sources/Generated/Strings.swift - - path: ../../../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../../../ElementX/Sources/Other/Avatars.swift - path: ../../../../ElementX/Sources/Other/Extensions/AttributedString.swift - path: ../../../../ElementX/Sources/Other/Extensions/Bundle.swift - path: ../../../../ElementX/Sources/Other/Extensions/ClientBuilder.swift diff --git a/UITests/Sources/Application.swift b/UITests/Sources/Application.swift index 861dc17519..2c208450d6 100644 --- a/UITests/Sources/Application.swift +++ b/UITests/Sources/Application.swift @@ -53,11 +53,11 @@ enum Application { extension XCUIApplication { static var recordMode: SnapshotTestingConfiguration.Record = .missing - @MainActor /// Assert screenshot for a screen with the given identifier. Does not fail if a screenshot is newly created. /// - Parameter identifier: Identifier of the UI test screen /// - Parameter step: An optional integer that can be used to take multiple snapshots per test identifier. /// - Parameter insets: Optional insets with which to crop the image by. + @MainActor func assertScreenshot(_ identifier: UITestsScreenIdentifier, step: Int? = nil, insets: UIEdgeInsets? = nil, delay: Duration = .seconds(1), precision: Float = 0.99) async throws { var snapshotName = identifier.rawValue if let step { diff --git a/UITests/Sources/BugReportUITests.swift b/UITests/Sources/BugReportUITests.swift index d87441c964..954da3b4ac 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: app) + app.textFields[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: app) + app.textFields[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 index 5bc5bea00d..6000b0d1c2 100644 --- a/UITests/Sources/EncryptionResetUITests.swift +++ b/UITests/Sources/EncryptionResetUITests.swift @@ -26,6 +26,9 @@ class EncryptionResetUITests: XCTestCase { // Confirm the intent to reset. app.buttons[A11yIdentifiers.encryptionResetScreen.continueReset].tap() app.buttons[A11yIdentifiers.alertInfo.primaryButton].tap() + + try await Task.sleep(for: .seconds(2.0)) + try await app.assertScreenshot(.encryptionReset, step: Step.passwordScreen) // Enter the password and submit. diff --git a/UITests/Sources/PollFormScreenUITests.swift b/UITests/Sources/PollFormScreenUITests.swift index fa02f72b9d..2456a474e9 100644 --- a/UITests/Sources/PollFormScreenUITests.swift +++ b/UITests/Sources/PollFormScreenUITests.swift @@ -16,15 +16,15 @@ class PollFormScreenUITests: XCTestCase { func testFilledPoll() async throws { let app = Application.launch(.createPoll) - let questionTextField = app.textViews[A11yIdentifiers.pollFormScreen.question] + let questionTextField = app.textFields[A11yIdentifiers.pollFormScreen.question] questionTextField.tapCenter() questionTextField.typeText("Do you like polls?") - let option1TextField = app.textViews[A11yIdentifiers.pollFormScreen.optionID(0)] + let option1TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(0)] option1TextField.tapCenter() option1TextField.typeText("Yes") - let option2TextField = app.textViews[A11yIdentifiers.pollFormScreen.optionID(1)] + let option2TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(1)] option2TextField.tapCenter() option2TextField.typeText("No") diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 9779ae8bf0..fbecde99de 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -23,17 +23,7 @@ class ComposerToolbarViewModelTests: XCTestCase { AppSettings.resetAllSettings() appSettings = AppSettings() ServiceLocator.shared.register(appSettings: appSettings) - wysiwygViewModel = WysiwygComposerViewModel() - completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init()) - draftServiceMock = ComposerDraftServiceMock() - viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, - completionSuggestionService: completionSuggestionServiceMock, - mediaProvider: MediaProviderMock(configuration: .init()), - mentionDisplayHelper: ComposerMentionDisplayHelper.mock, - analyticsService: ServiceLocator.shared.analytics, - composerDraftService: draftServiceMock) - - viewModel.context.composerFormattingEnabled = true + setUpViewModel() } override func tearDown() { @@ -41,14 +31,14 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerFocus() { - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default))) XCTAssertTrue(viewModel.state.bindings.composerFocused) viewModel.process(timelineAction: .removeFocus) XCTAssertFalse(viewModel.state.bindings.composerFocused) } func testComposerMode() { - let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default) viewModel.process(timelineAction: .setMode(mode: mode)) XCTAssertEqual(viewModel.state.composerMode, mode) viewModel.process(timelineAction: .clear) @@ -56,7 +46,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerModeIsPublished() { - let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default) let expectation = expectation(description: "Composer mode is published") let cancellable = viewModel .context @@ -101,7 +91,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testSuggestions() { let suggestions: [SuggestionItem] = [.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()))] + .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCAvatar, range: .init()))] let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)) viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: mockCompletionSuggestionService, @@ -236,7 +226,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), type: .default))) viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.saveDraft() @@ -340,7 +330,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(nil) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) XCTAssertTrue(viewModel.state.composerEmpty) @@ -356,7 +346,7 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: nil, draftType: .newMessage)) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -373,7 +363,7 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: "Hello world!", draftType: .newMessage)) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertTrue(viewModel.context.composerFormattingEnabled) @@ -391,11 +381,11 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: nil, draftType: .edit(eventID: "testID"))) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) - XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"))) + XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), type: .default)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!")) } @@ -424,7 +414,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(.init(details: loadedReply, isThreaded: true)) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -464,7 +454,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(.init(details: loadedReply, isThreaded: true)) } - viewModel.loadDraft() + await viewModel.loadDraft() await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -483,7 +473,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testSaveVolatileDraftWhenEditing() { viewModel.context.composerFormattingEnabled = false viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString)))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .default))) let draft = draftServiceMock.saveVolatileDraftReceivedDraft XCTAssertNotNil(draft) @@ -622,6 +612,45 @@ class ComposerToolbarViewModelTests: XCTestCase { viewModel.process(viewAction: .sendMessage) try await deferred.fulfill() } + + func testRestoreDoesntOverwriteInitialText() async { + let sharedText = "Some shared text" + let expectation = expectation(description: "Wait for draft to be restored") + expectation.isInverted = true + setUpViewModel(initialText: sharedText) { + defer { expectation.fulfill() } + return .success(.init(plainText: "Hello world!", + htmlText: nil, + draftType: .newMessage)) + } + viewModel.context.composerFormattingEnabled = false + await viewModel.loadDraft() + + await fulfillment(of: [expectation], timeout: 1) + XCTAssertFalse(viewModel.context.composerFormattingEnabled) + XCTAssertEqual(viewModel.state.composerMode, .default) + XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: sharedText)) + } + + // MARK: - Helpers + + private func setUpViewModel(initialText: String? = nil, loadDraftClosure: (() async -> Result)? = nil) { + wysiwygViewModel = WysiwygComposerViewModel() + completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init()) + draftServiceMock = ComposerDraftServiceMock() + if let loadDraftClosure { + draftServiceMock.loadDraftClosure = loadDraftClosure + } + + viewModel = ComposerToolbarViewModel(initialText: initialText, + wysiwygViewModel: wysiwygViewModel, + completionSuggestionService: completionSuggestionServiceMock, + mediaProvider: MediaProviderMock(configuration: .init()), + mentionDisplayHelper: ComposerMentionDisplayHelper.mock, + analyticsService: ServiceLocator.shared.analytics, + composerDraftService: draftServiceMock) + viewModel.context.composerFormattingEnabled = true + } } private extension MentionSuggestionItem { diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index b145dfbfd9..110d788468 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -25,7 +25,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { override func setUpWithError() throws { cancellables.removeAll() - clientProxy = ClientProxyMock(.init(userID: "@a:b.com")) + clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com")) userSession = UserSessionMock(.init(clientProxy: clientProxy)) let parameters = CreateRoomFlowParameters() usersSubject.send([.mockAlice, .mockBob, .mockCharlie]) @@ -68,21 +68,25 @@ class CreateRoomScreenViewModelTests: XCTestCase { func testCreateRoomRequirements() { XCTAssertFalse(context.viewState.canCreateRoom) - context.roomName = "A" + context.send(viewAction: .updateRoomName("A")) XCTAssertTrue(context.viewState.canCreateRoom) } func testCreateKnockingRoom() async { - context.roomName = "A" + context.send(viewAction: .updateRoomName("A")) context.roomTopic = "B" context.isRoomPrivate = false + // When setting the room as private we always reset the knocking state to the default value of false + // so we need to wait a main actor cycle to ensure the view state is updated + await Task.yield() context.isKnockingOnly = true XCTAssertTrue(context.viewState.canCreateRoom) let expectation = expectation(description: "Wait for the room to be created") - clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, localAliasPart in XCTAssertTrue(isKnockingOnly) XCTAssertFalse(isPrivate) + XCTAssertEqual(localAliasPart, "a") defer { expectation.fulfill() } return .success("") } @@ -90,14 +94,67 @@ class CreateRoomScreenViewModelTests: XCTestCase { await fulfillment(of: [expectation]) } + func testCreatePublicRoomFailsForInvalidAlias() async throws { + context.send(viewAction: .updateRoomName("A")) + context.roomTopic = "B" + context.isRoomPrivate = false + // When setting the room as private we always reset the alias + // so we need to wait a main actor cycle to ensure the view state is updated + await Task.yield() + + // we wait for the debounce to show the error + let deferred = deferFulfillment(context.$viewState) { viewState in + viewState.aliasErrors.contains(.invalidSymbols) && !viewState.canCreateRoom + } + context.send(viewAction: .updateAliasLocalPart("#:")) + try await deferred.fulfill() + + // We also want to force the room creation in case the user may tap the button before the debounce + // blocked it + context.send(viewAction: .createRoom) + await Task.yield() + XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled) + } + + func testCreatePublicRoomFailsForExistingAlias() async throws { + clientProxy.isAliasAvailableReturnValue = .success(false) + context.send(viewAction: .updateRoomName("A")) + context.roomTopic = "B" + context.isRoomPrivate = false + // When setting the room as private we always reset the alias + // so we need to wait a main actor cycle to ensure the view state is updated + await Task.yield() + + // we wait for the debounce to show the error + let deferred = deferFulfillment(context.$viewState) { viewState in + viewState.aliasErrors.contains(.alreadyExists) && !viewState.canCreateRoom + } + context.send(viewAction: .updateAliasLocalPart("abc")) + try await deferred.fulfill() + + // We also want to force the room creation in case the user may tap the button before the debounce + // blocked it + let expectation = expectation(description: "Wait for the alias to be checked again") + clientProxy.isAliasAvailableClosure = { _ in + defer { + expectation.fulfill() + } + return .success(false) + } + context.send(viewAction: .createRoom) + await fulfillment(of: [expectation]) + XCTAssertEqual(clientProxy.isAliasAvailableCallsCount, 2) + XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled) + } + func testCreatePrivateRoomCantHaveKnockRule() async { - context.roomName = "A" + context.send(viewAction: .updateRoomName("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 + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in XCTAssertFalse(isKnockingOnly) XCTAssertTrue(isPrivate) expectation.fulfill() @@ -105,4 +162,35 @@ class CreateRoomScreenViewModelTests: XCTestCase { } await fulfillment(of: [expectation]) } + + func testNameAndAddressSync() async { + context.isRoomPrivate = true + await Task.yield() + context.send(viewAction: .updateRoomName("abc")) + XCTAssertEqual(context.viewState.aliasLocalPart, "abc") + XCTAssertEqual(context.viewState.roomName, "abc") + context.send(viewAction: .updateRoomName("DEF")) + XCTAssertEqual(context.viewState.roomName, "DEF") + XCTAssertEqual(context.viewState.aliasLocalPart, "def") + context.send(viewAction: .updateRoomName("a b c")) + XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c") + XCTAssertEqual(context.viewState.roomName, "a b c") + context.send(viewAction: .updateAliasLocalPart("hello-world")) + // This removes the sync + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world") + XCTAssertEqual(context.viewState.roomName, "a b c") + + context.send(viewAction: .updateRoomName("Hello Matrix!")) + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world") + XCTAssertEqual(context.viewState.roomName, "Hello Matrix!") + + // Deleting the whole name will restore the sync + context.send(viewAction: .updateRoomName("")) + XCTAssertEqual(context.viewState.aliasLocalPart, "") + XCTAssertEqual(context.viewState.roomName, "") + + context.send(viewAction: .updateRoomName("Hello# Matrix!")) + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!") + XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!") + } } diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index e485844793..1361b8303e 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -20,13 +20,6 @@ class HomeScreenViewModelTests: XCTestCase { override func setUpWithError() throws { cancellables.removeAll() - roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) - clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", roomSummaryProvider: roomSummaryProvider)) - viewModel = HomeScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), - analyticsService: ServiceLocator.shared.analytics, - appSettings: ServiceLocator.shared.settings, - selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), - userIndicatorController: ServiceLocator.shared.userIndicatorController) } override func tearDown() { @@ -34,6 +27,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testSelectRoom() async throws { + setupViewModel() + let mockRoomId = "mock_room_id" var correctResult = false var selectedRoomId = "" @@ -57,6 +52,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testTapUserAvatar() async throws { + setupViewModel() + var correctResult = false viewModel.actions @@ -76,6 +73,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testLeaveRoomAlert() async throws { + setupViewModel() + let mockRoomId = "1" clientProxy.roomForIdentifierClosure = { _ in .joined(JoinedRoomProxyMock(.init(id: mockRoomId, name: "Some room"))) } @@ -92,6 +91,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testLeaveRoomError() async throws { + setupViewModel() + let mockRoomId = "1" let room = JoinedRoomProxyMock(.init(id: mockRoomId, name: "Some room")) room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) } @@ -110,6 +111,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testLeaveRoomSuccess() async throws { + setupViewModel() + let mockRoomId = "1" var correctResult = false let expectation = expectation(description: #function) @@ -136,6 +139,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testShowRoomDetails() async throws { + setupViewModel() + let mockRoomId = "1" var correctResult = false viewModel.actions @@ -155,6 +160,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testFilters() async throws { + setupViewModel() + context.filtersState.activateFilter(.people) try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2) @@ -162,6 +169,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testSearch() async throws { + setupViewModel() + context.isSearchFieldFocused = true context.searchQuery = "lude to Found" try await Task.sleep(for: .milliseconds(100)) @@ -170,6 +179,8 @@ class HomeScreenViewModelTests: XCTestCase { } func testFiltersEmptyState() async throws { + setupViewModel() + context.filtersState.activateFilter(.people) context.filtersState.activateFilter(.favourites) try await Task.sleep(for: .milliseconds(100)) @@ -177,4 +188,89 @@ class HomeScreenViewModelTests: XCTestCase { context.isSearchFieldFocused = true XCTAssertFalse(context.viewState.shouldShowEmptyFilterState) } + + func testSetUpRecoveryBannerState() async throws { + // Given a view model without a visible security banner. + let securityStateStateSubject = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .unknown)) + setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) + XCTAssertEqual(context.viewState.securityBannerMode, .none) + + // When the recovery state comes through as disabled. + var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled)) + try await deferred.fulfill() + + // Then the banner should be shown to set up recovery. + XCTAssertEqual(context.viewState.securityBannerMode, .show(.setUpRecovery)) + + // When the recovery is enabled. + deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .enabled)) + try await deferred.fulfill() + + // Then the banner should no longer be shown. + XCTAssertEqual(context.viewState.securityBannerMode, .none) + } + + func testDismissSetUpRecoveryBannerState() async throws { + // Given a view model with the setup recovery banner shown. + let securityStateStateSubject = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .unknown)) + setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) + var deferred = deferFulfillment(context.$viewState) { $0.securityBannerMode == .show(.setUpRecovery) } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled)) + try await deferred.fulfill() + + // When the banner is dismissed. + deferred = deferFulfillment(context.$viewState) { $0.securityBannerMode == .dismissed } + context.send(viewAction: .skipRecoveryKeyConfirmation) + + // Then the banner should no longer be shown. + try await deferred.fulfill() + + // And when the recovery state comes through a second time the banner should still not be shown. + let failure = deferFailure(context.$viewState, timeout: 1) { $0.securityBannerMode != .dismissed } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled)) + try await failure.fulfill() + } + + func testOutOfSyncRecoveryBannerState() async throws { + // Given a view model without a visible security banner. + let securityStateStateSubject = CurrentValueSubject(.init(verificationState: .verified, recoveryState: .unknown)) + setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher()) + XCTAssertEqual(context.viewState.securityBannerMode, .none) + + // When the recovery state comes through as incomplete. + var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .incomplete)) + try await deferred.fulfill() + + // Then the banner should be shown for out of sync recovery. + XCTAssertEqual(context.viewState.securityBannerMode, .show(.recoveryOutOfSync)) + + // When the recovery is enabled. + deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false } + securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .enabled)) + try await deferred.fulfill() + + // Then the banner should no longer be shown. + XCTAssertEqual(context.viewState.securityBannerMode, .none) + } + + // MARK: - Helpers + + private func setupViewModel(securityStatePublisher: CurrentValuePublisher? = nil) { + roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) + clientProxy = ClientProxyMock(.init(userID: "@mock:client.com", + roomSummaryProvider: roomSummaryProvider)) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + if let securityStatePublisher { + userSession.sessionSecurityStatePublisher = securityStatePublisher + } + + viewModel = HomeScreenViewModel(userSession: userSession, + analyticsService: ServiceLocator.shared.analytics, + appSettings: ServiceLocator.shared.settings, + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } } diff --git a/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift b/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift new file mode 100644 index 0000000000..fba35e93f7 --- /dev/null +++ b/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift @@ -0,0 +1,23 @@ +// +// 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 KnockRequestsListScreenViewModelTests: XCTestCase { + var viewModel: KnockRequestsListScreenViewModelProtocol! + + var context: KnockRequestsListScreenViewModelType.Context { + viewModel.context + } + + override func setUpWithError() throws { + viewModel = KnockRequestsListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()), mediaProvider: MediaProviderMock()) + } +} diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 3ab0e77fdd..dc7389e969 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -150,8 +150,8 @@ class LoggingTests: XCTestCase { sender: .init(id: "sender"), content: .init(filename: "ImageString", caption: "ImageString", - source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), - thumbnailSource: nil)) + imageInfo: .mockImage, + thumbnailInfo: nil)) let videoMessage = VideoRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, @@ -161,9 +161,8 @@ class LoggingTests: XCTestCase { sender: .init(id: "sender"), content: .init(filename: "VideoString", caption: "VideoString", - duration: 0, - source: nil, - thumbnailSource: nil)) + videoInfo: .mockVideo, + thumbnailInfo: nil)) let fileMessage = FileRoomTimelineItem(id: .randomEvent, timestamp: "", isOutgoing: false, @@ -174,6 +173,7 @@ class LoggingTests: XCTestCase { content: .init(filename: "FileString", caption: "FileString", source: nil, + fileSize: nil, thumbnailSource: nil, contentType: nil)) @@ -228,28 +228,19 @@ class LoggingTests: XCTestCase { let rustEmoteMessage = EmoteMessageContent(body: emoteString, formatted: FormattedBody(format: .html, body: "\(emoteString)")) - let rustImageMessage = ImageMessageContent(body: "ImageString", - formatted: nil, - rawFilename: "ImageString", - filename: "ImageString", + let rustImageMessage = ImageMessageContent(filename: "ImageString", caption: "ImageString", formattedCaption: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustVideoMessage = VideoMessageContent(body: "VideoString", - formatted: nil, - rawFilename: "VideoString", - filename: "VideoString", + let rustVideoMessage = VideoMessageContent(filename: "VideoString", caption: "VideoString", formattedCaption: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustFileMessage = FileMessageContent(body: "FileString", - formatted: nil, - rawFilename: "FileString", - filename: "FileString", + let rustFileMessage = FileMessageContent(filename: "FileString", caption: "FileString", formattedCaption: nil, source: MediaSource(noPointer: .init()), @@ -280,13 +271,13 @@ class LoggingTests: XCTestCase { XCTAssertFalse(content.contains(emoteString)) XCTAssertTrue(content.contains(String(describing: ImageMessageContent.self))) - XCTAssertFalse(content.contains(rustImageMessage.body)) + XCTAssertFalse(content.contains(rustImageMessage.filename)) XCTAssertTrue(content.contains(String(describing: VideoMessageContent.self))) - XCTAssertFalse(content.contains(rustVideoMessage.body)) + XCTAssertFalse(content.contains(rustVideoMessage.filename)) XCTAssertTrue(content.contains(String(describing: FileMessageContent.self))) - XCTAssertFalse(content.contains(rustFileMessage.body)) + XCTAssertFalse(content.contains(rustFileMessage.filename)) } func testLogFileSorting() async throws { diff --git a/UnitTests/Sources/MediaPlayerProviderTests.swift b/UnitTests/Sources/MediaPlayerProviderTests.swift index 92ef5997d5..d4837d4728 100644 --- a/UnitTests/Sources/MediaPlayerProviderTests.swift +++ b/UnitTests/Sources/MediaPlayerProviderTests.swift @@ -15,50 +15,13 @@ class MediaPlayerProviderTests: XCTestCase { private var mediaPlayerProvider: MediaPlayerProvider! private let oggMimeType = "audio/ogg" - private let someURL = URL("/some/url") - private let someOtherURL = URL("/some/other/url") + private let someURL = URL.mockMXCAudio + private let someOtherURL = URL.mockMXCFile override func setUp() async throws { mediaPlayerProvider = MediaPlayerProvider() } - func testPlayerForWrongMediaType() async throws { - let mediaSourceWithoutMimeType = MediaSourceProxy(url: someURL, mimeType: nil) - switch mediaPlayerProvider.player(for: mediaSourceWithoutMimeType) { - case .failure(.unsupportedMediaType): - // Ok - break - default: - XCTFail("An error is expected") - } - - let mediaSourceVideo = MediaSourceProxy(url: someURL, mimeType: "video/mp4") - switch mediaPlayerProvider.player(for: mediaSourceVideo) { - case .failure(.unsupportedMediaType): - // Ok - break - default: - XCTFail("An error is expected") - } - } - - func testPlayerFor() async throws { - let mediaSource = MediaSourceProxy(url: someURL, mimeType: oggMimeType) - guard case .success(let playerA) = mediaPlayerProvider.player(for: mediaSource) else { - XCTFail("A valid player is expected") - return - } - - // calling it again with another mediasource must returns the same player - let otherMediaSource = MediaSourceProxy(url: someOtherURL, mimeType: oggMimeType) - guard case .success(let playerB) = mediaPlayerProvider.player(for: otherMediaSource) else { - XCTFail("A valid player is expected") - return - } - - XCTAssert(playerA === playerB) - } - func testPlayerStates() async throws { let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent) // By default, there should be no player state diff --git a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift index 09d024d381..bca846f2b6 100644 --- a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift @@ -10,12 +10,12 @@ import MatrixRustSDK import XCTest final class MediaLoaderTests: XCTestCase { - func testMediaRequestCoalescing() async { + func testMediaRequestCoalescing() async throws { let mediaLoadingClient = ClientSDKMock() mediaLoadingClient.getMediaContentMediaSourceReturnValue = Data() let mediaLoader = MediaLoader(client: mediaLoadingClient) - let mediaSource = MediaSourceProxy(url: URL.documentsDirectory, mimeType: nil) + let mediaSource = try MediaSourceProxy(url: .mockMXCFile, mimeType: nil) do { for _ in 1...10 { @@ -28,12 +28,12 @@ final class MediaLoaderTests: XCTestCase { } } - func testMediaThumbnailRequestCoalescing() async { + func testMediaThumbnailRequestCoalescing() async throws { let mediaLoadingClient = ClientSDKMock() mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightReturnValue = Data() let mediaLoader = MediaLoader(client: mediaLoadingClient) - let mediaSource = MediaSourceProxy(url: URL.documentsDirectory, mimeType: nil) + let mediaSource = try MediaSourceProxy(url: .mockMXCImage, mimeType: nil) do { for _ in 1...10 { diff --git a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift index c33640482f..a012afe776 100644 --- a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift @@ -36,7 +36,7 @@ final class MediaProviderTests: XCTestCase { return } - let loadTask = mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg")) + let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) let connectivitySubject = CurrentValueSubject(.unreachable) @@ -59,7 +59,7 @@ final class MediaProviderTests: XCTestCase { } func testLoadingRetriedOnReconnectionCancelsAfterSecondFailure() async throws { - let loadTask = mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg")) + let loadTask = try mediaProvider.loadImageRetryingOnReconnection(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg")) let connectivitySubject = CurrentValueSubject(.reachable) @@ -73,57 +73,57 @@ final class MediaProviderTests: XCTestCase { } func test_whenImageFromSourceWithSourceNil_nilReturned() throws { - let image = mediaProvider.imageFromSource(nil, size: AvatarSize.room(on: .timeline).scaledSize) + let image = try mediaProvider.imageFromSource(nil, size: Avatars.Size.room(on: .timeline).scaledSize) XCTAssertNil(image) } func test_whenImageFromSourceWithSourceNotNilAndImageCacheContainsImage_ImageIsReturned() throws { - let avatarSize = AvatarSize.room(on: .timeline) - let url = URL.picturesDirectory + let avatarSize = Avatars.Size.room(on: .timeline) + let url = URL.mockMXCImage let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() imageCache.retrievedImagesInMemory[key] = imageForKey - let image = mediaProvider.imageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), - size: avatarSize.scaledSize) + let image = try mediaProvider.imageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), + size: avatarSize.scaledSize) XCTAssertEqual(image, imageForKey) } func test_whenImageFromSourceWithSourceNotNilAndImageNotCached_nilReturned() throws { - let image = mediaProvider.imageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + let image = try mediaProvider.imageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: Avatars.Size.room(on: .timeline).scaledSize) XCTAssertNil(image) } func test_whenLoadImageFromSourceAndImageCacheContainsImage_successIsReturned() async throws { - let avatarSize = AvatarSize.room(on: .timeline) - let url = URL.picturesDirectory + let avatarSize = Avatars.Size.room(on: .timeline) + let url = URL.mockMXCImage let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() imageCache.retrievedImagesInMemory[key] = imageForKey - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), - size: avatarSize.scaledSize) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), + size: avatarSize.scaledSize) XCTAssertEqual(Result.success(imageForKey), result) } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageSucceeds_successIsReturned() async throws { - let avatarSize = AvatarSize.room(on: .timeline) - let url = URL.picturesDirectory + let avatarSize = Avatars.Size.room(on: .timeline) + let url = URL.mockMXCImage let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() imageCache.retrievedImages[key] = imageForKey - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), - size: avatarSize.scaledSize) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), + size: avatarSize.scaledSize) XCTAssertEqual(Result.success(imageForKey), result) } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageThumbnailIsLoaded() async throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let expectedImage = try loadTestImage() mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = expectedImage.pngData() - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: avatarSize.scaledSize) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: avatarSize.scaledSize) switch result { case .success(let image): XCTAssertEqual(image.pngData(), expectedImage.pngData()) @@ -133,15 +133,15 @@ final class MediaProviderTests: XCTestCase { } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageIsStored() async throws { - let avatarSize = AvatarSize.room(on: .timeline) - let url = URL.picturesDirectory + let avatarSize = Avatars.Size.room(on: .timeline) + let url = URL.mockMXCImage let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let expectedImage = try loadTestImage() mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = expectedImage.pngData() - _ = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), - size: avatarSize.scaledSize) + _ = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), + size: avatarSize.scaledSize) let storedImage = try XCTUnwrap(imageCache.storedImages[key]) XCTAssertEqual(expectedImage.pngData(), storedImage.pngData()) } @@ -151,8 +151,8 @@ final class MediaProviderTests: XCTestCase { mediaLoader.loadMediaContentForSourceReturnValue = expectedImage.pngData() - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: nil) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: nil) switch result { case .success(let image): XCTAssertEqual(image.pngData(), expectedImage.pngData()) @@ -164,8 +164,8 @@ final class MediaProviderTests: XCTestCase { func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndLoadImageThumbnailFails_errorIsThrown() async throws { mediaLoader.loadMediaThumbnailForSourceWidthHeightThrowableError = MediaProviderTestsError.error - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: Avatars.Size.room(on: .timeline).scaledSize) switch result { case .success: XCTFail("Should fail") @@ -177,8 +177,8 @@ final class MediaProviderTests: XCTestCase { func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSizeAndLoadImageContentFails_errorIsThrown() async throws { mediaLoader.loadMediaContentForSourceThrowableError = MediaProviderTestsError.error - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: nil) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: nil) switch result { case .success: XCTFail("Should fail") @@ -190,8 +190,8 @@ final class MediaProviderTests: XCTestCase { func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndImageThumbnailIsLoadedWithCorruptedData_errorIsThrown() async throws { mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = Data() - let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), + size: Avatars.Size.room(on: .timeline).scaledSize) switch result { case .success: XCTFail("Should fail") diff --git a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift index 880f27acbb..81218b0311 100644 --- a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift +++ b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift @@ -10,4 +10,136 @@ import XCTest @testable import ElementX @MainActor -class MediaUploadPreviewScreenViewModelTests: XCTestCase { } +class MediaUploadPreviewScreenViewModelTests: XCTestCase { + var timelineProxy: TimelineProxyMock! + var viewModel: MediaUploadPreviewScreenViewModel! + var context: MediaUploadPreviewScreenViewModel.Context { viewModel.context } + + enum TestError: Swift.Error { + case unexpectedParameter + case unknown + } + + override func setUp() { + AppSettings.resetAllSettings() + let appSettings = AppSettings() + appSettings.optimizeMediaUploads = false + ServiceLocator.shared.register(appSettings: appSettings) + } + + deinit { + AppSettings.resetAllSettings() + } + + func testImageUploadWithoutCaption() async throws { + setUpViewModel(url: imageURL, expectedCaption: nil) + context.caption = .init("") + try await send() + } + + func testImageUploadWithBlankCaption() async throws { + setUpViewModel(url: imageURL, expectedCaption: nil) + context.caption = .init(" ") + try await send() + } + + func testImageUploadWithCaption() async throws { + let caption = "This is a really great image!" + setUpViewModel(url: imageURL, expectedCaption: caption) + context.caption = .init(string: caption) + try await send() + } + + func testVideoUploadWithoutCaption() async throws { + setUpViewModel(url: videoURL, expectedCaption: nil) + context.caption = .init("") + try await send() + } + + func testVideoUploadWithCaption() async throws { + let caption = "Check out this video!" + setUpViewModel(url: videoURL, expectedCaption: caption) + context.caption = .init(string: caption) + try await send() + } + + func testAudioUploadWithoutCaption() async throws { + setUpViewModel(url: audioURL, expectedCaption: nil) + context.caption = .init("") + try await send() + } + + func testAudioUploadWithCaption() async throws { + let caption = "Listen to this!" + setUpViewModel(url: audioURL, expectedCaption: caption) + context.caption = .init(string: caption) + try await send() + } + + func testFileUploadWithoutCaption() async throws { + setUpViewModel(url: fileURL, expectedCaption: nil) + context.caption = .init("") + try await send() + } + + func testFileUploadWithCaption() async throws { + let caption = "Please will you check my article." + setUpViewModel(url: fileURL, expectedCaption: caption) + context.caption = .init(string: caption) + try await send() + } + + // MARK: - Helpers + + private var audioURL: URL { assertResourceURL(filename: "test_audio.mp3") } + private var fileURL: URL { assertResourceURL(filename: "test_pdf.pdf") } + private var imageURL: URL { assertResourceURL(filename: "test_animated_image.gif") } + private var videoURL: URL { assertResourceURL(filename: "landscape_test_video.mov") } + + private func assertResourceURL(filename: String) -> URL { + guard let url = Bundle(for: Self.self).url(forResource: filename, withExtension: nil) else { + XCTFail("Failed retrieving test asset") + return .picturesDirectory + } + return url + } + + private func setUpViewModel(url: URL, expectedCaption: String?) { + timelineProxy = TimelineProxyMock(.init()) + timelineProxy.sendAudioUrlAudioInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in + self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) + } + timelineProxy.sendFileUrlFileInfoCaptionRequestHandleClosure = { [weak self] _, _, caption, _ in + self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) + } + timelineProxy.sendImageUrlThumbnailURLImageInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in + self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) + } + timelineProxy.sendVideoUrlThumbnailURLVideoInfoCaptionRequestHandleClosure = { [weak self] _, _, _, caption, _ in + self?.verifyCaption(caption, expectedCaption: expectedCaption) ?? .failure(.sdkError(TestError.unknown)) + } + + let roomProxy = JoinedRoomProxyMock(.init()) + roomProxy.timeline = timelineProxy + viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: UserIndicatorControllerMock(), + roomProxy: roomProxy, + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), + title: "Some File", + url: url, + shouldShowCaptionWarning: true) + } + + private func verifyCaption(_ caption: String?, expectedCaption: String?) -> Result { + guard caption == expectedCaption else { + XCTFail("The sent caption '\(caption ?? "nil")' does not match the expected value '\(expectedCaption ?? "nil")'").self + return .failure(.sdkError(TestError.unexpectedParameter)) + } + return .success(()) + } + + private func send() async throws { + let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } + context.send(viewAction: .send) + try await deferred.fulfill() + } +} diff --git a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift index 93ae732697..245935a585 100644 --- a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift +++ b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift @@ -336,7 +336,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase { // 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.size ?? 0, 1_850_479, accuracy: 100) XCTAssertEqual(imageInfo.width, 3024) XCTAssertEqual(imageInfo.height, 4032) diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 2755b65e41..a368e4e35a 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -108,15 +108,16 @@ final class NotificationManagerTests: XCTestCase { XCTAssertEqual(request.content.title, "Title") XCTAssertEqual(request.content.subtitle, "Subtitle") } - + func test_whenStart_notificationCategoriesAreSet() throws { - // let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, - // title: L10n.actionQuickReply, - // options: []) + let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, + title: L10n.actionQuickReply, + options: []) let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message, - actions: [], + actions: [replyAction], intentIdentifiers: [], options: []) + let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite, actions: [], intentIdentifiers: [], diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index f690d00367..6b738a65c6 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: .randomEvent, + sendHandle: .mock, roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) } diff --git a/UnitTests/Sources/RestorationTokenTests.swift b/UnitTests/Sources/RestorationTokenTests.swift index 404ae04ad3..72009d52ef 100644 --- a/UnitTests/Sources/RestorationTokenTests.swift +++ b/UnitTests/Sources/RestorationTokenTests.swift @@ -31,7 +31,7 @@ class RestorationTokenTests: XCTestCase { XCTAssertNil(decodedToken.pusherNotificationClientIdentifier, "There should not be a push notification client ID.") XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, .sessionsBaseDirectory.appending(component: "@user_example.com"), "The session directory should match the original location set by the Rust SDK from our base directory.") - XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_example.com"), + XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: "@user_example.com"), "The cache directory should be derived from the session directory but in the caches directory.") } @@ -60,7 +60,7 @@ class RestorationTokenTests: XCTestCase { "The push notification client identifier should not be changed.") XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory, "The session directory should not be changed.") - XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName), + XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName), "The cache directory should be derived from the session directory but in the caches directory.") } @@ -75,7 +75,7 @@ class RestorationTokenTests: XCTestCase { oidcData: "data-from-mas", slidingSyncVersion: .native), sessionDirectory: .sessionsBaseDirectory.appending(component: sessionDirectoryName), - cacheDirectory: .cachesBaseDirectory.appending(component: sessionDirectoryName), + cacheDirectory: .sessionCachesBaseDirectory.appending(component: sessionDirectoryName), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusher-identifier") let data = try JSONEncoder().encode(originalToken) diff --git a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift index 2088df6e3e..4befd2efbc 100644 --- a/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsEditScreenViewModelTests.swift @@ -58,7 +58,7 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase { } func testAvatarDidChange() { - setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .picturesDirectory, members: [.mockMeAdmin])) + setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .mockMXCAvatar, members: [.mockMeAdmin])) context.send(viewAction: .removeImage) XCTAssertTrue(context.viewState.avatarDidChange) XCTAssertTrue(context.viewState.canSave) @@ -100,7 +100,7 @@ class RoomDetailsEditScreenViewModelTests: XCTestCase { } func testDeleteAvatar() { - setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .picturesDirectory, members: [.mockMeAdmin])) + setupViewModel(roomProxyConfiguration: .init(name: "Some room", avatarURL: .mockMXCAvatar, members: [.mockMeAdmin])) XCTAssertNotNil(context.viewState.avatarURL) context.send(viewAction: .removeImage) XCTAssertNil(context.viewState.avatarURL) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 18e20f4524..4ec720826e 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -218,6 +218,56 @@ class RoomFlowCoordinatorTests: XCTestCase { XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator) } + func testShareMediaRoute() async throws { + await setupRoomFlowCoordinator() + + try await process(route: .room(roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "1", mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload)) + + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + + try await process(route: .childRoom(roomID: "2", via: [])) + XCTAssertNil(navigationStackCoordinator.sheetCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1) + + try await process(route: .share(sharePayload)) + + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + } + + func testShareTextRoute() async throws { + await setupRoomFlowCoordinator() + + try await process(route: .room(roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + let sharePayload: ShareExtensionPayload = .text(roomID: "1", text: "Important text") + try await process(route: .share(sharePayload)) + + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") + + try await process(route: .childRoom(roomID: "2", via: [])) + XCTAssertNil(navigationStackCoordinator.sheetCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1) + + try await process(route: .share(sharePayload)) + + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") + } + // MARK: - Private private func process(route: AppRoute) async throws { diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 773c9af2c7..306abca905 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -184,13 +184,13 @@ class RoomScreenViewModelTests: XCTestCase { try await deferred.fulfill() configuration.name = "NewName" - configuration.avatarURL = .documentsDirectory + configuration.avatarURL = .mockMXCAvatar configuration.hasOngoingCall = true roomProxyMock.canUserJoinCallUserIDReturnValue = .success(true) deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.roomTitle == "NewName" && - viewState.roomAvatar == .room(id: "TestID", name: "NewName", avatarURL: .documentsDirectory) && + viewState.roomAvatar == .room(id: "TestID", name: "NewName", avatarURL: .mockMXCAvatar) && viewState.canJoinCall && viewState.hasOngoingCall } diff --git a/UnitTests/Sources/SessionDirectoriesTests.swift b/UnitTests/Sources/SessionDirectoriesTests.swift index 62db895c7a..dfb934b648 100644 --- a/UnitTests/Sources/SessionDirectoriesTests.swift +++ b/UnitTests/Sources/SessionDirectoriesTests.swift @@ -21,7 +21,7 @@ class SessionDirectoriesTests: XCTestCase { // Then the directories should be generated in the correct location, using an escaped version of the user ID XCTAssertEqual(sessionDirectories.dataDirectory, .sessionsBaseDirectory.appending(component: "@user_matrix.org")) - XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_matrix.org")) + XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: "@user_matrix.org")) } func testInitWithDataDirectory() { @@ -34,7 +34,7 @@ class SessionDirectoriesTests: XCTestCase { // Then the data directory should remain unchanged and the caches directory should be generated. XCTAssertEqual(sessionDirectories.dataDirectory, sessionDirectory) - XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName)) + XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName)) } func testPathOutput() { diff --git a/UnitTests/Sources/UserProfileScreenViewModelTests.swift b/UnitTests/Sources/UserProfileScreenViewModelTests.swift index f040de8c28..6706966438 100644 --- a/UnitTests/Sources/UserProfileScreenViewModelTests.swift +++ b/UnitTests/Sources/UserProfileScreenViewModelTests.swift @@ -15,7 +15,7 @@ class UserProfileScreenViewModelTests: XCTestCase { var context: UserProfileScreenViewModelType.Context { viewModel.context } func testInitialState() async throws { - let profile = UserProfileProxy(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: .picturesDirectory) + let profile = UserProfileProxy(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) @@ -35,7 +35,7 @@ class UserProfileScreenViewModelTests: XCTestCase { } func testInitialStateAccountOwner() async throws { - let profile = UserProfileProxy(userID: RoomMemberProxyMock.mockMe.userID, displayName: "Me", avatarURL: .picturesDirectory) + let profile = UserProfileProxy(userID: RoomMemberProxyMock.mockMe.userID, displayName: "Me", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 83f79dfa92..be295696a1 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -211,8 +211,8 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 1) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "1") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 1) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "1") // A child event route should push a new room screen onto the stack and focus on the event. userSessionFlowCoordinator.handleAppRoute(.childEvent(eventID: "2", roomID: "2", via: []), animated: true) @@ -221,27 +221,73 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1) XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 2) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "2") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 2) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2") // A subsequent regular event route should clear the stack and set the new room as the root of the stack. try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 3) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "3") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 3) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3") // A regular event route for the same room should set a new instance of the room as the root of the stack. try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 4) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "4", + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 4) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "4", "A new timeline should be created for the same room ID, so that the screen isn't stale while loading.") } + func testShareMediaRouteWithoutRoom() async throws { + try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil)) + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: nil, mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload), + expectedState: .shareExtensionRoomList(sharePayload: sharePayload)) + + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) + } + + func testShareMediaRouteWithRoom() async throws { + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "2", mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload), + expectedState: .roomList(selectedRoomID: "2")) + + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + } + + func testShareTextRouteWithoutRoom() async throws { + try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil)) + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text") + try await process(route: .share(sharePayload), + expectedState: .shareExtensionRoomList(sharePayload: sharePayload)) + + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) + } + + func testShareTextRouteWithRoom() async throws { + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text") + try await process(route: .share(sharePayload), + expectedState: .roomList(selectedRoomID: "2")) + + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + XCTAssertNil(splitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") + } + // MARK: - Private private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws { diff --git a/UnitTests/Sources/VoiceMessageCacheTests.swift b/UnitTests/Sources/VoiceMessageCacheTests.swift index 12f8681486..d5cb77a7f7 100644 --- a/UnitTests/Sources/VoiceMessageCacheTests.swift +++ b/UnitTests/Sources/VoiceMessageCacheTests.swift @@ -16,7 +16,7 @@ class VoiceMessageCacheTests: XCTestCase { private var mediaSource: MediaSourceProxy! private var fileManager: FileManager! - private let someURL = URL("/some/url") + private let someURL = URL.mockMXCAudio private let testFilename = "test-file" private let mpeg4aacFileExtension = "m4a" private let testTemporaryDirectory = URL.temporaryDirectory.appendingPathComponent("test-voice-messsage-cache") @@ -26,7 +26,7 @@ class VoiceMessageCacheTests: XCTestCase { voiceMessageCache.clearCache() fileManager = FileManager.default - mediaSource = MediaSourceProxy(url: someURL, mimeType: "audio/ogg") + mediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/ogg") // Create the temporary directory we will use try fileManager.createDirectory(at: testTemporaryDirectory, withIntermediateDirectories: true) diff --git a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift index 7a85f15dc4..b9dba42870 100644 --- a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift +++ b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift @@ -16,7 +16,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { private var voiceMessageCache: VoiceMessageCacheMock! private var mediaProvider: MediaProviderMock! - private let someURL = URL("/some/url") + private let someURL = URL.mockMXCAudio private let audioOGGMimeType = "audio/ogg" override func setUp() async throws { @@ -28,7 +28,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { func testLoadVoiceMessageFromSourceUnsupportedMedia() async throws { // Only "audio/ogg" file are supported - let unsupportedMediaSource = MediaSourceProxy(url: someURL, mimeType: "audio/wav") + let unsupportedMediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/wav") do { _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(unsupportedMediaSource, body: nil) XCTFail("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") @@ -49,7 +49,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { let cachedConvertedFileURL = URL("/some/url/cached_converted_file.m4a") voiceMessageCache.fileURLForReturnValue = nil - let mediaSource = MediaSourceProxy(url: someURL, mimeType: "audio/ogg; codecs=opus") + let mediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/ogg; codecs=opus") mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) @@ -67,7 +67,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { func testLoadVoiceMessageFromSourceAlreadyCached() async throws { // Check if the file is already present in cache voiceMessageCache.fileURLForReturnValue = URL("/converted_file/url") - let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) + let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTAssertEqual(url, URL("/converted_file/url")) // The file must have be search in the cache @@ -81,7 +81,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { // An error must be reported if the file cannot be retrieved do { voiceMessageCache.fileURLForReturnValue = nil - let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) + let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTFail("A `MediaProviderError.failedRetrievingFile` error is expected") } catch { @@ -102,7 +102,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { // Check if the file is not already present in cache voiceMessageCache.fileURLForReturnValue = nil - let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) + let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) let audioConverter = AudioConverterMock() voiceMessageCache.cacheMediaSourceUsingMoveReturnValue = .success(cachedConvertedFileURL) @@ -145,7 +145,7 @@ class VoiceMessageMediaManagerTests: XCTestCase { voiceMessageCache: voiceMessageCache, audioConverter: audioConverter) - let mediaSource = MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) + let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) for _ in 0..<10 { let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) XCTAssertEqual(url, cachedConvertedFileURL) diff --git a/UnitTests/Sources/VoiceMessageRecorderTests.swift b/UnitTests/Sources/VoiceMessageRecorderTests.swift index bba8e525fb..57e45a1a2f 100644 --- a/UnitTests/Sources/VoiceMessageRecorderTests.swift +++ b/UnitTests/Sources/VoiceMessageRecorderTests.swift @@ -43,9 +43,7 @@ class VoiceMessageRecorderTests: XCTestCase { audioPlayer.state = .stopped mediaPlayerProvider = MediaPlayerProviderMock() - mediaPlayerProvider.playerForClosure = { _ in - .success(self.audioPlayer) - } + mediaPlayerProvider.player = audioPlayer audioConverter = AudioConverterMock() voiceMessageCache = VoiceMessageCacheMock() voiceMessageCache.urlForRecording = FileManager.default.temporaryDirectory.appendingPathComponent("test-voice-message").appendingPathExtension("m4a") @@ -121,11 +119,10 @@ class VoiceMessageRecorderTests: XCTestCase { return } XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true) - XCTAssert(audioPlayer.loadMediaSourceUsingAutoplayCalled) - XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.url, recordingURL) - XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.mediaSource.mimeType, "audio/m4a") - XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.mediaSource.url, recordingURL) - XCTAssertEqual(audioPlayer.loadMediaSourceUsingAutoplayReceivedArguments?.autoplay, true) + XCTAssert(audioPlayer.loadSourceURLPlaybackURLAutoplayCalled) + XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.sourceURL, recordingURL) + XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.playbackURL, recordingURL) + XCTAssertEqual(audioPlayer.loadSourceURLPlaybackURLAutoplayReceivedArguments?.autoplay, true) XCTAssertFalse(audioPlayer.playCalled) } @@ -141,7 +138,7 @@ class VoiceMessageRecorderTests: XCTestCase { func testResumePlayback() async throws { try await setRecordingComplete() - audioPlayer.url = recordingURL + audioPlayer.playbackURL = recordingURL guard case .success = await voiceMessageRecorder.startPlayback() else { XCTFail("Playback should start") @@ -149,7 +146,7 @@ class VoiceMessageRecorderTests: XCTestCase { } XCTAssertEqual(voiceMessageRecorder.previewAudioPlayerState?.isAttached, true) // The media must not have been reloaded - XCTAssertFalse(audioPlayer.loadMediaSourceUsingAutoplayCalled) + XCTAssertFalse(audioPlayer.loadSourceURLPlaybackURLAutoplayCalled) XCTAssertTrue(audioPlayer.playCalled) } @@ -230,7 +227,7 @@ class VoiceMessageRecorderTests: XCTestCase { let timelineProxy = TimelineProxyMock() let roomProxy = JoinedRoomProxyMock() roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -251,7 +248,7 @@ class VoiceMessageRecorderTests: XCTestCase { let timelineProxy = TimelineProxyMock() let roomProxy = JoinedRoomProxyMock() roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -274,7 +271,7 @@ class VoiceMessageRecorderTests: XCTestCase { let timelineProxy = TimelineProxyMock() let roomProxy = JoinedRoomProxyMock() roomProxy.timeline = timelineProxy - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleReturnValue = .failure(.sdkError(SDKError.generic)) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -310,7 +307,7 @@ class VoiceMessageRecorderTests: XCTestCase { XCTAssertEqual(destination.pathExtension, "ogg") } - timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = { url, audioInfo, waveform, _, _ in + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleClosure = { url, audioInfo, waveform, _ in XCTAssertEqual(url, convertedFileURL) XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime) XCTAssertEqual(audioInfo.size, convertedFileSize) @@ -326,7 +323,7 @@ class VoiceMessageRecorderTests: XCTestCase { } XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled) - XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled) + XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformRequestHandleCalled) // the converted file must have been deleted if let convertedFileURL { diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index f24d2827e5..553041fe9c 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -31,6 +31,7 @@ targets: dependencies: - target: ElementX + - package: MatrixRustSDK info: path: ../SupportingFiles/Info.plist @@ -47,4 +48,4 @@ targets: - path: ../SupportingFiles - path: ../../ElementX/Sources/Other/InfoPlistReader.swift - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit - - path: ../Resources + - path: ../../DevelopmentAssets diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9e336b42d7..cd4d3b3e28 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -7,7 +7,7 @@ if File.exist?(enterprise) end before_all do - xcversion(version: "16.0") + xcversion(version: "16.1") ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180" @@ -84,7 +84,7 @@ lane :unit_tests do |options| run_tests( scheme: "UnitTests", - device: "iPhone 16 (18.0)", + device: "iPhone 16 (18.1)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, @@ -94,7 +94,7 @@ lane :unit_tests do |options| if !options[:skip_previews] run_tests( scheme: "PreviewTests", - device: "iPhone SE (3rd generation) (18.0)", + device: "iPhone SE (3rd generation) (18.1)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, @@ -107,15 +107,15 @@ end lane :ui_tests do |options| create_simulator_if_necessary( - name: "iPhone 16 (18.0)", + name: "iPhone-18.1", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16", - runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" + runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-1" ) create_simulator_if_necessary( - name: "iPad (10th generation) (18.0)", + name: "iPad-18.1", type: "com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation", - runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" + runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-1" ) if options[:test_name] @@ -128,7 +128,7 @@ lane :ui_tests do |options| run_tests( scheme: "UITests", - devices: ["iPhone 16 (18.0)", "iPad (10th generation) (18.0)"], + devices: ["iPhone-18.1", "iPad-18.1"], ensure_devices_found: true, prelaunch_simulator: true, result_bundle: true, @@ -143,16 +143,16 @@ lane :integration_tests do clear_derived_data() create_simulator_if_necessary( - name: "iPhone 16 Pro (18.0)", + name: "iPhone-18.1", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro", - runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-0" + runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-1" ) reset_simulator = ENV.key?('CI') run_tests( scheme: "IntegrationTests", - device: "iPhone 16 Pro (18.0)", + device: "iPhone-18.1", ensure_devices_found: true, result_bundle: true, reset_simulator: reset_simulator diff --git a/project.yml b/project.yml index 4c0b181f7b..7660990072 100644 --- a/project.yml +++ b/project.yml @@ -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.9.4 + MARKETING_VERSION: 1.9.8 CURRENT_PROJECT_VERSION: 1 SUPPORTS_MACCATALYST: false @@ -53,6 +53,7 @@ include: - path: UITests/SupportingFiles/target.yml - path: IntegrationTests/SupportingFiles/target.yml - path: NSE/SupportingFiles/target.yml +- path: ShareExtension/SupportingFiles/target.yml # - path: NCE/SupportingFiles/target.yml (not used yet) # - path: MyAppVariant/override.yml @@ -60,11 +61,11 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.65 + exactVersion: 1.0.77 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios - revision: e3f9665621872f60d3652579c3f0dc7bf806e72c + revision: 901f3f2fc150db82cf8a2c4da53914b31f681b56 # path: ../compound-ios AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events