diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml index 37a103035..39c90d509 100644 --- a/.github/workflows/ci-ui-tests.yml +++ b/.github/workflows/ci-ui-tests.yml @@ -1,9 +1,6 @@ name: UI Tests CI on: - push: - branches: [ develop ] - pull_request: workflow_dispatch: diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 738454280..a5cd49328 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -17,7 +17,8 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') + contains(github.event.issue.labels.*.name, 'A-Tags') || + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: actions/github-script@v5 with: @@ -58,8 +59,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -82,8 +83,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -106,8 +107,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -129,8 +130,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -151,8 +152,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -174,8 +175,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -197,8 +198,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -267,7 +268,7 @@ jobs: name: Add labelled issues to PS features team 3 runs-on: ubuntu-latest if: > - contains(github.event.issue.labels.*.name, 'A-Composer-WYSIWYG') + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: octokit/graphql-action@v2.x id: add_to_project @@ -286,3 +287,27 @@ jobs: env: PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + voip: + name: Add labelled issues to VoIP project board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: VoIP') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ABMIk" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-review-requests.yml b/.github/workflows/triage-review-requests.yml index 8306cffef..295a18fdf 100644 --- a/.github/workflows/triage-review-requests.yml +++ b/.github/workflows/triage-review-requests.yml @@ -58,8 +58,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -72,7 +72,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_pr_to_project: - name: Move PRs asking for design review to the design board + name: Move PRs asking for product review to the product board runs-on: ubuntu-latest steps: - uses: octokit/graphql-action@v2.x @@ -125,8 +125,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } diff --git a/CHANGES.md b/CHANGES.md index 5aa8502c9..8f8e82c1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,40 @@ +## Changes in 1.9.10 (2022-11-01) + +✨ Features + +- Changed the info in the background audio message player. ([#6870](https://github.com/vector-im/element-ios/pull/6870)) +- Added voice message support to the Rich Text Composer ([#6941](https://github.com/vector-im/element-ios/issues/6941)) + +🙌 Improvements + +- Improves external links interaction UX. ([#6936](https://github.com/vector-im/element-ios/pull/6936)) +- Verification: Deprecate legacy device-to-device verification ([#6937](https://github.com/vector-im/element-ios/pull/6937)) +- Crypto: Define MXCrypto and MXCrossSigning as protocols ([#6943](https://github.com/vector-im/element-ios/pull/6943)) +- Hide the old session list when the new device manager is enabled. ([#6999](https://github.com/vector-im/element-ios/pull/6999)) +- Upgrade MatrixSDK version ([v0.24.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.2)). +- Added a responsive placeholder text to the Rich Text Composer ([#6935](https://github.com/vector-im/element-ios/issues/6935)) +- Added the maximise/minimise toggle button to the Rich Text Composer ([#6954](https://github.com/vector-im/element-ios/issues/6954)) + +🐛 Bugfixes + +- Timeline: Fix layout for SwiftUI content views. ([#5326](https://github.com/vector-im/element-ios/issues/5326)) +- Updates the avatar image loading logics. ([#6847](https://github.com/vector-im/element-ios/issues/6847)) +- Fixes input text view height when containing multiple lines of text. ([#6849](https://github.com/vector-im/element-ios/issues/6849)) +- Fixed the placeholder flickering in the input toolbar when there is an height change. ([#6949](https://github.com/vector-im/element-ios/issues/6949)) + +🧱 Build + +- Add Z-Labs tag for rich text editor and update to the new label naming. ([#6996](https://github.com/vector-im/element-ios/pull/6996)) + +🚧 In development 🚧 + +- Device Manager: Multi-session selection. ([#6928](https://github.com/vector-im/element-ios/issues/6928)) + +Others + +- Updated templates readme file. ([#6925](https://github.com/vector-im/element-ios/issues/6925)) + + ## Changes in 1.9.9 (2022-10-18) ✨ Features diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index f891de397..f0fe26011 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.10 -CURRENT_PROJECT_VERSION = 1.9.10 +MARKETING_VERSION = 1.9.11 +CURRENT_PROJECT_VERSION = 1.9.11 diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index a89427c3a..fee3796ff 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -172,7 +172,7 @@ class CommonConfiguration: NSObject, Configurable { func setupSettingsWhenLoaded(for matrixSession: MXSession) { // Do not warn for unknown devices. We have cross-signing now - matrixSession.crypto?.warnOnUnknowDevices = false + (matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false } } diff --git a/Podfile b/Podfile index c0144e7ad..d89c32f61 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.24.1' +$matrixSDKVersion = '= 0.24.2' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -154,5 +154,14 @@ post_install do |installer| config.build_settings['WARNING_CFLAGS'] ||= ['$(inherited)','-Wno-nullability-completeness'] config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness'] end + + # Fix Xcode 14 resource bundle signing issues + # https://github.com/CocoaPods/CocoaPods/issues/11402#issuecomment-1259231655 + if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle" + target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end end diff --git a/Podfile.lock b/Podfile.lock index b3673f8ce..3d8bb303e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -55,9 +55,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.24.1): - - MatrixSDK/Core (= 0.24.1) - - MatrixSDK/Core (0.24.1): + - MatrixSDK (0.24.2): + - MatrixSDK/Core (= 0.24.2) + - MatrixSDK/Core (0.24.2): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -65,12 +65,12 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.24.1): - - MatrixSDKCrypto (= 0.1.2) - - MatrixSDK/JingleCallStack (0.24.1): + - MatrixSDK/CryptoSDK (0.24.2): + - MatrixSDKCrypto (= 0.1.5) + - MatrixSDK/JingleCallStack (0.24.2): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.2) + - MatrixSDKCrypto (0.1.5) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) @@ -122,8 +122,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.24.1) - - MatrixSDK/JingleCallStack (= 0.24.1) + - MatrixSDK (= 0.24.2) + - MatrixSDK/JingleCallStack (= 0.24.2) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -220,8 +220,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 0cb0727dd82f88d0115da93ce94ed9556a195c9b - MatrixSDKCrypto: e6e69cb16f9e459761567d078af0c17929f6a3c2 + MatrixSDK: 1b64384084050652fcffafdf8641200f1ab25060 + MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -241,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 82fb79d0a6b074f77950ec73304a749eb0329d12 +PODFILE CHECKSUM: 96a971e076c61e54ae5bb7bf30ecba80563eeacf COCOAPODS: 1.11.3 diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index ef28187e4..464458994 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "d5ef7054fb43924d5b92d5d627347ca2bc333717" + "revision" : "3cafc52d257411e06d4be4d0e2cdeeb4bd4b7099" } }, { diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json new file mode 100644 index 000000000..132fb8937 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_list_item_not_selected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg new file mode 100644 index 000000000..7b73d0c6e --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json new file mode 100644 index 000000000..7c5fd8698 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_list_item_selected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg new file mode 100644 index 000000000..13680d43a --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 6fc34bb32..5e7c7dee1 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -153,15 +153,15 @@ "room_two_users_are_typing" = "%@ und %@ tippen…"; "room_many_users_are_typing" = "%@, %@ und andere tippen…"; "room_message_placeholder" = "Nachricht senden (unverschlüsselt)…"; -"encrypted_room_message_placeholder" = "Verschlüsselte Nachricht…"; -"room_message_short_placeholder" = "Sende eine Nachricht…"; +"encrypted_room_message_placeholder" = "Verschlüsselte Nachricht senden …"; +"room_message_short_placeholder" = "Nachricht senden …"; "room_offline_notification" = "Verbindung zum Server wurde unterbrochen."; -"room_unsent_messages_notification" = "Nachrichten wurden nicht gesendet."; -"room_unsent_messages_unknown_devices_notification" = "Nachrichten wurden nicht gesendet, da unbekannte Sitzungen vorhanden waren."; +"room_unsent_messages_notification" = "Senden der Nachrichten fehlgeschlagen."; +"room_unsent_messages_unknown_devices_notification" = "Senden der Nachrichten aufgrund unbekannter Sitzungen fehlgeschlagen."; "room_prompt_resend" = "Alle erneut senden"; -"room_prompt_cancel" = "Alles abbrechen"; +"room_prompt_cancel" = "Alle abbrechen"; "room_resend_unsent_messages" = "Ungesendete Nachrichten erneut senden"; -"room_delete_unsent_messages" = "Lösche ungesendete Nachrichten"; +"room_delete_unsent_messages" = "Nicht gesendete Nachrichten löschen"; "room_event_action_copy" = "Kopieren"; "room_event_action_quote" = "Zitieren"; "room_event_action_more" = "Mehr"; @@ -301,7 +301,7 @@ "room_participants_action_unban" = "Entsperren"; "room_participants_action_set_default_power_level" = "Besondere Berechtigungen entziehen"; "room_participants_action_start_voice_call" = "Starte Sprach-Anruf"; -"room_ongoing_conference_call" = "Laufender Konferenz-Anruf. Trete bei als %@ oder %@."; +"room_ongoing_conference_call" = "Laufender Konferenzanruf. Tritt als %@ oder %@ bei."; "room_event_action_redact" = "Entfernen"; "room_warning_about_encryption" = "Ende-zu-Ende-Verschlüsselung ist in Beta und ist evtl. nicht zuverlässig.\n\nMan sollte noch nicht darauf vertrauen, dass die Daten sicher sind.\n\nGeräte werden Nachrichten von vor dem Beitritt des Raumes nicht entschlüsseln können.\n\nVerschlüsselte Nachrichten sind nicht lesbar in Anwendungen, die die Verschlüsselung noch nicht implementiert haben."; "unknown_devices_alert" = "Dieser Raum enthält unbekannte Sitzungen, die nicht verifiziert wurden.\nDas bedeutet, es gibt keine Garantie, dass sie den angegebenen Benutzern gehört.\nWir empfehlen eine Überprüfung für jedes Gerät, bevor du weitermachst. Du kannst die Nachricht auch ohne Verifizierung erneut senden."; @@ -411,14 +411,14 @@ "auth_home_server_placeholder" = "URL (z.B. https://matrix.org)"; "auth_identity_server_placeholder" = "URL (z. B. https://vector.im)"; "room_ongoing_conference_call_close" = "Schließen"; -"room_conference_call_no_power" = "Du brauchst die Berechtigung Konferenzgespräche in diesem Raum zu verwalten"; +"room_conference_call_no_power" = "Du bist nicht berechtigt, Konferenzgespräche in diesem Raum zu verwalten"; "settings_labs_create_conference_with_jitsi" = "Erstelle Konferenzgespräche mit Jitsi"; "call_already_displayed" = "Es existiert bereits ein Gespräch."; "call_jitsi_error" = "Konferenzgespräch konnte nicht betreten werden."; // Widget "widget_no_power_to_manage" = "Du brauchst die Berechtigung um Widgets in diesem Raum zu verwalten"; "widget_creation_failure" = "Widget-Erstellung fehlgeschlagen"; -"room_ongoing_conference_call_with_close" = "Laufendes Konferenzgespräch. Trete mit %@ oder %@ bei. %@ es."; +"room_ongoing_conference_call_with_close" = "Laufendes Konferenzgespräch. Tritt als %@ oder %@ bei. %@ es."; "settings_ui_theme" = "Thema"; "settings_ui_theme_auto" = "Auto"; "settings_ui_theme_light" = "Hell"; @@ -436,13 +436,13 @@ "call_incoming_voice" = "Eingehender Anruf…"; "call_incoming_video" = "Eingehender Videoanruf…"; // Widget Integration Manager -"widget_integration_need_to_be_able_to_invite" = "Du musst Benutzer einladen können um das zu tun."; +"widget_integration_need_to_be_able_to_invite" = "Du musst Benutzer einladen dürfen, um dies zu tun."; "widget_integration_unable_to_create" = "Erstellen des Widgets nicht möglich."; "widget_integration_failed_to_send_request" = "Senden der Anfrage fehlgeschlagen."; "widget_integration_room_not_recognised" = "Dieser Raum wurde nicht erkannt."; "widget_integration_positive_power_level" = "Berechtigungslevel muss eine positive Zahl sein."; "widget_integration_must_be_in_room" = "Du bist nicht in diesem Raum."; -"widget_integration_no_permission_in_room" = "Du hast keine Berechtigung dies in diesem Raum zu tun."; +"widget_integration_no_permission_in_room" = "Du bist nicht berechtigt, dies in diesem Raum zu tun."; "widget_integration_missing_room_id" = "room_id fehlt in der Anfrage."; "widget_integration_missing_user_id" = "user_id fehlt in der Anfrage."; "widget_integration_room_not_visible" = "Raum %@ ist nicht sichtbar."; @@ -502,7 +502,7 @@ // Group rooms "group_rooms_filter_rooms" = "Filtere Community-Räume"; "e2e_room_key_request_message_new_device" = "Du hast die neue Sitzung '%@' hinzugefügt, welche Verschlüsselungs-Schlüssel anfordert."; -"room_do_not_have_permission_to_post" = "Du hast keine Berechtigung Nachrichten in diesem Raum zu senden"; +"room_do_not_have_permission_to_post" = "Du bist nicht berechtigt, Nachrichten in diesem Raum zu senden"; "room_event_action_kick_prompt_reason" = "Grund für das Entfernen des Benutzers"; "room_event_action_ban_prompt_reason" = "Grund für die Verbannung der Person"; "room_action_send_photo_or_video" = "Foto oder Video senden"; @@ -532,8 +532,8 @@ "rerequest_keys_alert_title" = "Anfrage gesendet"; "rerequest_keys_alert_message" = "Bitte %@ auf einem anderen Gerät öffnen, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann."; "room_message_reply_to_placeholder" = "Antwort senden (unverschlüsselt)…"; -"encrypted_room_message_reply_to_placeholder" = "Sende eine verschlüsselte Antwort…"; -"room_message_reply_to_short_placeholder" = "Sende eine Antwort…"; +"encrypted_room_message_reply_to_placeholder" = "Verschlüsselte Antwort senden …"; +"room_message_reply_to_short_placeholder" = "Antwort senden …"; "room_replacement_information" = "Dieser Raum wurde ersetzt und ist nicht länger aktiv."; "room_replacement_link" = "Die Konversation wird hier fortgesetzt."; "room_predecessor_information" = "Dieser Raum ist die Fortsetzung einer anderen Konversation."; @@ -743,8 +743,8 @@ "room_action_send_file" = "Datei senden"; "room_message_edits_history_title" = "Bearbeitungsverlauf"; // Widget -"widget_no_integrations_server_configured" = "Kein Integrationsserver konfiguriert"; -"widget_integrations_server_failed_to_connect" = "Verbindung zum Integrationsserver fehlgeschlagen"; +"widget_no_integrations_server_configured" = "Kein Integrations-Server konfiguriert"; +"widget_integrations_server_failed_to_connect" = "Verbindung zum Integrations-Server fehlgeschlagen"; "device_verification_security_advice" = "Für maximale Sicherheit empfehlen wir, dies persönlich zu tun oder ein anderes vertrauenswürdiges Kommunikationsmittel zu verwenden"; "device_verification_incoming_description_1" = "Überprüfe diese Sitzung, um sie als vertrauenswürdig zu markieren. Sitzungen von Partnern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende verschlüsselten Nachrichten."; "device_verification_incoming_description_2" = "Wenn du diese Sitzung verifizierst, wird sie für dich und für dein Gegenüber als vertrauenswürdig gekennzeichnet."; @@ -821,7 +821,7 @@ "media_type_accessibility_video" = "Video"; "media_type_accessibility_location" = "Standort"; "media_type_accessibility_file" = "Datei"; -"media_type_accessibility_sticker" = "Aufkleber"; +"media_type_accessibility_sticker" = "Sticker"; "settings_identity_server_settings" = "IDENTITÄTSERVER"; "settings_three_pids_management_information_part1" = "Verwalte hier, mit welchen E-Mail-Adressen oder Telefonnummern du dich anmeldest, oder dein Konto wiederherstellen kannst. Kontrolliere, wer dich finden kann "; "settings_three_pids_management_information_part3" = "."; @@ -902,7 +902,7 @@ "room_participants_security_loading" = "Lade…"; "room_participants_security_information_room_not_encrypted" = "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt."; "settings_security" = "SICHERHEIT"; -"settings_integrations_allow_description" = "Benutze einen Integrationsmanager (%@), um Bots, Bridges, Widgets und Aufkleberpakete zu verwalten.\n\nIntegrationsmanager erhalten Konfigurationsdaten und können Widgets verändern, Raum-Einladungen versenden sowie Berechtigungen in deinem Namen einstellen."; +"settings_integrations_allow_description" = "Nutze einen Integrationsassistenten (%@), um Bots, Brücken, Widgets und Sticker-Pakete zu verwalten.\n\nIntegrationsassistenten erhalten Konfigurationsdaten und können Widgets verändern, Raumeinladungen versenden sowie Berechtigungen in deinem Namen einstellen."; "settings_labs_enable_cross_signing" = "Aktiviere Cross-Signing, um deinen Gesprächspartner anstatt dessen Gerät zu verifizieren (in Entwicklung)"; // Security settings "security_settings_title" = "Sicherheit"; @@ -1301,8 +1301,8 @@ "settings_show_NSFW_public_rooms" = "Öffentliche Räume mit anstößigen Inhalte anzeigen"; "room_open_dialpad" = "Wähltastatur"; "room_place_voice_call" = "Sprachanruf"; -"room_unsent_messages_cancel_message" = "Bist du dir sicher alle nicht gesendete Nachrichten in diesem Raum zu löschen?"; -"room_unsent_messages_cancel_title" = "Lösche nicht gesendete Nachrichten"; +"room_unsent_messages_cancel_message" = "Bist du dir sicher, dass du alle nicht gesendeten Nachrichten in diesem Raum löschen möchtest?"; +"room_unsent_messages_cancel_title" = "Nicht gesendete Nachrichten löschen"; "callbar_return" = "Zurück"; "callbar_only_multiple_paused" = "%@ pausierte Anrufe"; "callbar_only_single_paused" = "Pausierter Anruf"; @@ -1483,7 +1483,7 @@ // Alert explaining what an identity server / integration manager is. "service_terms_modal_information_title_identity_server" = "Indentitätsserver"; -"service_terms_modal_description_integration_manager" = "Das erlaubt dir Bots, Bridges und Stickerpacks zu verwenden."; +"service_terms_modal_description_integration_manager" = "Dies wird dir die Verwendung von Bots, Brücken und Sticker-Paketen ermöglichen."; "service_terms_modal_description_identity_server" = "Dies erlaubt Personen, die deine Telefonnummer oder E-Mail in ihren Kontakten hat, dich zu finden."; "service_terms_modal_table_header_identity_server" = "NUTZUNGSBEDINGUNGEN IDENTITÄTSSERVER"; "service_terms_modal_table_header_integration_manager" = "NUTZUNGSBEDINGUNGEN INTEGRATIONSMANAGER"; @@ -1506,12 +1506,12 @@ "poll_edit_form_add_option" = "Option hinzufügen"; "poll_edit_form_option_number" = "Option %lu"; "poll_edit_form_question_or_topic" = "Frage oder Thematik"; -"room_event_action_end_poll" = "Umfrage beenden"; -"room_event_action_remove_poll" = "Umfrage entfernen"; +"room_event_action_end_poll" = "Abstimmung beenden"; +"room_event_action_remove_poll" = "Abstimmung entfernen"; // Mark: - Polls -"poll_edit_form_create_poll" = "Umfrage erstellen"; +"poll_edit_form_create_poll" = "Abstimmung erstellen"; "settings_labs_enabled_polls" = "Umfragen"; "share_extension_send_now" = "Jetzt senden"; "accessibility_button_label" = "Knopf"; @@ -1538,7 +1538,7 @@ "poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage"; "poll_edit_form_input_placeholder" = "Schreib etwas"; "analytics_prompt_terms_link_upgrade" = "hier"; -"poll_timeline_not_closed_title" = "Fehler beim Beenden der Abstimmung"; +"poll_timeline_not_closed_title" = "Beenden der Abstimmung fehlgeschlagen"; "poll_timeline_vote_not_registered_subtitle" = "Wir konnten deine Stimme leider nicht erfassen. Versuche es bitte erneut"; "poll_timeline_total_final_results" = "Es wurden %lu Stimmen abgegeben"; "poll_timeline_total_final_results_one_vote" = "Es wurde 1 Stimme abgegeben"; @@ -1547,7 +1547,7 @@ "poll_timeline_not_closed_subtitle" = "Versuche es bitte erneut"; "poll_timeline_vote_not_registered_title" = "Stimme nicht erfasst"; "poll_edit_form_post_failure_subtitle" = "Versuche es bitte erneut"; -"poll_edit_form_post_failure_title" = "Fehler beim Senden der Abstimmung"; +"poll_edit_form_post_failure_title" = "Absenden der Abstimmung fehlgeschlagen"; "share_extension_low_quality_video_message" = "Für eine bessere Qualität sende es in %@ oder sende es in niedriger Qualität."; "share_extension_low_quality_video_title" = "Das Video wird in niedriger Qualität gesendet werden"; "analytics_prompt_stop" = "Teilen beenden"; @@ -1588,11 +1588,11 @@ "onboarding_splash_register_button_title" = "Konto erstellen"; "settings_enable_room_message_bubbles" = "Nachrichtenblasen"; "poll_edit_form_update_failure_subtitle" = "Bitte erneut versuchen"; -"poll_edit_form_poll_type" = "Umfragetyp"; -"poll_edit_form_poll_type_closed_description" = "Ergebnisse werden erst angezeigt, wenn du die Umfrage beendest"; -"poll_edit_form_poll_type_closed" = "Geschlossene Umfrage"; -"poll_edit_form_poll_type_open_description" = "Ergebnisse werden direkt nach Stimmabgabe angezeigt"; -"poll_edit_form_poll_type_open" = "Offene Umfrage"; +"poll_edit_form_poll_type" = "Abstimmungsart"; +"poll_edit_form_poll_type_closed_description" = "Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest"; +"poll_edit_form_poll_type_closed" = "Abgeschlossene Abstimmung"; +"poll_edit_form_poll_type_open_description" = "Abstimmende können die Ergebnisse nach Stimmabgabe sehen"; +"poll_edit_form_poll_type_open" = "Laufende Abstimmung"; "poll_edit_form_update_failure_title" = "Aktualisierung der Umfrage fehlgeschlagen"; "threads_empty_tip" = "Hinweis: Tippe auf eine Nachricht und wähle „Thread“ um einen neuen zu starten."; "threads_empty_info_my" = "Antworte auf einen laufenden Thread oder tippe auf eine Nachricht und wähle „Thread“ um einen neuen zu starten."; @@ -1766,7 +1766,7 @@ "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ hat den zukünftigen Verlauf für alle Raumteilnehmer ab deren Einladung sichtbar gemacht."; "notice_crypto_unable_to_decrypt" = "** Entschlüsselung nicht möglich: %@ **"; "notice_crypto_error_unknown_inbound_session_id" = "Die absendende Sitzung hat uns keine Schlüssel für diese Nachricht gesendet."; -"notice_sticker" = "Aufkleber"; +"notice_sticker" = "Sticker"; "notice_in_reply_to" = "Als Antwort auf"; // room display name "room_displayname_empty_room" = "Leerer Raum"; @@ -2365,7 +2365,7 @@ "spaces_explore_rooms_room_number" = "%@ Räume"; "spaces_create_space_title" = "Einen Space erstellen"; "spaces_add_space_title" = "Space erstellen"; -"space_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen zu diesem Space einzuladen"; +"space_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen in diesen Space einzuladen"; "room_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen zu diesem Raum einzuladen"; "room_invite_to_room_option_detail" = "Sie werden kein Teil von %@ sein."; "room_invite_to_room_option_title" = "Nur zu diesem Raum"; @@ -2639,3 +2639,15 @@ // Send Media Actions "wysiwyg_composer_start_action_media_picker" = "Fotobibliothek"; "settings_labs_enable_wysiwyg_composer" = "Probiere den Rich-Text-Editor aus (bald auch mit Plain-Text-Modus)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Sprachübertragung"; +"voice_broadcast_already_in_progress_message" = "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen."; +"voice_broadcast_blocked_by_someone_else_message" = "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest."; +"voice_broadcast_permission_denied_message" = "Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Sprachübertragung kann nicht gestartet werden"; +"settings_labs_enable_voice_broadcast" = "Sprachübertragung (in aktiver Entwicklung)"; +"voice_broadcast_playback_loading_error" = "Wiedergabe der Sprachübertragung nicht möglich."; +"deselect_all" = "Alle abwählen"; +"user_other_session_menu_select_sessions" = "Sitzungen auswählen"; +"user_other_session_selected_count" = "%@ ausgewählt"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bbf95979f..c798ed21a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -936,7 +936,7 @@ Tap the + to start adding people."; "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; "manage_session_rename" = "Rename session"; - +"manage_session_sign_out_other_sessions" = "Sign out of all other sessions"; // User sessions management "user_sessions_settings" = "Manage sessions"; @@ -2460,6 +2460,9 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_no_verified_sessions" = "No verified sessions found."; "user_other_session_no_unverified_sessions" = "No unverified sessions found."; "user_other_session_clear_filter" = "Clear filter"; +"user_other_session_selected_count" = "%@ selected"; +"user_other_session_menu_select_sessions" = "Select sessions"; +"user_other_session_menu_sign_out_sessions" = "Sign out of %@ sessions"; // First item is client name and second item is session display name "user_session_name" = "%@: %@"; @@ -2590,6 +2593,7 @@ To enable access, tap Settings> Location and select Always"; "reset_to_default" = "Reset to default"; "resend_message" = "Resend the message"; "select_all" = "Select All"; +"deselect_all" = "Deselect All"; "cancel_upload" = "Cancel Upload"; "cancel_download" = "Cancel Download"; "show_details" = "Show Details"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index f9bdd08c5..1dd7c1b42 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2506,3 +2506,86 @@ "authentication_qr_login_start_subtitle" = "Kasuta selle seadme kaamerat ja logi sisse teises seadmes kuvatud QR-koodi alusel:"; "authentication_qr_login_start_title" = "Loe QR-koodi"; "authentication_login_with_qr" = "Logi sisse QR-koodi abil"; +"wysiwyg_composer_format_action_strikethrough" = "Kasuta allajoonitud kirja"; +"wysiwyg_composer_format_action_underline" = "Kasuta läbijoonitud kirja"; +"wysiwyg_composer_format_action_italic" = "Kasuta kaldkirja"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Kasuta paksu kirja"; +"wysiwyg_composer_start_action_voice_broadcast" = "Ringhäälingukõne"; +"wysiwyg_composer_start_action_text_formatting" = "Tekstivorming"; +"wysiwyg_composer_start_action_camera" = "Kaamera"; +"wysiwyg_composer_start_action_location" = "Asukoht"; +"wysiwyg_composer_start_action_polls" = "Küsitlused"; +"wysiwyg_composer_start_action_attachments" = "Manused"; +"wysiwyg_composer_start_action_stickers" = "Kleepsud"; + + +// Mark: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Fotode kogu"; +"user_session_details_last_activity" = "Viimati kasutusel"; +"device_type_name_unknown" = "Tundmatu seadmetüüp"; +"device_type_name_mobile" = "Mobiiltelefon"; +"device_type_name_web" = "Veebiliides"; +"device_type_name_desktop" = "Töölauarakendus"; +"user_inactive_session_item_with_date" = "Pole olnud kasutusel üle 90 päeva (%@)"; +"user_inactive_session_item" = "Pole olnud kasutusel üle 90 päeva"; +"user_session_item_details_last_activity" = "Viimati kasutusel %@"; +"user_other_session_clear_filter" = "Eemalda filter"; +"user_other_session_no_unverified_sessions" = "Verifitseerimata sessioone ei leidu."; +"user_other_session_no_verified_sessions" = "Verifitseeritud sessioone ei leidu."; +"user_other_session_no_inactive_sessions" = "Ei leidu sessioone, mis pole aktiivses kasutuses."; +"user_other_session_filter_menu_inactive" = "Pole pidevas kasutuses"; +"user_other_session_filter_menu_unverified" = "Verifitseerimata"; +"user_other_session_filter_menu_verified" = "Verifitseeritud"; +"user_other_session_filter_menu_all" = "Kõik sessioonid"; +"user_other_session_filter" = "Filtreeri"; +"user_other_session_verified_sessions_header_subtitle" = "Parima turvalisuse nimel logi välja neist sessioonidest, mida sa enam ei kasuta või ei tunne ära."; +"user_other_session_current_session_details" = "Sinu praegune sessioon"; +"user_other_session_unverified_sessions_header_subtitle" = "Turvalise sõnumvahetuse nimel verifitseeri kõik oma sessioonid ning logi neist välja, mida sa enam ei kasuta või ei tunne enam ära."; +"user_other_session_security_recommendation_title" = "Turvalisusega seotud soovitused"; +"user_other_session_verified_additional_info" = "See sessioon on valmis turvaliseks sõnumivahetuseks."; +"user_other_session_unverified_additional_info" = "Parima turvalisuse ja töökindluse nimel verifitseeri see sessioon või logi ta võrgust välja."; +"user_session_verification_unknown_additional_info" = "Selle sessiooni olekut ei saa tuvastada enne kui oled ta verifitseerinud."; +"user_session_verification_unknown_short" = "Teadmata olek"; +"user_session_verification_unknown" = "Verifitseerimise olek on määratlemata"; +"user_sessions_overview_link_device" = "Seo teise seadmega"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"voice_broadcast_playback_loading_error" = "Selle ringhäälingukõne esitamine ei õnnestu."; +"voice_broadcast_already_in_progress_message" = "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus."; +"voice_broadcast_blocked_by_someone_else_message" = "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud."; +"voice_broadcast_permission_denied_message" = "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Uue ringhäälingukõne alustamine pole võimalik"; +"sign_out_confirmation_message" = "Kas sa oled kindel et soovid välja logida?"; + +// MARK: Sign out warning + +"sign_out" = "Logi välja"; +"manage_session_rename" = "Muuda sessiooni nime"; +"manage_session_name_info_link" = "Lisateave"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Palun arvesta, et sessioonide nimed on näha ka kõikidele osapooltele, kellega sa suhtled. %@"; +"manage_session_name_hint" = "Sinu enda kirjutatud sessiooninimede alusel on sul oma seadmeid lihtsam ära tunda."; +"settings_labs_enable_voice_broadcast" = "Ringhäälingukõne (aktiivses arenduses)"; +"settings_labs_enable_wysiwyg_composer" = "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)"; +"authentication_qr_login_failure_retry" = "Proovi uuesti"; +"authentication_qr_login_failure_request_timed_out" = "Sidumine ei lõppenud etteantud aja jooksul."; +"authentication_qr_login_failure_request_denied" = "Teine seade lükkas päringu tagasi."; +"authentication_qr_login_failure_invalid_qr" = "QR-kood on vigane."; +"authentication_qr_login_failure_title" = "Seose loomine ei õnenstunud"; +"authentication_qr_login_loading_signed_in" = "Sa oled oma teises seadmes sisse loginud Matrix'i võrku."; +"authentication_qr_login_loading_waiting_signin" = "Ootame, et teine seade logiks võrku."; +"authentication_qr_login_loading_connecting_device" = "Loon ühendust seadmega"; +"authentication_qr_login_confirm_alert" = "Palun vaata, et sa kindlasti tead, kust see QR-kood kuvatakse. Sellisel viisil seadmete sidumisel sa annad oma kasutajakontole täiemahulise ligipääsu."; +"authentication_qr_login_confirm_subtitle" = "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:"; +"deselect_all" = "Eemalda kõik valikud"; +"user_other_session_menu_select_sessions" = "Vali sessioonid"; +"user_other_session_selected_count" = "%@ valitud"; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index 8ae59ee6b..fe51dcd78 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -1270,3 +1270,23 @@ "microphone_access_not_granted_for_voice_message" = "جهت ارسال پیام صوتی نیاز به دسترسی به میکروفون وجود دارد اما %@ دسترسی استفاده از آن را ندارد"; "e2e_passphrase_too_short" = "کلمه عبور بیش از حد کوتاه است (حداقل می‌بایست %d کاراکتر باشد)"; "message_reply_to_sender_sent_a_voice_message" = "یک پیام صوتی ارسال کنید."; +"onboarding_splash_page_1_title" = "صاحب گفتگوهای خود شوید."; +"onboarding_splash_login_button_title" = "من از قبل حساب کاربری دارم"; + +// MARK: Onboarding +"onboarding_splash_register_button_title" = "ساخت حساب کاربری"; +"accessibility_button_label" = "دکمه"; +"saving" = "در حال ذخیره"; + +// Activities +"loading" = "در حال بارگزاری"; +"invite_to" = "دعوت به %@"; +"confirm" = "تأیید"; +"edit" = "ویرایش"; +"suggest" = "پیشنهاد"; +"add" = "افزودن"; +"existing" = "خروج"; +"new_word" = "جدید"; +"stop" = "توقف"; +"joining" = "پیوستن"; +"enable" = "فعال"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index fdb8d22d9..1f72aab67 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2625,4 +2625,15 @@ "authentication_qr_login_start_subtitle" = "Használd a kamerát ezen az eszközön a másik eszközödön megjelenő QR kód beolvasására:"; "authentication_qr_login_start_title" = "QR kód beolvasása"; "authentication_login_with_qr" = "Belépés QR kóddal"; -"settings_labs_enable_voice_broadcast" = "Hang közvetítés (aktív fejlesztés alatt). Jelenleg a hang közvetítést csak a szoba idővonalán jelezzük, egyenlőre nem lehet hangot sugározni vagy belehallgatni a közvetítésbe"; +"settings_labs_enable_voice_broadcast" = "Hang közvetítés (aktív fejlesztés alatt)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Hang közvetítés"; +"voice_broadcast_playback_loading_error" = "A hang közvetítés nem játszható le."; +"voice_broadcast_already_in_progress_message" = "Egy hang közvetítés már folyamatban van. Először fejezd be a jelenlegi közvetítést egy új indításához."; +"voice_broadcast_blocked_by_someone_else_message" = "Valaki már elindított egy hang közvetítést. Várd meg a közvetítés végét az új indításához."; +"voice_broadcast_permission_denied_message" = "Nincs jogosultságod hang közvetítést indítani ebben a szobában. Vedd fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Az új hang közvetítés nem indítható el"; +"deselect_all" = "Semmit nem jelöl ki"; +"user_other_session_menu_select_sessions" = "Munkamenetek kiválasztása"; +"user_other_session_selected_count" = "%@ kiválasztva"; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 210d60bb3..be31a8bdb 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2832,3 +2832,15 @@ "manage_session_name_info" = "Harap diketahui bahwa nama sesi juga terlihat ke orang-orang yang Anda berkomunikasi. %@"; "manage_session_name_hint" = "Nama sesi khusus dapat membantu Anda mengenal perangkat Anda dengan lebih mudah."; "settings_labs_enable_wysiwyg_composer" = "Coba editor teks kaya (mode teks biasa akan datang)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Siaran suara"; +"voice_broadcast_playback_loading_error" = "Tidak dapat memainkan siaran suara ini."; +"voice_broadcast_already_in_progress_message" = "Anda saat ini merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru."; +"voice_broadcast_blocked_by_someone_else_message" = "Ada orang lain yang saat ini merekam sebuah siaran suara. Tunggu siaran suaranya berakhir untuk memulai yang baru."; +"voice_broadcast_permission_denied_message" = "Anda tidak memiliki izin untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Tidak dapat memulai sebuah siaran suara baru"; +"settings_labs_enable_voice_broadcast" = "Siaran suara (dalam pengembangan aktif)"; +"deselect_all" = "Batalkan Semua Pilihan"; +"user_other_session_menu_select_sessions" = "Pilih sesi"; +"user_other_session_selected_count" = "%@ dipilih"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 671e2d08b..0637f0fe9 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2605,4 +2605,15 @@ "manage_session_name_info" = "Ricorda che i nomi di sessione sono anche visibili alle persone con cui comunichi. %@"; "manage_session_name_hint" = "I nomi di sessione personalizzati possono aiutarti a riconoscere i tuoi dispositivi più facilmente."; "settings_labs_enable_wysiwyg_composer" = "Prova l'editor in rich text (il testo semplice è in arrivo)"; -"settings_labs_enable_voice_broadcast" = "Broadcast voce (in sviluppo attivo). Attualmente rileviamo solo il broadcast vocale nella linea temporale della stanza, non è possibile inviare o ascoltare un vero broadcast vocale"; +"settings_labs_enable_voice_broadcast" = "Trasmissione vocale (in sviluppo attivo)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Trasmissione vocale"; +"voice_broadcast_playback_loading_error" = "Impossibile avviare questa trasmissione vocale."; +"voice_broadcast_already_in_progress_message" = "Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova."; +"voice_broadcast_blocked_by_someone_else_message" = "Qualcun altro sta già registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova."; +"voice_broadcast_permission_denied_message" = "Non hai l'autorizzazione necessaria per iniziare un broadcast vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Impossibile iniziare una nuova trasmissione vocale"; +"deselect_all" = "Deseleziona tutti"; +"user_other_session_menu_select_sessions" = "Seleziona sessioni"; +"user_other_session_selected_count" = "%@ selezionate"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 254398f9d..297ff79b0 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -2463,7 +2463,7 @@ "room_access_settings_screen_upgrade_alert_note" = "Houd er rekening mee dat bij het upgraden een nieuwe versie van de kamer wordt gemaakt. Alle huidige berichten blijven in deze gearchiveerde ruimte."; "room_access_settings_screen_upgrade_alert_message_no_param" = "Iedereen in een bovenliggende space kan deze ruimte vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen."; "room_access_settings_screen_upgrade_alert_message" = "Iedereen in %@ kan deze ruimte vinden en er lid van worden - het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen."; -"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de applicatie gebruikt."; +"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de toepassing gebruikt."; "settings_presence_offline_mode" = "Offline modus"; "settings_presence" = "Aanwezigheid"; "threads_discourage_information_2" = "\n\nWilt u toch threads inschakelen?"; @@ -2652,3 +2652,156 @@ // User sessions management "user_sessions_settings" = "Beheer sessies"; "invite_to" = "Uitnodigen %@"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "De authenticiteit van dit versleutelde bericht kan niet worden gegarandeerd op dit apparaat."; +"deselect_all" = "Deselecteer alles"; +"wysiwyg_composer_format_action_strikethrough" = "Onderstrepen formaat toepassen"; +"wysiwyg_composer_format_action_underline" = "Doorstrepen formaat toepassen"; +"wysiwyg_composer_format_action_italic" = "Cursief formaat toepassen"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Vet formaat toepassen"; +"wysiwyg_composer_start_action_voice_broadcast" = "Spraakuitzending"; +"wysiwyg_composer_start_action_text_formatting" = "Tekst opmaak"; +"wysiwyg_composer_start_action_camera" = "Camera"; +"wysiwyg_composer_start_action_location" = "Locatie"; +"wysiwyg_composer_start_action_polls" = "Peilingen"; +"wysiwyg_composer_start_action_attachments" = "Bijlagen"; +"wysiwyg_composer_start_action_stickers" = "Stikkers"; + + +// Mark: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Fotobibliotheek"; +"user_session_overview_session_details_button_title" = "Sessie details"; +"user_session_overview_session_title" = "Sessie"; +"user_session_overview_current_session_title" = "Huidige sessie"; +"user_session_details_application_url" = "URL"; +"user_session_details_application_version" = "Versie"; +"user_session_details_application_name" = "Naam"; +"user_session_details_device_os" = "Besturingssysteem"; +"user_session_details_device_browser" = "Browser"; +"user_session_details_device_model" = "Model"; +"user_session_details_device_ip_location" = "IP locatie"; +"user_session_details_device_ip_address" = "IP adres"; +"user_session_details_last_activity" = "Laatste activiteit"; +"user_session_details_session_section_footer" = "Kopieer alle gegevens door erop te tikken en ingedrukt te houden."; +"user_session_details_session_id" = "Sessie ID"; +"user_session_details_session_name" = "Sessie naam"; +"user_session_details_device_section_header" = "Apparaat"; +"device_name_unknown" = "Onbekende toepassing"; +"settings_labs_enable_new_app_layout" = "Nieuwe toepassing-indeling"; +"settings_labs_enable_new_client_info_feature" = "Noteer de naam, versie en url van de toepassing om sessies gemakkelijker te herkennen in sessiebeheer"; +"user_session_details_application_section_header" = "Toepassing"; +"user_session_details_session_section_header" = "Sessie"; +"user_session_details_title" = "Toon details"; +"device_type_name_unknown" = "Onbekend"; +"device_type_name_mobile" = "Mobiel"; +"device_type_name_web" = "Web"; +"device_type_name_desktop" = "Desktop"; +"device_name_mobile" = "%@ Mobiel"; +"device_name_web" = "%@ Web"; +"device_name_desktop" = "%@ Desktop"; +"user_inactive_session_item_with_date" = "Meer dan 90 dagen inactief (%@)"; +"user_inactive_session_item" = "90+ dagen inactief"; +"user_session_item_details_last_activity" = "Laatste activiteit %@"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_other_session_menu_select_sessions" = "Selecteer sessies"; +"user_other_session_selected_count" = "%@ geselecteerd"; +"user_other_session_clear_filter" = "Leeg filter"; +"user_other_session_no_unverified_sessions" = "Geen niet geverifieerde sessies gevonden."; +"user_other_session_no_verified_sessions" = "Geen geverifieerde sessies gevonden."; +"user_other_session_no_inactive_sessions" = "Geen inactieve sessies gevonden."; +"user_other_session_filter_menu_inactive" = "Inactief"; +"user_other_session_filter_menu_unverified" = "Niet geverifieerd"; +"user_other_session_filter_menu_verified" = "Geverifieerd"; +"user_other_session_filter_menu_all" = "Alle sessies"; +"user_other_session_filter" = "Filter"; +"user_other_session_verified_sessions_header_subtitle" = "Voor de beste beveiliging logt u uit bij elke sessie die u niet meer herkent of gebruikt."; +"user_other_session_current_session_details" = "Uw huidige sessie"; +"user_other_session_unverified_sessions_header_subtitle" = "Verifieer uw sessies voor verbeterde beveiligde berichtenuitwisseling of meld u af bij sessies die u niet meer herkent of gebruikt."; +"user_other_session_security_recommendation_title" = "Beveiligingsaanbeveling"; +"user_session_push_notifications_message" = "Indien ingeschakeld, ontvangt deze sessie pushmeldingen."; +"user_session_push_notifications" = "Pushmeldingen"; +"user_other_session_verified_additional_info" = "Deze sessie is klaar voor beveiligde berichtenuitwisseling."; +"user_other_session_unverified_additional_info" = "Verifieer of meld u af bij deze sessie voor de beste beveiliging en betrouwbaarheid."; +"user_session_verification_unknown_additional_info" = "Verifieer uw huidige sessie om de verificatiestatus van deze sessie weer te geven."; +"user_session_unverified_additional_info" = "Verifieer uw huidige sessie voor verbeterde beveiligde berichtenuitwisseling."; +"user_session_verified_additional_info" = "Uw huidige sessie is klaar voor beveiligde berichtenuitwisseling."; +"user_session_learn_more" = "Meer lezen"; +"user_session_view_details" = "Bekijk details"; +"user_session_verify_action" = "Sessie verifiëren"; +"user_session_verification_unknown_short" = "Onbekend"; +"user_session_unverified_short" = "Niet geverifieerd"; +"user_session_verified_short" = "Geverifieerd"; +"user_session_verification_unknown" = "Onbekende verificatiestatus"; +"user_session_unverified" = "Niet geverifieerde sessie"; +"user_session_verified" = "Geverifieerde sessie"; +"user_sessions_view_all_action" = "Alles bekijken (%d)"; +"user_sessions_overview_link_device" = "Een apparaat koppelen"; +"user_sessions_overview_current_session_section_title" = "Huidige sessie"; +"user_sessions_overview_other_sessions_section_info" = "Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt."; +"user_sessions_overview_other_sessions_section_title" = "Andere sessies"; +"user_sessions_overview_security_recommendations_inactive_info" = "Overweeg om u af te melden bij oude sessies (90 dagen of ouder) die u niet meer gebruikt."; +"user_sessions_overview_security_recommendations_inactive_title" = "Inactieve sessies"; +"user_sessions_overview_security_recommendations_unverified_info" = "Verifieer of meld u af bij niet geverifieerde sessies."; +"user_sessions_overview_security_recommendations_unverified_title" = "Niet geverifieerde sessies"; +"user_sessions_overview_security_recommendations_section_info" = "Verbeter uw accountbeveiliging door deze aanbevelingen op te volgen."; +"user_sessions_overview_security_recommendations_section_title" = "Beveiligingsaanbevelingen"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"all_chats_user_menu_accessibility_label" = "Gebruikersmenu"; +"voice_broadcast_playback_loading_error" = "Kan deze spraakuitzending niet afspelen."; +"voice_broadcast_already_in_progress_message" = "U neemt al een spraakuitzending op. Beëindig uw huidige spraakuitzending om een nieuwe te starten."; +"voice_broadcast_blocked_by_someone_else_message" = "Iemand anders neemt al een spraakuitzending op. Wacht tot hun spraakuitzending is afgelopen om een nieuwe te starten."; +"voice_broadcast_permission_denied_message" = "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtigingen te upgraden."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Kan geen nieuwe spraakuitzending starten"; +"sign_out_confirmation_message" = "Weet u zeker dat u zich wilt afmelden?"; + +// MARK: Sign out warning + +"sign_out" = "Afmelden"; +"manage_session_rename" = "Sessie hernoemen"; +"manage_session_name_info_link" = "Lees meer"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Houd er rekening mee dat sessienamen ook zichtbaar zijn voor mensen met wie u communiceert. %@"; +"manage_session_name_hint" = "Met aangepaste sessienamen kunt u uw apparaten gemakkelijker herkennen."; +"settings_labs_enable_voice_broadcast" = "Voice-uitzending (in actieve ontwikkeling)"; +"settings_labs_enable_wysiwyg_composer" = "Probeer de rich-text-editor (platte tekst-modus komt binnenkort)"; +"settings_labs_enable_new_session_manager" = "Nieuwe sessiemanager"; +"room_first_message_placeholder" = "Stuur uw eerste bericht…"; +"authentication_qr_login_failure_retry" = "Probeer het nog eens"; +"authentication_qr_login_failure_request_timed_out" = "De koppeling is niet binnen de vereiste tijd voltooid."; +"authentication_qr_login_failure_request_denied" = "Het verzoek is geweigerd op het andere apparaat."; +"authentication_qr_login_failure_invalid_qr" = "QR-code is ongeldig."; +"authentication_qr_login_failure_title" = "Koppelen mislukt"; +"authentication_qr_login_loading_signed_in" = "U bent nu aangemeld op uw andere apparaat."; +"authentication_qr_login_loading_waiting_signin" = "Wachten tot het apparaat zich aanmeldt."; +"authentication_qr_login_loading_connecting_device" = "Verbinden met apparaat"; +"authentication_qr_login_confirm_alert" = "Zorg ervoor dat u de herkomst van deze code kent. Door apparaten te koppelen, geeft u iemand volledige toegang tot uw account."; +"authentication_qr_login_confirm_subtitle" = "Controleer of de onderstaande code overeenkomt met uw andere apparaat:"; +"authentication_qr_login_confirm_title" = "Beveiligde verbinding tot stand gebracht"; +"authentication_qr_login_scan_subtitle" = "Positioneer de QR-code in het vierkant hieronder"; +"authentication_qr_login_scan_title" = "Scan QR-code"; +"authentication_qr_login_display_step2" = "Selecteer 'Aanmelden met QR-code'"; +"authentication_qr_login_display_step1" = "Open Element op uw andere apparaat"; +"authentication_qr_login_display_subtitle" = "Scan de onderstaande QR-code met uw apparaat dat is uitgelogd."; +"authentication_qr_login_display_title" = "Een apparaat koppelen"; +"authentication_qr_login_start_display_qr" = "QR-code weergeven op dit apparaat"; +"authentication_qr_login_start_need_alternative" = "Een alternatieve methode nodig?"; +"authentication_qr_login_start_step4" = "Selecteer 'Toon QR-code op dit apparaat'"; +"authentication_qr_login_start_step3" = "Selecteer 'Een apparaat koppelen'"; +"authentication_qr_login_start_step2" = "Ga naar Instellingen -> Beveiliging en privacy"; +"authentication_qr_login_start_step1" = "Open Element op uw andere apparaat"; +"authentication_qr_login_start_subtitle" = "Gebruik de camera op dit apparaat om de QR-code te scannen die op uw andere apparaat wordt weergegeven:"; +"authentication_qr_login_start_title" = "Scan QR-code"; +"authentication_login_with_qr" = "Log in met QR-code"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 3b82fe79e..8532c4b4d 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -751,7 +751,7 @@ "group_participants_invited_section" = "ZAPROSZONY"; "receipt_status_read" = "Odczytano: "; // Media picker -"media_picker_title" = "Selektor mediów"; +"media_picker_title" = "Biblioteka mediów"; // Image picker "image_picker_action_camera" = "Zrób zdjęcie"; "image_picker_action_library" = "Wybierz z biblioteki"; @@ -2569,7 +2569,7 @@ // Mark: - All Chats -"all_chats_title" = "Wszystkie rozmowy"; +"all_chats_title" = "Rozmowy"; "spaces_subspace_creation_visibility_message" = "Utworzona przestrzeń zostanie dodana do %@."; "spaces_subspace_creation_visibility_title" = "Jakiego rodzaju podprzestrzeń chcesz utworzyć?"; "spaces_explore_rooms_format" = "Przeglądaj %@"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 3d6b5ee57..3741d9841 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1688,7 +1688,7 @@ "invite_user" = "Convidar Usuária(o) matrix"; "reset_to_default" = "Resettar para default"; "resend_message" = "Reenviar a mensagem"; -"select_all" = "Selecionar Todas"; +"select_all" = "Selecionar Todas(os)"; "cancel_upload" = "Cancelar Upload"; "cancel_download" = "Cancelar Download"; "show_details" = "Mostrar Detalhes"; @@ -2606,3 +2606,15 @@ "manage_session_name_info" = "Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica. %@"; "manage_session_name_hint" = "Nomes de sessões personalizados podem ajudar você a reconhecer seus dispositivos mais facilmente."; "settings_labs_enable_wysiwyg_composer" = "Experimente o editor de texto rico (modo de texto puro vindo em breve)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Broadcast de voz"; +"voice_broadcast_playback_loading_error" = "Incapaz de tocar este broadcast de voz."; +"voice_broadcast_already_in_progress_message" = "Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo."; +"voice_broadcast_blocked_by_someone_else_message" = "Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo."; +"voice_broadcast_permission_denied_message" = "Você não tem as permissões requeridas para começar um broadcast de voz nesta sala. Contacte um(a) administrador(a) da sala para fazer upgrade de suas permissões."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Não dá para começar um novo broadcast de voz"; +"settings_labs_enable_voice_broadcast" = "Broadcast de voz (sob desenvolvimento ativo)"; +"deselect_all" = "Desselecionar Todas(os)"; +"user_other_session_menu_select_sessions" = "Selecionar sessões"; +"user_other_session_selected_count" = "%@ selecionadas"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 5c3fb89eb..7680e9765 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -603,7 +603,7 @@ "key_backup_recover_from_passphrase_passphrase_title" = "Ввод"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "Введите секретную фразу"; "key_backup_recover_from_passphrase_recover_action" = "Разблокировать историю"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Не знаете вашу секретную фразу для восстановления? Вы можете "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Не помните свою мнемоническую фразу? Вы можете "; "key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "использовать ключ безопасности"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; "key_backup_recover_from_recovery_key_info" = "Используйте ключ безопасности для разблокировки истории безопасных сообщений"; @@ -624,7 +624,7 @@ "key_backup_setup_success_from_recovery_key_recovery_key_title" = "Ключ безопасности"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Сделать копию"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "Я сделал копию"; -"key_backup_recover_invalid_passphrase_title" = "Неверная секретная фраза для восстановления"; +"key_backup_recover_invalid_passphrase_title" = "Неверная мнемоническая фраза"; "key_backup_recover_invalid_recovery_key_title" = "Несоответствующий ключ безопасности"; "key_backup_setup_banner_title" = "Не теряйте зашифрованные сообщения"; "key_backup_setup_banner_subtitle" = "Начать использовать ключ восстановления"; @@ -641,7 +641,7 @@ "key_backup_setup_intro_setup_action_with_existing_backup" = "Использовать ключ восстановления"; "settings_key_backup_info" = "Зашифрованные сообщения защищены сквозным шифрованием. Только вы и получатель(и) имеют ключи для чтения этих сообщений."; "settings_key_backup_info_signout_warning" = "Сделайте резервную копию ключей перед выходом, чтобы не потерять их."; -"key_backup_setup_passphrase_title" = "Защитите резервную копию секретной фразой"; +"key_backup_setup_passphrase_title" = "Защитите резервную копию мнемонической фразой"; "key_backup_setup_passphrase_setup_recovery_key_info" = "Или защитите свою резервную копию с помощью ключа безопасности, сохранив ее в безопасном месте."; "key_backup_setup_passphrase_setup_recovery_key_action" = "(Расширенный) Настройка с ключом безопасности"; // Success from passphrase @@ -654,7 +654,7 @@ "sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Зашифрованные сообщения будут утеряны"; "sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Мне не нужны мои зашифрованные сообщения"; "sign_out_non_existing_key_backup_alert_title" = "Вы потеряете доступ к зашифрованным сообщениям если выйдете сейчас"; -"key_backup_recover_invalid_passphrase" = "Невозможно расшифровать резервную копию с помощью этой секретной фразы: убедитесь, что вы ввели верную секретную фразу для восстановления."; +"key_backup_recover_invalid_passphrase" = "Невозможно расшифровать резервную копию с помощью этой фразы: убедитесь, что вы ввели верную мнемоническую фразу."; "key_backup_recover_invalid_recovery_key" = "Невозможно расшифровать резервную копию с помощью этого ключа: убедитесь, что вы ввели верный ключ безопасности."; "e2e_key_backup_wrong_version_button_settings" = "Настройки"; "key_backup_setup_intro_manual_export_info" = "(Расширенный)"; @@ -986,7 +986,7 @@ "secure_key_backup_setup_intro_info" = "Защитите себя от потери доступа к зашифрованным сообщениям и данным, создав резервную копию ключей шифрования на своём сервере."; "secure_key_backup_setup_intro_use_security_key_title" = "Используйте ключ безопасности"; "secure_key_backup_setup_intro_use_security_key_info" = "Создайте ключ безопасности для хранения в надежном месте, например в менеджере паролей или сейфе."; -"secure_key_backup_setup_intro_use_security_passphrase_title" = "Использовать секретную фразу"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Использовать мнемоническую фразу"; "secure_key_backup_setup_intro_use_security_passphrase_info" = "Введите секретную фразу, известную только вам, и создайте ключ для резервного копирования."; "secure_key_backup_setup_existing_backup_error_title" = "Резервная копия сообщений уже существует"; "secure_key_backup_setup_existing_backup_error_info" = "Разблокируйте его для повторного использования в защищенной резервной копии или удалите для создания новой резервной копии сообщений в защищенной резервной копии."; @@ -1024,7 +1024,7 @@ "device_verification_self_verify_wait_information" = "Подтвердите этот сеанс на одном из других ваших сеансов, предоставив ему доступ к зашифрованным сообщениям.\n\nИспользуйте последнюю версию %@ на других ваших устройствах:"; "device_verification_self_verify_wait_additional_information" = "Это работает с %@ и другими клиентами Matrix с поддержкой кросс-подписи."; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Используйте ключ безопасности"; -"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Используйте секретную фразу или ключ безопасности"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Используйте мнемоническую фразу или бумажный ключ"; "device_verification_self_verify_wait_recover_secrets_additional_information" = "Если вы не можете получить доступ к существующему сеансу"; "key_verification_verify_sas_title_emoji" = "Сравните смайлы"; "key_verification_verify_sas_title_number" = "Сравните числа"; @@ -1102,17 +1102,17 @@ "user_verification_session_details_verify_action_current_user" = "Интерактивная проверка"; "user_verification_session_details_verify_action_current_user_manually" = "Ручная проверка с помощью текста"; "user_verification_session_details_verify_action_other_user" = "Подтверждение вручную"; -"secrets_recovery_with_passphrase_title" = "Секретная фраза"; +"secrets_recovery_with_passphrase_title" = "Мнемоническая фраза"; "secrets_recovery_with_passphrase_information_default" = "Получите доступ к своей защищённой истории сообщений и вашей личности с кросс-подписью для проверки других сеансов, введя секретную фразу."; -"secrets_recovery_with_passphrase_information_verify_device" = "Используйте секретную фразу, чтобы проверить это устройство."; +"secrets_recovery_with_passphrase_information_verify_device" = "Используйте свою мнемоническую фразу, чтобы заверить эту сессию."; "secrets_recovery_with_passphrase_passphrase_title" = "Ввод"; -"secrets_recovery_with_passphrase_passphrase_placeholder" = "Введите секретную фразу"; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Введите мнемоническую фразу"; "secrets_recovery_with_passphrase_recover_action" = "Использовать секретную фразу"; -"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Не знаете вашу секретную фразу? Вы можете "; -"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "использовать ключ безопасности"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Не помните свою мнемоническую фразу? Вы можете "; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "использовать бумажный ключ"; "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; "secrets_recovery_with_passphrase_invalid_passphrase_title" = "Невозможно получить доступ к секретному хранилищу"; -"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Убедитесь, что вы ввели правильную секретную фразу."; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Убедитесь, что вы ввели верную мнемоническую фразу."; "secrets_recovery_with_key_title" = "Ключ безопасности"; "secrets_recovery_with_key_information_default" = "Получите доступ к своей защищённой истории сообщений и вашей личности с кросс-подписью для проверки других сеансов, введя ключ безопасности."; "secrets_recovery_with_key_information_verify_device" = "Используйте ключ безопасности, чтобы проверить это устройство."; @@ -1128,11 +1128,11 @@ "secrets_setup_recovery_key_done_action" = "Готово"; "secrets_setup_recovery_key_storage_alert_title" = "Храните его в безопасности"; "secrets_setup_recovery_key_storage_alert_message" = "✓ Распечатайте и храните в безопасном месте\n✓ Сохраните его на USB-носителе или резервном носителе\n✓ Скопируйте его в свое личное облачное хранилище"; -"secrets_setup_recovery_passphrase_title" = "Задайте секретную фразу"; +"secrets_setup_recovery_passphrase_title" = "Задайте мнемоническую фразу"; "secrets_setup_recovery_passphrase_information" = "Введите секретную фразу, известную только вам, для защиты данных на вашем сервере."; "secrets_setup_recovery_passphrase_additional_information" = "Не используйте пароль своей учетной записи."; "secrets_setup_recovery_passphrase_validate_action" = "Готово"; -"secrets_setup_recovery_passphrase_confirm_information" = "Для подтверждения введите вашу секретную фразу ещё раз."; +"secrets_setup_recovery_passphrase_confirm_information" = "Введите мнемоническую фразу ещё раз, чтобы подтвердить её."; "secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Подтвердить"; "secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Подтвердить секретную фразу"; "cross_signing_setup_banner_title" = "Настройка шифрования"; @@ -1238,8 +1238,8 @@ // MARK: - Home "home_empty_view_title" = "Добро пожаловать в %@,\n%@"; -"secrets_setup_recovery_passphrase_summary_information" = "Запомните свою секретную фразу. Её можно использовать для разблокировки ваших зашифрованных сообщений и данных."; -"secrets_setup_recovery_passphrase_summary_title" = "Сохраните вашу секретную фразу"; +"secrets_setup_recovery_passphrase_summary_information" = "Запомните свою мнемоническую фразу. Её можно использовать для разблокировки ваших зашифрованных сообщений и данных."; +"secrets_setup_recovery_passphrase_summary_title" = "Сохраните свою мнемоническую фразу"; "favourites_empty_view_information" = "Вы можете добавить в избранное несколькими способами - самый быстрый - просто нажать и удерживать. Нажмите на звёздочку, и они автоматически появятся здесь, и вы их навсегда сохраните."; // MARK: - Favourites @@ -1355,7 +1355,7 @@ "space_feature_unavailable_title" = "Пространств ещё нет"; "secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Введите свой ключ безопасности, чтобы продолжить."; -"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Введите секретную фразу, чтобы продолжить."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Введите мнемоническую фразу, чтобы продолжить."; "key_verification_verify_qr_code_scan_code_other_device_action" = "Сканирование с помощью этого устройства"; // Success from secure backup diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 78fe3194e..ba65a69cb 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2828,3 +2828,15 @@ "manage_session_name_info" = "Uvedomte si, že názvy relácií sú viditeľné aj pre ľudí, s ktorými komunikujete. %@"; "manage_session_name_hint" = "Vlastné názvy relácií vám pomôžu ľahšie rozpoznať vaše zariadenia."; "settings_labs_enable_wysiwyg_composer" = "Vyskúšajte rozšírený textový editor (čistý textový režim sa objaví čoskoro)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Hlasové vysielanie"; +"voice_broadcast_already_in_progress_message" = "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové."; +"voice_broadcast_blocked_by_someone_else_message" = "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové."; +"voice_broadcast_permission_denied_message" = "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Nie je možné spustiť nové hlasové vysielanie"; +"settings_labs_enable_voice_broadcast" = "Hlasové vysielanie (v štádiu aktívneho vývoja)"; +"voice_broadcast_playback_loading_error" = "Toto hlasové vysielanie nie je možné prehrať."; +"deselect_all" = "Zrušiť výber všetkých"; +"user_other_session_selected_count" = "%@ vybratých"; +"user_other_session_menu_select_sessions" = "Vyberte relácie"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 95706cdf6..d1d1d7d7c 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2830,3 +2830,15 @@ "manage_session_name_info" = "Зауважте, що назви сеансів також видно людям, з якими ви спілкуєтесь. %@"; "manage_session_name_hint" = "Власні назви сеансів допоможуть вам легше розпізнавати ваші пристрої."; "settings_labs_enable_wysiwyg_composer" = "Спробуйте розширений текстовий редактор (незабаром з'явиться режим звичайного тексту)"; +"wysiwyg_composer_start_action_voice_broadcast" = "Голосові повідомлення"; +"voice_broadcast_playback_loading_error" = "Неможливо відтворити це голосове повідомлення."; +"voice_broadcast_already_in_progress_message" = "Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову."; +"voice_broadcast_blocked_by_someone_else_message" = "Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову."; +"voice_broadcast_permission_denied_message" = "Ви не маєте необхідних дозволів для початку трансляції голосового повідомлення в цій кімнаті. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "Не вдалося розпочати трансляцію нового голосового повідомлення"; +"settings_labs_enable_voice_broadcast" = "Голосові повідомлення (в активній розробці)"; +"deselect_all" = "Скасувати вибір усіх"; +"user_other_session_menu_select_sessions" = "Вибрати сеанси"; +"user_other_session_selected_count" = "Вибрано %@"; diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index 801221597..df47c1674 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -329,7 +329,7 @@ { if (self.mxSession.crypto) { - [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] onComplete:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) { + [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) { UserEncryptionTrustLevel userEncryptionTrustLevel; double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted; @@ -341,7 +341,7 @@ else if (trustedDevicesPercentage == 0.0) { // Verify if the user has the user has cross-signing enabled - if ([self.mxSession.crypto crossSigningKeysForUser:userId]) + if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId]) { userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified; } @@ -357,6 +357,9 @@ onComplete(userEncryptionTrustLevel); + } failure:^(NSError *error) { + MXLogErrorDetails(@"[MXRoom+Riot] Error fetching trust level summary", error); + onComplete(UserEncryptionTrustLevelUnknown); }]; } else diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 99c2c7041..922174427 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -127,6 +127,8 @@ internal class Asset: NSObject { internal static let userOtherSessionsUnverified = ImageAsset(name: "user_other_sessions_unverified") internal static let userOtherSessionsVerified = ImageAsset(name: "user_other_sessions_verified") internal static let userSessionListItemInactiveSession = ImageAsset(name: "user_session_list_item_inactive_session") + internal static let userSessionListItemNotSelected = ImageAsset(name: "user_session_list_item_not_selected") + internal static let userSessionListItemSelected = ImageAsset(name: "user_session_list_item_selected") internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified") internal static let userSessionVerificationUnknown = ImageAsset(name: "user_session_verification_unknown") internal static let userSessionVerified = ImageAsset(name: "user_session_verified") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4a6cd2f1b..773f4c39b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1523,6 +1523,10 @@ public class VectorL10n: NSObject { public static var delete: String { return VectorL10n.tr("Vector", "delete") } + /// Deselect All + public static var deselectAll: String { + return VectorL10n.tr("Vector", "deselect_all") + } /// This operation requires additional authentication.\nTo continue, please enter your password. public static var deviceDetailsDeletePromptMessage: String { return VectorL10n.tr("Vector", "device_details_delete_prompt_message") @@ -3643,6 +3647,10 @@ public class VectorL10n: NSObject { public static var manageSessionSignOut: String { return VectorL10n.tr("Vector", "manage_session_sign_out") } + /// Sign out of all other sessions + public static var manageSessionSignOutOtherSessions: String { + return VectorL10n.tr("Vector", "manage_session_sign_out_other_sessions") + } /// Manage session public static var manageSessionTitle: String { return VectorL10n.tr("Vector", "manage_session_title") @@ -8671,6 +8679,14 @@ public class VectorL10n: NSObject { public static var userOtherSessionFilterMenuVerified: String { return VectorL10n.tr("Vector", "user_other_session_filter_menu_verified") } + /// Select sessions + public static var userOtherSessionMenuSelectSessions: String { + return VectorL10n.tr("Vector", "user_other_session_menu_select_sessions") + } + /// Sign out of %@ sessions + public static func userOtherSessionMenuSignOutSessions(_ p1: String) -> String { + return VectorL10n.tr("Vector", "user_other_session_menu_sign_out_sessions", p1) + } /// No inactive sessions found. public static var userOtherSessionNoInactiveSessions: String { return VectorL10n.tr("Vector", "user_other_session_no_inactive_sessions") @@ -8687,6 +8703,10 @@ public class VectorL10n: NSObject { public static var userOtherSessionSecurityRecommendationTitle: String { return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title") } + /// %@ selected + public static func userOtherSessionSelectedCount(_ p1: String) -> String { + return VectorL10n.tr("Vector", "user_other_session_selected_count", p1) + } /// Verify or sign out from this session for best security and reliability. public static var userOtherSessionUnverifiedAdditionalInfo: String { return VectorL10n.tr("Vector", "user_other_session_unverified_additional_info") diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index c84ff4809..dff221ba3 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -195,7 +195,9 @@ UINavigationControllerDelegate - (BOOL)presentIncomingKeyVerificationRequest:(id)incomingKeyVerificationRequest inSession:(MXSession*)session; -- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession; +- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember + session:(MXSession*)mxSession + completion:(void (^)(void))completion; - (BOOL)presentCompleteSecurityForSession:(MXSession*)mxSession; diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 65650817c..63d2afe50 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -128,6 +128,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni If any the currently displayed key verification dialog */ KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter; + + /** + Completion block for the requester of key verification + */ + void (^keyVerificationCompletionBlock)(void); /** Currently displayed secure backup setup @@ -2283,9 +2288,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Stay in launching during the first server sync if the store is empty. isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); - if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists) + if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists && [mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) { - [mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; + [(MXLegacyCrypto *)mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; } break; case MXSessionStateRunning: @@ -2498,6 +2503,12 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)checkLocalPrivateKeysInSession:(MXSession*)mxSession { + if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + return; + } + MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; + MXRecoveryService *recoveryService = mxSession.crypto.recoveryService; NSUInteger keysCount = 0; if ([recoveryService hasSecretWithSecretId:MXSecretId.keyBackup]) @@ -2518,7 +2529,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { // We should have 3 of them. If not, request them again as mitigation MXLogDebug(@"[AppDelegate] checkLocalPrivateKeysInSession: request keys because keysCount = %@", @(keysCount)); - [mxSession.crypto requestAllPrivateKeys]; + [crypto requestAllPrivateKeys]; } } @@ -3478,17 +3489,24 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession called while the app is not active. Ignore it."); return; } + + if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Only legacy crypto allows manually accepting/rejecting key requests"); + return; + } + MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; MXWeakify(self); - [mxSession.crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { + [crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { MXStrongifyAndReturnIfNil(self); MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@", - mxSession.crypto.crossSigning.state, + crypto.crossSigning.state, @(pendingKeyRequests.count), self->roomKeyRequestViewController ? @"YES" : @"NO"); - if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) + if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) { if (self->roomKeyRequestViewController) { @@ -3518,13 +3536,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Give the client a chance to refresh the device list MXWeakify(self); - [mxSession.crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { + [crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { MXStrongifyAndReturnIfNil(self); MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId]; if (deviceInfo) { - if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) + if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) { BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); @@ -3532,7 +3550,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); - self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession onComplete:^{ + self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{ self->roomKeyRequestViewController = nil; @@ -3546,7 +3564,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // If the device was new before, it's not any more. if (wasNewDevice) { - [mxSession.crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; + [crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; } else { @@ -3555,13 +3573,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } else if (deviceInfo.trustLevel.isVerified) { - [mxSession.crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ [self checkPendingRoomKeyRequests]; }]; } else { - [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ [self checkPendingRoomKeyRequests]; }]; } @@ -3569,7 +3587,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni else { MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId); - [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ [self checkPendingRoomKeyRequests]; }]; } @@ -3700,7 +3718,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return presented; } -- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession +- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember + session:(MXSession*)mxSession + completion:(void (^)(void))completion; { MXLogDebug(@"[AppDelegate][MXKeyVerification] presentUserVerificationForRoomMember: %@", roomMember); @@ -3713,6 +3733,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [keyVerificationCoordinatorBridgePresenter presentFrom:self.presentedViewController roomMember:roomMember animated:YES]; presented = YES; + + keyVerificationCompletionBlock = completion; } else { @@ -3744,11 +3766,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { - MXCrypto *crypto = coordinatorBridgePresenter.session.crypto; - if (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled) + id crypto = coordinatorBridgePresenter.session.crypto; + if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled)) { MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); - [crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; + [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; } [self dismissKeyVerificationCoordinatorBridgePresenter]; } @@ -3765,6 +3787,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni }]; keyVerificationCoordinatorBridgePresenter = nil; + + if (keyVerificationCompletionBlock) { + keyVerificationCompletionBlock(); + } + keyVerificationCompletionBlock = nil; } #pragma mark - New request @@ -3984,7 +4011,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)registerUserDidSignInOnNewDeviceNotificationForSession:(MXSession*)session { - MXCrossSigning *crossSigning = session.crypto.crossSigning; + id crossSigning = session.crypto.crossSigning; if (!crossSigning) { @@ -4075,7 +4102,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)registerDidChangeCrossSigningKeysNotificationForSession:(MXSession*)session { - MXCrossSigning *crossSigning = session.crypto.crossSigning; + id crossSigning = session.crypto.crossSigning; if (!crossSigning) { diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 47ed8bfab..5aa6b3731 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -758,8 +758,8 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate { // MARK: - KeyVerificationCoordinatorDelegate extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto, - !crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled { + if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, + !backup.hasPrivateKeyInCryptoStore || !backup.enabled { MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) } @@ -810,5 +810,4 @@ extension AuthenticationCoordinator: AuthFallBackViewControllerDelegate { func authFallBackViewControllerDidClose(_ authFallBackViewController: AuthFallBackViewController) { dismissFallback() } - } diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index 13b776c4e..e8ca770ab 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -219,8 +219,8 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate // MARK: - KeyVerificationCoordinatorDelegate extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto, - !crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled { + if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, + !backup.hasPrivateKeyInCryptoStore || !backup.enabled { MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) } diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift index 7d5d7f48b..214c76695 100644 --- a/Riot/Modules/Authentication/SessionVerificationListener.swift +++ b/Riot/Modules/Authentication/SessionVerificationListener.swift @@ -69,7 +69,7 @@ class SessionVerificationListener { } if session.state == .storeDataReady { - if let crypto = session.crypto, crypto.crossSigning != nil { + if let crypto = session.crypto as? MXLegacyCrypto { // Do not make key share requests while the "Complete security" is not complete. // If the device is self-verified, the SDK will restore the existing key backup. // Then, it will re-enable outgoing key share requests @@ -78,7 +78,8 @@ class SessionVerificationListener { } else if session.state == .running { unregisterSessionStateChangeNotification() - if let crypto = session.crypto, let crossSigning = crypto.crossSigning { + if let crypto = session.crypto { + let crossSigning = crypto.crossSigning crossSigning.refreshState { [weak self] stateUpdated in guard let self = self else { return } @@ -100,7 +101,7 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } else { @@ -110,12 +111,12 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } } else { - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } case .crossSigningExists: @@ -124,12 +125,12 @@ class SessionVerificationListener { default: MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } failure: { [weak self] error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self?.completion?(.authenticationIsComplete) } } else { diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 71c52ba72..a8019c06c 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -373,7 +373,12 @@ CallAudioRouteMenuViewDelegate> // Acknowledge the existence of all devices [self startActivityIndicator]; - [self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{ + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices"); + return; + } + [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{ [self stopActivityIndicator]; diff --git a/Riot/Modules/CrossSigning/CrossSigningService.swift b/Riot/Modules/CrossSigning/CrossSigningService.swift index ab40e6369..a4df60aaa 100644 --- a/Riot/Modules/CrossSigning/CrossSigningService.swift +++ b/Riot/Modules/CrossSigning/CrossSigningService.swift @@ -78,14 +78,14 @@ final class CrossSigningService: NSObject { func setupCrossSigningRequest() -> AuthenticatedEndpointRequest { let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" - return AuthenticatedEndpointRequest(path: path, httpMethod: "POST") + return AuthenticatedEndpointRequest(path: path, httpMethod: "POST", params: [:]) } /// Setup cross-signing without authentication. Useful when a grace period is enabled. @discardableResult func setupCrossSigningWithoutAuthentication(for session: MXSession, success: @escaping (() -> Void), failure: @escaping ((Error) -> Void)) -> MXHTTPOperation? { - guard let crossSigning = session.crypto.crossSigning else { + guard let crossSigning = session.crypto?.crossSigning else { failure(CrossSigningServiceError.unknown) return nil } diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift index f545b2e44..2877de09d 100644 --- a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift +++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift @@ -72,7 +72,7 @@ final class CrossSigningSetupCoordinator: CrossSigningSetupCoordinatorType { } private func setupCrossSigning(with authenticationParameters: [String: Any]) { - guard let crossSigning = self.parameters.session.crypto.crossSigning else { + guard let crossSigning = self.parameters.session.crypto?.crossSigning else { return } diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift index d7d0a9a31..82404834e 100644 --- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift +++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift @@ -22,7 +22,7 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType { // MARK: Private - private let session: MXSession + private let keyBackup: MXKeyBackup private let navigationRouter: NavigationRouterType private let keyBackupVersion: MXKeyBackupVersion @@ -34,8 +34,8 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType { // MARK: - Setup - init(session: MXSession, keyBackupVersion: MXKeyBackupVersion, navigationRouter: NavigationRouterType? = nil) { - self.session = session + init(keyBackup: MXKeyBackup, keyBackupVersion: MXKeyBackupVersion, navigationRouter: NavigationRouterType? = nil) { + self.keyBackup = keyBackup self.keyBackupVersion = keyBackupVersion if let navigationRouter = navigationRouter { @@ -52,7 +52,7 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType { let rootCoordinator: Coordinator & Presentable // Check if we have the private key locally - if self.session.crypto.backup.hasPrivateKeyInCryptoStore { + if keyBackup.hasPrivateKeyInCryptoStore { rootCoordinator = self.createRecoverFromPrivateKeyCoordinator() } else { rootCoordinator = self.createRecoverWithUserInteractionCoordinator() @@ -93,19 +93,19 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType { } private func createRecoverFromPrivateKeyCoordinator() -> KeyBackupRecoverFromPrivateKeyCoordinator { - let coordinator = KeyBackupRecoverFromPrivateKeyCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion) + let coordinator = KeyBackupRecoverFromPrivateKeyCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion) coordinator.delegate = self return coordinator } private func createRecoverFromPassphraseCoordinator() -> KeyBackupRecoverFromPassphraseCoordinator { - let coordinator = KeyBackupRecoverFromPassphraseCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion) + let coordinator = KeyBackupRecoverFromPassphraseCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion) coordinator.delegate = self return coordinator } private func createRecoverFromRecoveryKeyCoordinator() -> KeyBackupRecoverFromRecoveryKeyCoordinator { - let coordinator = KeyBackupRecoverFromRecoveryKeyCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion) + let coordinator = KeyBackupRecoverFromRecoveryKeyCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion) coordinator.delegate = self return coordinator } diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift index a06e9befd..2d5be4578 100644 --- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift @@ -49,7 +49,12 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject { // MARK: - Public func present(from viewController: UIViewController, animated: Bool) { - let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion) + guard let keyBackup = session.crypto?.backup else { + MXLog.failure("[KeyBackupRecoverCoordinatorBridgePresenter] Cannot setup backups without backup module") + return + } + + let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(keyBackup: keyBackup, keyBackupVersion: keyBackupVersion) keyBackupSetupCoordinator.delegate = self viewController.present(keyBackupSetupCoordinator.toPresentable(), animated: animated, completion: nil) keyBackupSetupCoordinator.start() @@ -58,12 +63,16 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject { } func push(from navigationController: UINavigationController, animated: Bool) { + guard let keyBackup = session.crypto?.backup else { + MXLog.failure("[KeyBackupRecoverCoordinatorBridgePresenter] Cannot setup backups without backup module") + return + } MXLog.debug("[KeyBackupRecoverCoordinatorBridgePresenter] Push complete security from \(navigationController)") let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) - let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter) + let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(keyBackup: keyBackup, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter) keyBackupSetupCoordinator.delegate = self keyBackupSetupCoordinator.start() // Will trigger view controller push diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift index 03171ebd2..aab964c2f 100644 --- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift @@ -66,7 +66,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType { private func createSetupIntroViewController() -> KeyBackupSetupIntroViewController { - let backupState = self.session.crypto.backup?.state ?? MXKeyBackupStateUnknown + let backupState = self.session.crypto?.backup?.state ?? MXKeyBackupStateUnknown let isABackupAlreadyExists: Bool switch backupState { @@ -99,7 +99,12 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType { } private func showSetupPassphrase(animated: Bool) { - let keyBackupSetupPassphraseCoordinator = KeyBackupSetupPassphraseCoordinator(session: self.session) + guard let keyBackup = self.session.crypto?.backup else { + MXLog.failure("[KeyBackupSetupCoordinator] Cannot setup backups without backup module") + return + } + + let keyBackupSetupPassphraseCoordinator = KeyBackupSetupPassphraseCoordinator(keyBackup: keyBackup) keyBackupSetupPassphraseCoordinator.delegate = self keyBackupSetupPassphraseCoordinator.start() @@ -130,7 +135,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType { } private func createKeyBackupUsingSecureBackup(privateKey: Data, completion: @escaping (Result) -> Void) { - guard let keyBackup = session.crypto.backup, let recoveryService = session.crypto.recoveryService else { + guard let keyBackup = session.crypto?.backup, let recoveryService = session.crypto?.recoveryService else { return } diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift index ea0d2b549..f9c0342be 100644 --- a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift @@ -23,7 +23,6 @@ final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordin // MARK: Private - private let session: MXSession private var keyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModelType private let keyBackupSetupPassphraseViewController: KeyBackupSetupPassphraseViewController @@ -35,10 +34,8 @@ final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordin // MARK: - Setup - init(session: MXSession) { - self.session = session - - let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: self.session.crypto.backup) + init(keyBackup: MXKeyBackup) { + let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: keyBackup) let keyBackupSetupPassphraseViewController = KeyBackupSetupPassphraseViewController.instantiate(with: keyBackupSetupPassphraseViewModel) self.keyBackupSetupPassphraseViewModel = keyBackupSetupPassphraseViewModel self.keyBackupSetupPassphraseViewController = keyBackupSetupPassphraseViewController diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift index 51dd86b69..4129c4f47 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift @@ -324,12 +324,8 @@ extension KeyVerificationCoordinator: KeyVerificationDataLoadingCoordinatorDeleg // MARK: - DeviceVerificationStartCoordinatorDelegate extension KeyVerificationCoordinator: DeviceVerificationStartCoordinatorDelegate { - func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) { - self.showVerifyBySAS(transaction: transaction, animated: true) - } - - func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didTransactionCancelled transaction: MXSASTransaction) { - self.didCancel() + func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, otherDidAcceptRequest request: MXKeyVerificationRequest) { + self.showVerifyByScanning(keyVerificationRequest: request, animated: true) } func deviceVerificationStartCoordinatorDidCancel(_ coordinator: DeviceVerificationStartCoordinatorType) { diff --git a/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift b/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift index b2874db8f..7db1624c9 100644 --- a/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift +++ b/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift @@ -19,7 +19,6 @@ import Foundation enum KeyVerificationDataLoadingViewModelError: Error { - case unknown case transactionCancelled case transactionCancelledByMe(reason: MXTransactionCancelCode) } @@ -137,9 +136,7 @@ final class KeyVerificationDataLoadingViewModel: KeyVerificationDataLoadingViewM return } - let finalError = error ?? KeyVerificationDataLoadingViewModelError.unknown - - sself.update(viewState: .error(finalError)) + sself.update(viewState: .error(error)) }) } else { diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift index 29c312bfc..b064d4f84 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift @@ -92,21 +92,29 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai // be sure that session has completed its first sync if session.state >= .running { - // Always send request instead of waiting for an incoming one as per recent EW changes - MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting") - - let keyVerificationService = KeyVerificationService() - self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in - guard let self = self else { - return - } + if let existingRequest = verificationManager.pendingRequests.first(where: { $0.isFromMyUser && !$0.isFromMyDevice && $0.state == MXKeyVerificationRequestStatePending }) { + MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Accepting an existing self-verification request instead of starting a new one") - self.keyVerificationRequest = keyVerificationRequest + registerTransactionDidStateChangeNotification() + acceptKeyVerificationRequest(existingRequest) + } else { - }, failure: { [weak self] error in - self?.update(viewState: .error(error)) - }) - continueLoadData() + // Always send request instead of waiting for an incoming one as per recent EW changes + MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting") + + let keyVerificationService = KeyVerificationService() + self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in + guard let self = self else { + return + } + + self.keyVerificationRequest = keyVerificationRequest + + }, failure: { [weak self] error in + self?.update(viewState: .error(error)) + }) + continueLoadData() + } } else { // show loader self.update(viewState: .secretsRecoveryCheckingAvailability(VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsCheckingAvailability)) diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift index f6ba2bec8..0c806cedd 100644 --- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift +++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift @@ -63,13 +63,9 @@ extension DeviceVerificationStartCoordinator: DeviceVerificationStartViewModelCo func deviceVerificationStartViewModelDidUseLegacyVerification(_ viewModel: DeviceVerificationStartViewModelType) { self.delegate?.deviceVerificationStartCoordinatorDidCancel(self) } - - func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) { - self.delegate?.deviceVerificationStartCoordinator(self, didCompleteWithOutgoingTransaction: transaction) - } - - func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction) { - self.delegate?.deviceVerificationStartCoordinator(self, didTransactionCancelled: transaction) + + func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest) { + self.delegate?.deviceVerificationStartCoordinator(self, otherDidAcceptRequest: request) } func deviceVerificationStartViewModelDidCancel(_ viewModel: DeviceVerificationStartViewModelType) { diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift index 16a79760c..f26862e90 100644 --- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift +++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift @@ -19,8 +19,7 @@ import Foundation protocol DeviceVerificationStartCoordinatorDelegate: AnyObject { - func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) - func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didTransactionCancelled transaction: MXSASTransaction) + func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, otherDidAcceptRequest request: MXKeyVerificationRequest) func deviceVerificationStartCoordinatorDidCancel(_ coordinator: DeviceVerificationStartCoordinatorType) } diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift index 4a8d0e66f..8a64cc881 100644 --- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift +++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift @@ -29,7 +29,7 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy private let otherUser: MXUser private let otherDevice: MXDeviceInfo - private var transaction: MXSASTransaction! + private var request: MXKeyVerificationRequest? // MARK: Public @@ -52,12 +52,12 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy case .beginVerifying: self.beginVerifying() case .verifyUsingLegacy: - self.cancelTransaction() + self.cancelRequest() self.update(viewState: .verifyUsingLegacy(self.session, self.otherDevice)) case .verifiedUsingLegacy: self.coordinatorDelegate?.deviceVerificationStartViewModelDidUseLegacyVerification(self) case .cancel: - self.cancelTransaction() + self.cancelRequest() self.coordinatorDelegate?.deviceVerificationStartViewModelDidCancel(self) } } @@ -67,30 +67,22 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy private func beginVerifying() { self.update(viewState: .loading) - self.verificationManager.beginKeyVerification(withUserId: self.otherUser.userId, andDeviceId: self.otherDevice.deviceId, method: MXKeyVerificationMethodSAS, success: { [weak self] (transaction) in - - guard let sself = self else { - return - } - guard let sasTransaction = transaction as? MXSASTransaction, !sasTransaction.isIncoming else { + self.verificationManager.requestVerificationByToDevice(withUserId: otherUser.userId, deviceIds: [otherDevice.deviceId], methods: [MXKeyVerificationMethodSAS], success: { [weak self] request in + guard let self = self else { return } - sself.transaction = sasTransaction + self.request = request - sself.update(viewState: .loaded) - sself.registerTransactionDidStateChangeNotification(transaction: sasTransaction) + self.update(viewState: .loaded) + self.registerKeyVerificationRequestDidChangeNotification(for: request) }, failure: {[weak self] error in self?.update(viewState: .error(error)) }) } - private func cancelTransaction() { - guard let transaction = self.transaction else { - return - } - - transaction.cancel(with: MXTransactionCancelCode.user()) + private func cancelRequest() { + request?.cancel(with: MXTransactionCancelCode.user(), success: nil) } private func update(viewState: DeviceVerificationStartViewState) { @@ -98,37 +90,41 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy } - // MARK: - MXKeyVerificationTransactionDidChange + // MARK: - MXKeyVerificationRequestDidChange - private func registerTransactionDidStateChangeNotification(transaction: MXSASTransaction) { - NotificationCenter.default.addObserver(self, selector: #selector(transactionDidStateChange(notification:)), name: NSNotification.Name.MXKeyVerificationTransactionDidChange, object: transaction) + private func registerKeyVerificationRequestDidChangeNotification(for request: MXKeyVerificationRequest) { + NotificationCenter.default.addObserver(self, selector: #selector(requestDidStateChange(notification:)), name: .MXKeyVerificationRequestDidChange, object: request) } - private func unregisterTransactionDidStateChangeNotification() { - NotificationCenter.default.removeObserver(self, name: .MXKeyVerificationTransactionDidChange, object: nil) + private func unregisterKeyVerificationRequestDidChangeNotification() { + NotificationCenter.default.removeObserver(self, name: .MXKeyVerificationRequestDidChange, object: nil) } - - @objc private func transactionDidStateChange(notification: Notification) { - guard let transaction = notification.object as? MXSASTransaction, !transaction.isIncoming else { + + @objc private func requestDidStateChange(notification: Notification) { + guard let request = notification.object as? MXKeyVerificationRequest, request.requestId == self.request?.requestId else { return } - switch transaction.state { - case MXSASTransactionStateShowSAS: - self.unregisterTransactionDidStateChangeNotification() - self.coordinatorDelegate?.deviceVerificationStartViewModel(self, didCompleteWithOutgoingTransaction: transaction) - case MXSASTransactionStateCancelled: - guard let reason = transaction.reasonCancelCode else { + switch request.state { + case MXKeyVerificationRequestStateAccepted, MXKeyVerificationRequestStateReady: + self.unregisterKeyVerificationRequestDidChangeNotification() + self.coordinatorDelegate?.deviceVerificationStartViewModel(self, otherDidAcceptRequest: request) + + case MXKeyVerificationRequestStateCancelled: + guard let reason = request.reasonCancelCode else { return } - self.unregisterTransactionDidStateChangeNotification() + self.unregisterKeyVerificationRequestDidChangeNotification() self.update(viewState: .cancelled(reason)) - case MXSASTransactionStateCancelledByMe: - guard let reason = transaction.reasonCancelCode else { + case MXKeyVerificationRequestStateCancelledByMe: + guard let reason = request.reasonCancelCode else { return } - self.unregisterTransactionDidStateChangeNotification() + self.unregisterKeyVerificationRequestDidChangeNotification() self.update(viewState: .cancelledByMe(reason)) + case MXKeyVerificationRequestStateExpired: + self.unregisterKeyVerificationRequestDidChangeNotification() + self.update(viewState: .error(UserVerificationStartViewModelError.keyVerificationRequestExpired)) default: break } diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift index 015e80faf..c4f04b287 100644 --- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift +++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift @@ -25,8 +25,7 @@ protocol DeviceVerificationStartViewModelViewDelegate: AnyObject { protocol DeviceVerificationStartViewModelCoordinatorDelegate: AnyObject { func deviceVerificationStartViewModelDidUseLegacyVerification(_ viewModel: DeviceVerificationStartViewModelType) - func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) - func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction) + func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest) func deviceVerificationStartViewModelDidCancel(_ viewModel: DeviceVerificationStartViewModelType) } diff --git a/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift b/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift index 104da3b32..a46b30555 100644 --- a/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift +++ b/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift @@ -18,10 +18,6 @@ import Foundation -enum UserVerificationSessionsStatusViewModelError: Error { - case unknown -} - final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsStatusViewModelType { // MARK: - Properties @@ -103,7 +99,7 @@ final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsSta } private func getDevicesFromCache(for userId: String) -> [MXDeviceInfo] { - guard let deviceInfoMap = self.session.crypto.devices(forUser: self.userId) else { + guard let deviceInfoMap = self.session.crypto?.devices(forUser: self.userId) else { return [] } return Array(deviceInfoMap.values) @@ -128,9 +124,7 @@ final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsSta completion(.success(sessionsViewData)) }, failure: { error in - - let finalError = error ?? UserVerificationSessionsStatusViewModelError.unknown - completion(.failure(finalError)) + completion(.failure(error)) }) return httpOperation diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift index 45e8e378f..604253c26 100644 --- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift @@ -189,6 +189,7 @@ extension UserVerificationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { dismissPresenter(coordinator: coordinator) + delegate?.userVerificationCoordinatorDidComplete(self) } func keyVerificationCoordinatorDidCancel(_ coordinator: KeyVerificationCoordinatorType) { diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 128e9b161..97cc2fb5c 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -952,7 +952,10 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; { // Force a reload of device keys at the next session start. // This will fix potential UISIs other peoples receive for our messages. - [mxSession.crypto resetDeviceKeys]; + if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; + } // Clean other stores [mxSession.scanManager deleteAllAntivirusScans]; @@ -1743,8 +1746,18 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; return; } + if (![mxSession.crypto.crossSigning isKindOfClass:[MXLegacyCrossSigning class]]) { + MXLogFailure(@"Device dehydratation is currently only supported by legacy cross signing, add support to all implementations"); + if (failure) + { + failure(nil); + } + return; + } + MXLegacyCrossSigning *crossSigning = (MXLegacyCrossSigning *)mxSession.crypto.crossSigning;; + MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: starting device dehydration"); - [[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crypto:mxSession.crypto dehydrationKey:keyData success:^(NSString *deviceId) { + [[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crossSigning:crossSigning dehydrationKey:keyData success:^(NSString *deviceId) { MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device successfully dehydrated"); if (success) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 6d231262c..520209860 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -65,9 +65,12 @@ _event = event; _displayFix = MXKRoomBubbleComponentDisplayFixNone; - if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML]) + + NSString *format = event.content[@"format"]; + if ([format isKindOfClass:[NSString class]] && [format isEqualToString:kMXRoomMessageFormatHTML]) { - if ([((NSString*)event.content[@"formatted_body"]) containsString:@"unknownDevices complete:^{ + + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices"); + return; + } + [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{ self->unknownDevices = nil; [self stopActivityIndicator]; diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m index 1b39e9822..d5d9f08ed 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m @@ -806,7 +806,7 @@ static BOOL _disableLongPressGestureOnEvent; mimetype = bubbleData.attachment.contentInfo[@"mimetype"]; } - if ([mimetype isEqualToString:@"image/gif"]) + if ([mimetype isKindOfClass:[NSString class]] && [mimetype isEqualToString:@"image/gif"]) { if (_isAutoAnimatedGif) { diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 95c5c4a02..368dfaf65 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -37,7 +37,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var heightConstraint: NSLayoutConstraint! private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent) - private var viewModel: ComposerViewModelProtocol = ComposerViewModel(initialViewState: ComposerViewState()) + private var viewModel: ComposerViewModelProtocol = ComposerViewModel(initialViewState: ComposerViewState(bindings: ComposerBindings(focused: false))) // MARK: Public @@ -69,8 +69,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp inputAccessoryViewForKeyboard = UIView(frame: .zero) - let composer = Composer(viewModel: viewModel.context, + let composer = Composer( + viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, + resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } self.sendWysiwygMessage(content: content) @@ -88,7 +90,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp let subView: UIView = hostingViewController.view self.addSubview(subView) - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + self.translatesAutoresizingMaskIntoConstraints = false subView.translatesAutoresizingMaskIntoConstraints = false heightConstraint = subView.heightAnchor.constraint(equalToConstant: height) NSLayoutConstraint.activate([ @@ -103,7 +105,13 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp .sink(receiveValue: { [weak self] idealHeight in guard let self = self else { return } self.updateToolbarHeight(wysiwygHeight: idealHeight) - }) + }), + // Required to update the view constraints after minimise/maximise is tapped + wysiwygViewModel.$idealHeight + .removeDuplicates() + .sink { [weak hostingViewController] _ in + hostingViewController?.view.setNeedsLayout() + } ] update(theme: ThemeService.shared().theme) @@ -115,6 +123,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self.backgroundColor = .clear } + override func dismissKeyboard() { + self.viewModel.dismissKeyboard() + } + // MARK: - Private private func updateToolbarHeight(wysiwygHeight: CGFloat) { diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h index be19d7b71..e9db3a583 100644 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h +++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h @@ -39,10 +39,15 @@ @param deviceInfo the device to share keys to. @param wasNewDevice flag indicating whether this is the first time we meet the device. @param session the related matrix session. + @param crypto the related (legacy) crypto module @param onComplete a block called when the the dialog is closed. @return the newly created instance. */ -- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo wasNewDevice:(BOOL)wasNewDevice andMatrixSession:(MXSession*)session onComplete:(void (^)(void))onComplete; +- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo + wasNewDevice:(BOOL)wasNewDevice + andMatrixSession:(MXSession*)session + crypto:(MXLegacyCrypto *)crypto + onComplete:(void (^)(void))onComplete; /** Show the dialog in a modal way. diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m index 91f62a8d6..6f638bd78 100644 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m +++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m @@ -26,16 +26,24 @@ BOOL wasNewDevice; } + +@property (nonatomic, strong) MXLegacyCrypto *crypto; + @end @implementation RoomKeyRequestViewController -- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo wasNewDevice:(BOOL)theWasNewDevice andMatrixSession:(MXSession *)session onComplete:(void (^)(void))onCompleteBlock +- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo + wasNewDevice:(BOOL)theWasNewDevice + andMatrixSession:(MXSession *)session + crypto:(MXLegacyCrypto *)crypto + onComplete:(void (^)(void))onCompleteBlock { self = [super init]; if (self) { _mxSession = session; + _crypto = crypto; _device = deviceInfo; wasNewDevice = theWasNewDevice; onComplete = onCompleteBlock; @@ -90,7 +98,7 @@ self->_alertController = nil; // Accept the received requests from this device - [self.mxSession.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ self->onComplete(); }]; @@ -108,7 +116,7 @@ self->_alertController = nil; // Ignore all pending requests from this device - [self.mxSession.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + [self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ self->onComplete(); }]; @@ -160,14 +168,14 @@ keyVerificationCoordinatorBridgePresenter = nil; // Check device new status - [self.mxSession.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { + [self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId]; if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified) { // Accept the received requests from this device // As the device is now verified, all other key requests will be automatically accepted. - [self.mxSession.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ self->onComplete(); }]; diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift index 05b691f3a..2e8e7604c 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift @@ -63,7 +63,7 @@ final class SecretsResetViewModel: SecretsResetViewModelType { } private func resetSecrets(with authParameters: [String: Any]) { - guard let crossSigning = self.session.crypto.crossSigning else { + guard let crossSigning = self.session.crypto?.crossSigning else { return } MXLog.debug("[SecretsResetViewModel] resetSecrets") diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift index 3385063ce..53a03e359 100644 --- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift +++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift @@ -149,11 +149,11 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { } private func showKeyBackupRestore() { - guard let keyBackupVersion = self.keyBackup?.keyBackupVersion else { + guard let backup = keyBackup, let keyBackupVersion = backup.keyBackupVersion else { return } - let coordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: self.navigationRouter) + let coordinator = KeyBackupRecoverCoordinator(keyBackup: backup, keyBackupVersion: keyBackupVersion, navigationRouter: self.navigationRouter) self.add(childCoordinator: coordinator) coordinator.delegate = self diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift index 911b1daa7..db91179c6 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift @@ -47,7 +47,7 @@ enum DeactivateAccountServiceError: Error { @objcMembers class DeactivateAccountService: NSObject { private let session: MXSession private let uiaService: UserInteractiveAuthenticationService - private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST") + private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST", params: [:]) /// The authentication session's ID if interactive authentication has begun, otherwise `nil`. private var sessionID: String? diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index bab229a54..806e10cff 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -673,7 +673,7 @@ enum { NSString *title = [VectorL10n deviceDetailsDeletePromptTitle]; NSString *message = [VectorL10n deviceDetailsDeletePromptMessage]; - AuthenticatedEndpointRequest *deleteDeviceRequest = [[AuthenticatedEndpointRequest alloc] initWithPath:[NSString stringWithFormat:@"%@/devices/%@", kMXAPIPrefixPathR0, [MXTools encodeURIComponent:device.deviceId]] httpMethod:@"DELETE"]; + AuthenticatedEndpointRequest *deleteDeviceRequest = [[AuthenticatedEndpointRequest alloc] initWithPath:[NSString stringWithFormat:@"%@/devices/%@", kMXAPIPrefixPathR0, [MXTools encodeURIComponent:device.deviceId]] httpMethod:@"DELETE" params:[[NSDictionary alloc] init]]; ReauthenticationCoordinatorParameters *coordinatorParameters = [[ReauthenticationCoordinatorParameters alloc] initWithSession:self.mainSession presenter:self title:title message:message authenticatedEndpointRequest:deleteDeviceRequest]; diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 2e7993c38..d3c7c6c35 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -324,7 +324,7 @@ TableViewSectionsDelegate> // Crypto sessions section - if (RiotSettings.shared.settingsSecurityScreenShowSessions) + if (RiotSettings.shared.settingsSecurityScreenShowSessions && !RiotSettings.shared.enableNewSessionManager) { Section *sessionsSection = [Section sectionWithTag:SECTION_CRYPTO_SESSIONS]; @@ -627,7 +627,7 @@ TableViewSectionsDelegate> - (void)loadCrossSigning { - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; + id crossSigning = self.mainSession.crypto.crossSigning; [crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) { if (stateUpdated) @@ -643,7 +643,7 @@ TableViewSectionsDelegate> { NSInteger numberOfRowsInCrossSigningSection; - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; + id crossSigning = self.mainSession.crypto.crossSigning; switch (crossSigning.state) { case MXCrossSigningStateNotBootstrapped: // Action: Bootstrap @@ -661,7 +661,7 @@ TableViewSectionsDelegate> - (NSAttributedString*)crossSigningInformation { - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; + id crossSigning = self.mainSession.crypto.crossSigning; NSString *crossSigningInformation; switch (crossSigning.state) @@ -708,7 +708,7 @@ TableViewSectionsDelegate> buttonCell.mxkButton.accessibilityIdentifier = nil; // And customise it - MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; + id crossSigning = self.mainSession.crypto.crossSigning; switch (crossSigning.state) { case MXCrossSigningStateNotBootstrapped: // Action: Bootstrap diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 915e99e12..54bb95d34 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -1445,13 +1445,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> NSString *sdkVersionInfo = [NSString stringWithFormat:@"Matrix SDK %@", MatrixSDKVersion]; - NSString *olmVersionInfo = [NSString stringWithFormat:@"OLM %@", [OLMKit versionString]]; - [footerText appendFormat:@"%@\n", loggedUserInfo]; [footerText appendFormat:@"%@\n", homeserverInfo]; [footerText appendFormat:@"%@\n", appVersionInfo]; [footerText appendFormat:@"%@\n", sdkVersionInfo]; - [footerText appendFormat:@"%@", olmVersionInfo]; + [footerText appendFormat:@"%@", self.mainSession.crypto.version]; return [footerText copy]; } diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 6e2145c4c..3b5b8c9a8 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -274,7 +274,12 @@ { // Acknowledge the existence of all devices before leaving this screen [self startActivityIndicator]; - [mxSession.crypto setDevicesKnown:usersDevices complete:^{ + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices"); + return; + } + [(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{ [self stopActivityIndicator]; [self dismissViewControllerAnimated:YES completion:nil]; diff --git a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift index f8827427a..a6e107808 100644 --- a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift +++ b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift @@ -22,10 +22,11 @@ class AuthenticatedEndpointRequest: NSObject { let path: String let httpMethod: String - - init(path: String, httpMethod: String) { + let params: [String: Any] + init(path: String, httpMethod: String, params: [String: Any]) { self.path = path self.httpMethod = httpMethod + self.params = params super.init() } } @@ -37,6 +38,15 @@ extension AuthenticatedEndpointRequest { /// - Parameter deviceID: The device ID that is to be deleted. static func deleteDevice(_ deviceID: String) -> AuthenticatedEndpointRequest { let path = String(format: "%@/devices/%@", kMXAPIPrefixPathR0, MXTools.encodeURIComponent(deviceID)) - return AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE") + return AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE", params: [:]) + } +} + +extension AuthenticatedEndpointRequest { + /// Create an authenticated request on `_matrix/client/r0/delete_devices`. + /// - Parameter deviceIDs: IDs for devices that is to be deleted. + static func deleteDevices(_ deviceIDs: [String]) -> AuthenticatedEndpointRequest { + let path = String(format: "%@/delete_devices", kMXAPIPrefixPathR0) + return AuthenticatedEndpointRequest(path: path, httpMethod: "POST", params: ["devices": deviceIDs]) } } diff --git a/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift b/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift index 56319d3e6..7d7c709c5 100644 --- a/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift +++ b/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift @@ -131,7 +131,7 @@ final class UserInteractiveAuthenticationService: NSObject { success: @escaping (MXAuthenticationSession?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation { // Get the authentication flow required for this API - return self.session.matrixRestClient.authSessionForRequest(withMethod: request.httpMethod, path: request.path, parameters: [:], success: { [weak self] (authenticationSession) in + return self.session.matrixRestClient.authSessionForRequest(withMethod: request.httpMethod, path: request.path, parameters: request.params, success: { [weak self] (authenticationSession) in guard let self = self else { return } diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index 2233b352d..22d0063be 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -102,7 +102,10 @@ static MXSession *fakeSession; [session setStore:self.fileStore success:^{ MXStrongifyAndReturnIfNil(session); - session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now + if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now + } self.selectedRooms = [NSMutableArray array]; for (NSString *roomIdentifier in roomIdentifiers) { diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 0dc3f78d5..30059334e 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -106,7 +106,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { } func stopScanning(destroy: Bool) { - zxCapture.delegate = nil + if (zxCapture.delegate != nil) { + // Setting the zxCapture to nil without checking makes it start + // scanning and implicitly requesting camera access + zxCapture.delegate = nil + } guard zxCapture.running else { return @@ -292,7 +296,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { MXLog.debug("[QRLoginService] Received cross-signing details \(responsePayload)") if let masterKeyFromVerifyingDevice = responsePayload.masterKey, - let localMasterKey = session.crypto.crossSigningKeys(forUser: session.myUserId).masterKeys?.keys { + let localMasterKey = session.crypto.crossSigning.crossSigningKeys(forUser: session.myUserId)?.masterKeys?.keys { guard masterKeyFromVerifyingDevice == localMasterKey else { MXLog.error("[QRLoginService] Received invalid master key from verifying device") await teardownRendezvous(state: .failed(error: .rendezvousFailed)) @@ -348,6 +352,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { await teardownRendezvous() } + @MainActor private func teardownRendezvous(state: QRLoginServiceState? = nil) async { // Stop listening for changes, try deleting the resource _ = await rendezvousService?.tearDown() diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index 91cf8937f..ee618b8f8 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -36,6 +36,7 @@ struct ScreenList: View { VStack { TextField("Search", text: $searchQuery) .textFieldStyle(.roundedBorder) + .autocorrectionDisabled() .padding(.horizontal) .accessibilityIdentifier("searchQueryTextField") .onChange(of: searchQuery, perform: search) diff --git a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift index a2e7dc2b5..f0912c5bc 100644 --- a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift +++ b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift @@ -20,16 +20,33 @@ import XCTest extension XCUIApplication { func goToScreenWithIdentifier(_ identifier: String) { // Search for the screen identifier - textFields["searchQueryTextField"].tap() - typeText(identifier) - + let textField = textFields["searchQueryTextField"] let button = buttons[identifier] - let footer = staticTexts["footerText"] - while !button.isHittable, !footer.isHittable { - tables.firstMatch.swipeUp() + // Sometimes the search gets stuck without showing any results. Try to nudge it along + for _ in 0...10 { + textField.clearAndTypeText(identifier) + if button.exists { + break + } } button.tap() } } + +private extension XCUIElement { + func clearAndTypeText(_ text: String) { + guard let stringValue = value as? String else { + XCTFail("Tried to clear and type text into a non string value") + return + } + + tap() + + let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) + + typeText(deleteString) + typeText(text) + } +} diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index 48d7df054..724ae1ac7 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -29,11 +29,12 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel + let bindings = ComposerBindings(focused: false) switch self { - case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState()) - case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit)) - case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply)) + case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(bindings: bindings)) + case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, bindings: bindings)) + case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, bindings: bindings)) } let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxHeight: 360) @@ -54,7 +55,11 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { [viewModel, wysiwygviewModel], AnyView(VStack { Spacer() - Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, sendMessageAction: { _ in }, showSendMediaActions: { }) + Composer(viewModel: viewModel.context, + wysiwygViewModel: wysiwygviewModel, + resizeAnimationDuration: 0.1, + sendMessageAction: { _ in }, + showSendMediaActions: { }) }.frame( minWidth: 0, maxWidth: .infinity, diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift index 0f8ad1fdc..2e86750b5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift @@ -20,6 +20,8 @@ struct ComposerViewState: BindableState { var eventSenderDisplayName: String? var sendMode: ComposerSendMode = .send var placeholder: String? + + var bindings: ComposerBindings } extension ComposerViewState { @@ -45,3 +47,7 @@ extension ComposerViewState { } } } + +struct ComposerBindings { + var focused: Bool +} diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift index 08ce71fc0..aae6e1682 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift @@ -30,6 +30,55 @@ final class ComposerUITests: MockScreenTestCase { wysiwygTextView.typeText("test") XCTAssertTrue(sendButton.exists) XCTAssertFalse(app.buttons["editButton"].exists) + + let maximiseButton = app.buttons["maximiseButton"] + let minimiseButton = app.buttons["minimiseButton"] + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) + + maximiseButton.tap() + XCTAssertTrue(minimiseButton.exists) + XCTAssertFalse(maximiseButton.exists) + + minimiseButton.tap() + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) + } + + // This test requires "connect hardware keyboard" to be off on the simulator + // And may not work on the CI + func testFastTyping() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + let text = "fast typing test" + let wysiwygTextView = app.textViews.allElementsBoundByIndex[0] + XCTAssertTrue(wysiwygTextView.exists) + wysiwygTextView.tap() + sleep(1) + wysiwygTextView.typeText(text) + let options = XCTExpectedFailure.Options() + options.isStrict = false + XCTExpectFailure("Test may fail on CI", options: options) + let value = wysiwygTextView.value as? String + XCTAssert(value == text, "Text view value is: \(value ?? "nil")") + } + + // This test requires "connect hardware keyboard" to be off on the simulator + // And may not work on the CI + func testLongPressDelete() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + let text = "test1 test2 test3 test4 test5 test6 test7" + let wysiwygTextView = app.textViews.allElementsBoundByIndex[0] + XCTAssertTrue(wysiwygTextView.exists) + wysiwygTextView.tap() + sleep(1) + wysiwygTextView.typeText(text) + sleep(1) + app.keys["delete"].press(forDuration: 10.0) + let options = XCTExpectedFailure.Options() + options.isStrict = false + XCTExpectFailure("Test may fail on CI", options: options) + let value = wysiwygTextView.value as? String + XCTAssert(value == "", "Text view value is: \(value ?? "nil")") } func testReplyMode() throws { @@ -56,6 +105,19 @@ final class ComposerUITests: MockScreenTestCase { let textViewContent = wysiwygTextView.value as! String XCTAssertFalse(textViewContent.isEmpty) XCTAssertFalse(cancelButton.exists) + + let maximiseButton = app.buttons["maximiseButton"] + let minimiseButton = app.buttons["minimiseButton"] + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) + + maximiseButton.tap() + XCTAssertTrue(minimiseButton.exists) + XCTAssertFalse(maximiseButton.exists) + + minimiseButton.tap() + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) } func testEditMode() throws { @@ -82,5 +144,18 @@ final class ComposerUITests: MockScreenTestCase { let textViewContent = wysiwygTextView.value as! String XCTAssertTrue(textViewContent.isEmpty) XCTAssertFalse(cancelButton.exists) + + let maximiseButton = app.buttons["maximiseButton"] + let minimiseButton = app.buttons["minimiseButton"] + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) + + maximiseButton.tap() + XCTAssertTrue(minimiseButton.exists) + XCTAssertFalse(maximiseButton.exists) + + minimiseButton.tap() + XCTAssertFalse(minimiseButton.exists) + XCTAssertTrue(maximiseButton.exists) } } diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift index 5f16cfa42..cef424a29 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift @@ -23,7 +23,7 @@ final class ComposerViewModelTests: XCTestCase { var context: ComposerViewModel.Context! override func setUpWithError() throws { - viewModel = ComposerViewModel(initialViewState: ComposerViewState()) + viewModel = ComposerViewModel(initialViewState: ComposerViewState(bindings: ComposerBindings(focused: false))) context = viewModel.context } @@ -69,4 +69,10 @@ final class ComposerViewModelTests: XCTestCase { viewModel.placeholder = "Placeholder Test" XCTAssert(context.viewState.placeholder == "Placeholder Test") } + + func testDimissKeyboard() { + viewModel.state.bindings.focused = true + viewModel.dismissKeyboard() + XCTAssert(context.viewState.bindings.focused == false) + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index be7577e12..cab370558 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -25,7 +25,6 @@ struct Composer: View { @Environment(\.theme) private var theme: ThemeSwiftUI - @State private var focused = false @State private var isActionButtonShowing = false private let horizontalPadding: CGFloat = 12 @@ -51,8 +50,16 @@ struct Composer: View { viewModel.viewState.sendMode == .edit ? "editButton" : "sendButton" } + private var toggleButtonAcccessibilityIdentifier: String { + wysiwygViewModel.maximised ? "minimiseButton" : "maximiseButton" + } + + private var toggleButtonImageName: String { + wysiwygViewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name + } + private var borderColor: Color { - focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent + viewModel.focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent } private var formatItems: [FormatItem] { @@ -69,6 +76,7 @@ struct Composer: View { @ObservedObject var viewModel: ComposerViewModelType.Context @ObservedObject var wysiwygViewModel: WysiwygComposerViewModel + let resizeAnimationDuration: Double let sendMessageAction: (WysiwygComposerContent) -> Void let showSendMediaActions: () -> Void @@ -76,8 +84,6 @@ struct Composer: View { var body: some View { VStack(spacing: 8) { let rect = RoundedRectangle(cornerRadius: cornerRadius) - // TODO: Fix maximise animation bugs before re-enabling - // ZStack(alignment: .topTrailing) { VStack(spacing: 12) { if viewModel.viewState.shouldDisplayContext { HStack { @@ -103,41 +109,41 @@ struct Composer: View { .padding(.top, 8) .padding(.horizontal, horizontalPadding) } - WysiwygComposerView( - focused: $focused, - content: wysiwygViewModel.content, - replaceText: wysiwygViewModel.replaceText, - select: wysiwygViewModel.select, - didUpdateText: wysiwygViewModel.didUpdateText - ) - .tintColor(theme.colors.accent) - .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) - .frame(height: wysiwygViewModel.idealHeight) - .padding(.horizontal, horizontalPadding) - .onAppear { - wysiwygViewModel.setup() + HStack(alignment: .top, spacing: 0) { + WysiwygComposerView( + focused: $viewModel.focused, + viewModel: wysiwygViewModel + ) + .tintColor(theme.colors.accent) + .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) + .frame(height: wysiwygViewModel.idealHeight) + .onAppear { + wysiwygViewModel.setup() + } + Button { + wysiwygViewModel.maximised.toggle() + } label: { + Image(toggleButtonImageName) + .resizable() + .foregroundColor(theme.colors.tertiaryContent) + .frame(width: 16, height: 16) + } + .accessibilityIdentifier(toggleButtonAcccessibilityIdentifier) + .padding(.leading, 12) + .padding(.trailing, 4) } - // Button { - // withAnimation(.easeInOut(duration: 0.25)) { - // viewModel.maximised.toggle() - // } - // } label: { - // Image(viewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name) - // .foregroundColor(theme.colors.tertiaryContent) - // } - // .padding(.top, 4) - // .padding(.trailing, 12) - // } + .padding(.horizontal, horizontalPadding) .padding(.top, topPadding) .padding(.bottom, verticalPadding) } .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: 1)) + .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) .padding(.horizontal, horizontalPadding) .padding(.top, 8) .onTapGesture { - if !focused { - focused = true + if viewModel.focused { + viewModel.focused = true } } HStack(spacing: 0) { @@ -148,7 +154,6 @@ struct Composer: View { .resizable() .foregroundColor(theme.colors.tertiaryContent) .frame(width: 14, height: 14) - } .frame(width: 36, height: 36) .background(Circle().fill(theme.colors.system)) diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift index 1e44ed049..5d57e34b3 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift @@ -64,4 +64,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol callback?(.contentDidChange(isEmpty: isEmpty)) } } + + func dismissKeyboard() { + state.bindings.focused = false + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift index 70d943dc7..41005324b 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift @@ -22,4 +22,6 @@ protocol ComposerViewModelProtocol { var sendMode: ComposerSendMode { get set } var eventSenderDisplayName: String? { get set } var placeholder: String? { get set } + + func dismissKeyboard() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift index 5e7eaceef..9b363d367 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift @@ -24,36 +24,45 @@ class TimelinePollUITests: MockScreenTestCase { XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["20 votes cast"].exists) - XCTAssert(app.buttons["First, 10 votes"].exists) - XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%") + + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%") + + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%") - XCTAssert(app.buttons["Second, 5 votes"].exists) - XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%") + app.buttons["PollAnswerOption0"].tap() - XCTAssert(app.buttons["Third, 15 votes"].exists) - XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "11 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "55%") - app.buttons["First, 10 votes"].tap() + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "4 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "20%") - XCTAssert(app.buttons["First, 11 votes"].exists) - XCTAssertEqual(app.buttons["First, 11 votes"].value as! String, "55%") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%") - XCTAssert(app.buttons["Second, 4 votes"].exists) - XCTAssertEqual(app.buttons["Second, 4 votes"].value as! String, "20%") + app.buttons["PollAnswerOption2"].tap() - XCTAssert(app.buttons["Third, 15 votes"].exists) - XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%") - app.buttons["Third, 15 votes"].tap() + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "4 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "20%") - XCTAssert(app.buttons["First, 10 votes"].exists) - XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%") - - XCTAssert(app.buttons["Second, 4 votes"].exists) - XCTAssertEqual(app.buttons["Second, 4 votes"].value as! String, "20%") - - XCTAssert(app.buttons["Third, 16 votes"].exists) - XCTAssertEqual(app.buttons["Third, 16 votes"].value as! String, "80%") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "16 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "80%") } func testOpenUndisclosedPoll() { @@ -62,29 +71,29 @@ class TimelinePollUITests: MockScreenTestCase { XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["20 votes cast"].exists) - XCTAssert(!app.buttons["First, 10 votes"].exists) - XCTAssert(app.buttons["First"].exists) - XCTAssertTrue((app.buttons["First"].value as! String).isEmpty) - - XCTAssert(!app.buttons["Second, 5 votes"].exists) - XCTAssert(app.buttons["Second"].exists) - XCTAssertTrue((app.buttons["Second"].value as! String).isEmpty) - - XCTAssert(!app.buttons["Third, 15 votes"].exists) - XCTAssert(app.buttons["Third"].exists) - XCTAssertTrue((app.buttons["Third"].value as! String).isEmpty) - - app.buttons["First"].tap() - - XCTAssert(app.buttons["First"].exists) - XCTAssert(app.buttons["Second"].exists) - XCTAssert(app.buttons["Third"].exists) + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssert(!app.staticTexts["PollAnswerOption0Count"].exists) + XCTAssert(!app.progressIndicators["PollAnswerOption0Progress"].exists) - app.buttons["Third"].tap() + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssert(!app.staticTexts["PollAnswerOption1Count"].exists) + XCTAssert(!app.progressIndicators["PollAnswerOption1Progress"].exists) - XCTAssert(app.buttons["First"].exists) - XCTAssert(app.buttons["Second"].exists) - XCTAssert(app.buttons["Third"].exists) + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssert(!app.staticTexts["PollAnswerOption2Count"].exists) + XCTAssert(!app.progressIndicators["PollAnswerOption2Progress"].exists) + + app.buttons["PollAnswerOption0"].tap() + + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + + app.buttons["PollAnswerOption2"].tap() + + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") } func testClosedDisclosedPoll() { @@ -100,25 +109,31 @@ class TimelinePollUITests: MockScreenTestCase { private func checkClosedPoll() { XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["Final results based on 20 votes"].exists) + + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%") - XCTAssert(app.buttons["First, 10 votes"].exists) - XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%") - XCTAssert(app.buttons["Second, 5 votes"].exists) - XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%") - XCTAssert(app.buttons["Third, 15 votes"].exists) - XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%") + app.buttons["PollAnswerOption0"].tap() - app.buttons["First, 10 votes"].tap() + XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First") + XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%") - XCTAssert(app.buttons["First, 10 votes"].exists) - XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second") + XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%") - XCTAssert(app.buttons["Second, 5 votes"].exists) - XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%") - - XCTAssert(app.buttons["Third, 15 votes"].exists) - XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third") + XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes") + XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%") } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index aaaba7c37..2ffa68be9 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -41,6 +41,7 @@ struct TimelinePollAnswerOptionButton: View { .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) .accentColor(progressViewAccentColor) } + .accessibilityIdentifier("PollAnswerOption\(optionIndex)") } var answerOptionLabel: some View { @@ -53,6 +54,7 @@ struct TimelinePollAnswerOptionButton: View { Text(answerOption.text) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label") if poll.closed, answerOption.winner { Spacer() @@ -66,11 +68,13 @@ struct TimelinePollAnswerOptionButton: View { total: Double(poll.totalAnswerCount)) .progressViewStyle(LinearProgressViewStyle()) .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") if poll.shouldDiscloseResults { Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) .font(theme.fonts.footnote) .foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Count") } } } @@ -92,6 +96,10 @@ struct TimelinePollAnswerOptionButton: View { return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent } + + var optionIndex: Int { + poll.answerOptions.firstIndex { $0.id == answerOption.id } ?? Int.max + } } struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index 23b204083..f44744a9c 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -21,10 +21,7 @@ class UserSuggestionUITests: MockScreenTestCase { func testUserSuggestionScreen() throws { app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title) - XCTAssert(app.tables.firstMatch.waitForExistence(timeout: 1)) - - let firstButton = app.tables.firstMatch.buttons.firstMatch - _ = firstButton.waitForExistence(timeout: 10) - XCTAssert(firstButton.identifier == "displayNameText-userIdText") + let firstButton = app.buttons["displayNameText-userIdText"].firstMatch + XCTAssert(firstButton.waitForExistence(timeout: 10)) } } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift new file mode 100644 index 000000000..ccbfceab9 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift @@ -0,0 +1,30 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DestructiveButton