From d76d450365571109dfedf1584909df0df3a5fab7 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 3 Jan 2023 16:52:25 +0000 Subject: [PATCH 001/468] Fix both issues as outlined in 7222 --- .../LocationSharing/LocationManager.swift | 21 +++++++++++++++---- changelog.d/7222.bugfix | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7222.bugfix diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index 5fb222493..3b41e126a 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -43,6 +43,7 @@ class LocationManager: NSObject { private let locationManager: CLLocationManager private var authorizationHandler: LocationAuthorizationHandler? + private var authorizationReturnedSinceRequestingAlways = false // MARK: Public @@ -144,14 +145,16 @@ class LocationManager: NSObject { // See https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?changes=_6_6 private func tryToRequestAlwaysAuthorization(handler: @escaping LocationAuthorizationHandler) { self.authorizationHandler = handler + self.authorizationReturnedSinceRequestingAlways = false + self.locationManager.delegate = self self.locationManager.requestAlwaysAuthorization() Timer.scheduledTimer(withTimeInterval: Constants.waitForAuthorizationStatusDelay, repeats: false) { [weak self] _ in - guard let self = self else { + guard let self = self, !self.authorizationReturnedSinceRequestingAlways else { return } - self.authorizationRequestDidComplete(with: self.locationManager.authorizationStatus) + self.authorizationAlwaysRequestDidComplete(with: self.locationManager.authorizationStatus) } } @@ -175,7 +178,10 @@ class LocationManager: NSObject { return status } - private func authorizationRequestDidComplete(with status: CLAuthorizationStatus) { + private func checkShouldCompleteAuthorizationCheck(with status: CLAuthorizationStatus) { + } + + private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { return } @@ -191,7 +197,14 @@ extension LocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = self.locationManager.authorizationStatus - self.authorizationRequestDidComplete(with: status) + authorizationReturnedSinceRequestingAlways = true + if status == .authorizedAlways { + // LocationManager can call locationManagerDidChangeAuthorization multiple times. + // For example it calls it at initialisation of LocationManager manager and we are also seeing it called + // after requestAlwaysAuthorization but before the user has actually selected on option on the prompt. + // Therefore we should only call `authorizationAlwaysRequestDidComplete` once on the success of authorizedAlways being granted. + self.authorizationAlwaysRequestDidComplete(with: status) + } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { diff --git a/changelog.d/7222.bugfix b/changelog.d/7222.bugfix new file mode 100644 index 000000000..e1402cb0d --- /dev/null +++ b/changelog.d/7222.bugfix @@ -0,0 +1 @@ +Live Location Sharing does not work on first selection after granting "Allow always" location permission. From caa6755d2b3d53cbf14b27abefbecf78784306ff Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 3 Jan 2023 16:54:03 +0000 Subject: [PATCH 002/468] remove unnecessary function --- Podfile | 4 +- Podfile.lock | 33 ++++++++++------- .../xcshareddata/xcschemes/Riot.xcscheme | 37 ++++++++----------- .../LocationSharing/LocationManager.swift | 3 -- 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/Podfile b/Podfile index 14443360b..04ffe7c78 100644 --- a/Podfile +++ b/Podfile @@ -16,9 +16,9 @@ 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.6' +# $matrixSDKVersion = '= 0.24.6' # $matrixSDKVersion = :local -# $matrixSDKVersion = { :branch => 'develop'} +$matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } ######################################## diff --git a/Podfile.lock b/Podfile.lock index 5244de7b2..6ef48596d 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.5): - - MatrixSDK/Core (= 0.24.5) - - MatrixSDK/Core (0.24.5): + - MatrixSDK (0.24.6): + - MatrixSDK/Core (= 0.24.6) + - MatrixSDK/Core (0.24.6): - 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.5): - - MatrixSDKCrypto (= 0.1.5) - - MatrixSDK/JingleCallStack (0.24.5): + - MatrixSDK/CryptoSDK (0.24.6): + - MatrixSDKCrypto (= 0.1.7) + - MatrixSDK/JingleCallStack (0.24.6): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.5) + - MatrixSDKCrypto (0.1.7) - 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.5) - - MatrixSDK/JingleCallStack (= 0.24.5) + - MatrixSDK (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `develop`) + - MatrixSDK/JingleCallStack (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `develop`) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -165,7 +165,6 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatrixSDK - MatrixSDKCrypto - OLMKit - PostHog @@ -190,11 +189,17 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git + MatrixSDK: + :branch: develop + :git: https://github.com/matrix-org/matrix-ios-sdk.git CHECKOUT OPTIONS: AnalyticsEvents: :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f :git: https://github.com/matrix-org/matrix-analytics-events.git + MatrixSDK: + :commit: 8ae45d250cf0714b37aa6abdb8bffa8c2a438606 + :git: https://github.com/matrix-org/matrix-ios-sdk.git SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce @@ -220,8 +225,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 1557b3ed0a211db43a865cfdad93f07c2be92c9e - MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4 + MatrixSDK: 3c245333328e6ed91f7b660f4b6159e203d615d3 + MatrixSDKCrypto: 2bd9ca41b2c644839f4e680a64897d56b3f95392 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -241,6 +246,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: c93b326deaf9de3916d42a49d39d737612ab1d94 +PODFILE CHECKSUM: 3a48e44323e1b4ff13a7ed214461c825c588dc53 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 012a5a109..e1775adc4 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -1,11 +1,10 @@ + version = "1.3"> + buildImplicitDependencies = "YES"> @@ -35,11 +34,20 @@ + + + + @@ -52,17 +60,6 @@ - - - - - - - - - - diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index 3b41e126a..ee9ddb103 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -177,9 +177,6 @@ class LocationManager: NSObject { return status } - - private func checkShouldCompleteAuthorizationCheck(with status: CLAuthorizationStatus) { - } private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { From 1639fccdd1f4eb5da4b2191b44c797674b07a082 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 3 Jan 2023 16:54:41 +0000 Subject: [PATCH 003/468] Revert "remove unnecessary function" This reverts commit caa6755d2b3d53cbf14b27abefbecf78784306ff. --- Podfile | 4 +- Podfile.lock | 33 +++++++---------- .../xcshareddata/xcschemes/Riot.xcscheme | 37 +++++++++++-------- .../LocationSharing/LocationManager.swift | 3 ++ 4 files changed, 41 insertions(+), 36 deletions(-) diff --git a/Podfile b/Podfile index 04ffe7c78..14443360b 100644 --- a/Podfile +++ b/Podfile @@ -16,9 +16,9 @@ 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.6' +$matrixSDKVersion = '= 0.24.6' # $matrixSDKVersion = :local -$matrixSDKVersion = { :branch => 'develop'} +# $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } ######################################## diff --git a/Podfile.lock b/Podfile.lock index 6ef48596d..5244de7b2 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.6): - - MatrixSDK/Core (= 0.24.6) - - MatrixSDK/Core (0.24.6): + - MatrixSDK (0.24.5): + - MatrixSDK/Core (= 0.24.5) + - MatrixSDK/Core (0.24.5): - 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.6): - - MatrixSDKCrypto (= 0.1.7) - - MatrixSDK/JingleCallStack (0.24.6): + - MatrixSDK/CryptoSDK (0.24.5): + - MatrixSDKCrypto (= 0.1.5) + - MatrixSDK/JingleCallStack (0.24.5): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.7) + - 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 (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `develop`) - - MatrixSDK/JingleCallStack (from `https://github.com/matrix-org/matrix-ios-sdk.git`, branch `develop`) + - MatrixSDK (= 0.24.5) + - MatrixSDK/JingleCallStack (= 0.24.5) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -165,6 +165,7 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging + - MatrixSDK - MatrixSDKCrypto - OLMKit - PostHog @@ -189,17 +190,11 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :branch: develop - :git: https://github.com/matrix-org/matrix-ios-sdk.git CHECKOUT OPTIONS: AnalyticsEvents: :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :commit: 8ae45d250cf0714b37aa6abdb8bffa8c2a438606 - :git: https://github.com/matrix-org/matrix-ios-sdk.git SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce @@ -225,8 +220,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 3c245333328e6ed91f7b660f4b6159e203d615d3 - MatrixSDKCrypto: 2bd9ca41b2c644839f4e680a64897d56b3f95392 + MatrixSDK: 1557b3ed0a211db43a865cfdad93f07c2be92c9e + MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -246,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 3a48e44323e1b4ff13a7ed214461c825c588dc53 +PODFILE CHECKSUM: c93b326deaf9de3916d42a49d39d737612ab1d94 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index e1775adc4..012a5a109 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -1,10 +1,11 @@ + version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> @@ -34,20 +35,11 @@ - - - - @@ -60,6 +52,17 @@ + + + + + + + + + + diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index ee9ddb103..3b41e126a 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -177,6 +177,9 @@ class LocationManager: NSObject { return status } + + private func checkShouldCompleteAuthorizationCheck(with status: CLAuthorizationStatus) { + } private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { From d08a9670548ac1c35471ce1795319369d237db54 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 3 Jan 2023 16:55:28 +0000 Subject: [PATCH 004/468] remove unnecessary function --- Riot/Modules/LocationSharing/LocationManager.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index 3b41e126a..ee9ddb103 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -177,9 +177,6 @@ class LocationManager: NSObject { return status } - - private func checkShouldCompleteAuthorizationCheck(with status: CLAuthorizationStatus) { - } private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { From 4a6f346f1ed274814e6875614c5c6ba811db7f78 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 3 Jan 2023 17:54:02 +0000 Subject: [PATCH 005/468] Update Riot/Modules/LocationSharing/LocationManager.swift Co-authored-by: Alfonso Grillo --- Riot/Modules/LocationSharing/LocationManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index ee9ddb103..857a59597 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -177,7 +177,6 @@ class LocationManager: NSObject { return status } - private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) { guard let authorizationHandler = self.authorizationHandler else { return From e2a635f96114d89825eedd7686defc38f7012b4d Mon Sep 17 00:00:00 2001 From: Phl-Pro Date: Tue, 10 Jan 2023 15:24:46 +0100 Subject: [PATCH 006/468] Handle VoIP buttons when VB is used (#7225) --- Riot/Assets/en.lproj/Vector.strings | 38 ++++++++++--------- Riot/Generated/Strings.swift | 8 ++++ Riot/Modules/Room/RoomViewController.m | 19 +++++++++- .../VoiceBroadcastRecorderCoordinator.swift | 4 ++ .../VoiceBroadcastRecorderProvider.swift | 8 ++++ .../VoiceBroadcastRecorderService.swift | 3 ++ ...oiceBroadcastRecorderServiceProtocol.swift | 3 ++ changelog.d/pr-7225.change | 1 + 8 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 changelog.d/pr-7225.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 04d0cd693..c1b97f791 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1522,7 +1522,7 @@ Tap the + to start adding people."; "device_verification_cancelled_by_me" = "The verification has been cancelled. Reason: %@"; "device_verification_error_cannot_load_device" = "Cannot load session information."; -// Mark: Incoming +// MARK: Incoming "device_verification_incoming_title" = "Incoming Verification Request"; "device_verification_incoming_description_1" = "Verify this session to mark it as trusted. Trusting sessions of partners gives you extra peace of mind when using end-to-end encrypted messages."; "device_verification_incoming_description_2" = "Verifying this session will mark it as trusted, and also mark your session as trusted to the partner."; @@ -2011,12 +2011,12 @@ Tap the + to start adding people."; "share_invite_link_room_text" = "Hey, join this room on %@"; "share_invite_link_space_text" = "Hey, join this space on %@"; -// Mark: - Room avatar view +// MARK: - Room avatar view "room_avatar_view_accessibility_label" = "avatar"; "room_avatar_view_accessibility_hint" = "Change room avatar"; -// Mark: - Room creation introduction cell +// MARK: - Room creation introduction cell "room_intro_cell_add_participants_action" = "Add people"; @@ -2033,7 +2033,7 @@ Tap the + to start adding people."; "room_intro_cell_information_dm_sentence2" = "Only the two of you are in this conversation, no one else can join."; "room_intro_cell_information_multiple_dm_sentence2" = "Only you are in this conversation, unless any of you invites someone to join."; -// Mark: - Room invite +// MARK: - Room invite "room_invite_to_space_option_title" = "To %@"; "room_invite_to_space_option_detail" = "They can explore %@, but won’t be a member of %@."; @@ -2042,7 +2042,7 @@ Tap the + to start adding people."; "room_invite_not_enough_permission" = "You do not have permission to invite people to this room"; "space_invite_not_enough_permission" = "You do not have permission to invite people to this space"; -// Mark: - Spaces +// MARK: - Spaces "space_feature_unavailable_title" = "Spaces aren’t here yet"; "space_feature_unavailable_subtitle" = "Spaces aren't on iOS yet, but you can use them now on Web and Desktop"; @@ -2099,7 +2099,7 @@ Tap the + to start adding people."; "spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer."; -// Mark: - Space Creation +// MARK: - Space Creation "spaces_creation_hint" = "Spaces are a new way to group rooms and people."; "spaces_creation_visibility_title" = "What type of space do you want to create?"; @@ -2158,7 +2158,7 @@ Tap the + to start adding people."; "spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; -// Mark: Leave space +// MARK: Leave space "leave_space_action" = "Leave space"; "leave_space_and_one_room" = "Leave space and 1 room"; @@ -2171,17 +2171,17 @@ Tap the + to start adding people."; "room_event_action_reaction_more" = "%@ more"; -// Mark: Avatar +// MARK: Avatar "space_avatar_view_accessibility_label" = "avatar"; "space_avatar_view_accessibility_hint" = "Change space avatar"; -// Mark: - User avatar view +// MARK: - User avatar view "user_avatar_view_accessibility_label" = "avatar"; "user_avatar_view_accessibility_hint" = "Change user avatar"; -// Mark: - Side menu +// MARK: - Side menu "side_menu_reveal_action_accessibility_label" = "Left panel"; "side_menu_action_invite_friends" = "Invite friends"; @@ -2191,7 +2191,7 @@ Tap the + to start adding people."; "side_menu_app_version" = "Version %@"; "side_menu_coach_message" = "Swipe right or tap to see all rooms"; -// Mark: - Voice Messages +// MARK: - Voice Messages "voice_message_release_to_send" = "Hold to record, release to send"; "voice_message_remaining_recording_time" = "%@s left"; @@ -2200,7 +2200,7 @@ Tap the + to start adding people."; "voice_message_broadcast_in_progress_title" = "Can't start voice message"; "voice_message_broadcast_in_progress_message" = "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message"; -// Mark: - Voice broadcast +// MARK: - Voice Broadcast "voice_broadcast_unauthorized_title" = "Can't start a new voice broadcast"; "voice_broadcast_permission_denied_message" = "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions."; "voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one."; @@ -2213,8 +2213,10 @@ Tap the + to start adding people."; "voice_broadcast_stop_alert_title" = "Stop live broadcasting?"; "voice_broadcast_stop_alert_description" = "Are you sure you want to stop your live broadcast? This will end the broadcast, and the full recording will be available in the room."; "voice_broadcast_stop_alert_agree_button" = "Yes, stop"; +"voice_broadcast_voip_cannot_start_title" = "Can’t start a call"; +"voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call."; -// Mark: - Version check +// MARK: - Version check "version_check_banner_title_supported" = "We’re ending support for iOS %@"; "version_check_banner_subtitle_supported" = "We will soon be ending support for %@ on iOS %@. To continue using %@ to its full potential, we advise you to upgrade your version of iOS."; @@ -2230,7 +2232,7 @@ Tap the + to start adding people."; "version_check_modal_subtitle_deprecated" = "We've been working on enhancing %@ for a faster and more polished experience. Unfortunately your current version of iOS is not compatible with some of those fixes and is no longer supported.\nWe're advising you to upgrade your operating system to use %@ to its full potential."; "version_check_modal_action_title_deprecated" = "Find out how"; -// Mark: - All Chats +// MARK: - All Chats "all_chats_title" = "All chats"; "all_chats_section_title" = "Chats"; @@ -2274,12 +2276,12 @@ Tap the + to start adding people."; "all_chats_onboarding_title" = "What's new"; "all_chats_onboarding_try_it" = "Try it out"; -// Mark: - Room invites +// MARK: - Room invites "room_invites_empty_view_title" = "Nothing new."; "room_invites_empty_view_information" = "This is where your invites appear."; -// Mark: - Space Selector +// MARK: - Space Selector "space_selector_title" = "My spaces"; "space_selector_empty_view_title" = "No spaces yet."; @@ -2289,7 +2291,7 @@ Tap the + to start adding people."; "space_detail_nav_title" = "Space detail"; "space_invite_nav_title" = "Space invite"; -// Mark: - Polls +// MARK: - Polls "poll_edit_form_create_poll" = "Create poll"; @@ -2538,7 +2540,7 @@ To enable access, tap Settings> Location and select Always"; "user_session_overview_session_details_button_title" = "Session details"; -// Mark: - WYSIWYG Composer +// MARK: - WYSIWYG Composer // Send Media Actions "wysiwyg_composer_start_action_media_picker" = "Photo Library"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1dde36ec4..5730a6305 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9215,6 +9215,14 @@ public class VectorL10n: NSObject { public static var voiceBroadcastUnauthorizedTitle: String { return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") } + /// You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call. + public static var voiceBroadcastVoipCannotStartDescription: String { + return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_description") + } + /// Can’t start a call + public static var voiceBroadcastVoipCannotStartTitle: String { + return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_title") + } /// You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message public static var voiceMessageBroadcastInProgressMessage: String { return VectorL10n.tr("Vector", "voice_message_broadcast_in_progress_message") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index ca6daf5e0..7e9873209 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5187,7 +5187,14 @@ static CGSize kThreadListBarButtonItemImageSize; - (IBAction)onVoiceCallPressed:(id)sender { - if (self.isCallActive) + // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + + if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) { + [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle + message:VectorL10n.voiceBroadcastVoipCannotStartDescription]; + } + else if (self.isCallActive) { [self hangupCall]; } @@ -5199,7 +5206,15 @@ static CGSize kThreadListBarButtonItemImageSize; - (IBAction)onVideoCallPressed:(id)sender { - [self placeCallWithVideo:YES]; + // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + + if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) { + [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle + message:VectorL10n.voiceBroadcastVoipCannotStartDescription]; + } else { + [self placeCallWithVideo:YES]; + } } - (IBAction)onThreadListTapped:(id)sender diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index 2a9fe90b8..77d4c394a 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -68,6 +68,10 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func pauseRecording() { voiceBroadcastRecorderViewModel.context.send(viewAction: .pause) } + + func isVoiceBroadcastRecording() -> Bool { + return voiceBroadcastRecorderService.isRecording + } // MARK: - Private } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index e7f998716..7b82429cb 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -85,6 +85,14 @@ import Foundation voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecording() } + @objc public func isVoiceBroadcastRecording() -> Bool { + guard let coordinator = voiceBroadcastRecorderCoordinatorForCurrentEvent() else { + return false + } + + return coordinator.isVoiceBroadcastRecording() + } + // MARK: - Private /// Retrieve the voiceBroadcast recorder coordinator for the current event or nil if it hasn't been created yet diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index 437abbe3c..fd8fc664d 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -44,6 +44,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // MARK: Public weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? + var isRecording: Bool { + return audioEngine.isRunning + } // MARK: - Setup diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift index 9e48e2e9a..1b3e77878 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift @@ -25,6 +25,9 @@ protocol VoiceBroadcastRecorderServiceProtocol { /// Service delegate var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? { get set } + /// Returns if a voice broadcast is currently recording. + var isRecording: Bool { get } + /// Start voice broadcast recording. func startRecordingVoiceBroadcast() diff --git a/changelog.d/pr-7225.change b/changelog.d/pr-7225.change new file mode 100644 index 000000000..df6cfd7a7 --- /dev/null +++ b/changelog.d/pr-7225.change @@ -0,0 +1 @@ +Labs: VoiceBroadcast: Handle VoIP buttons when VB is used From d542c40f8f2662f60a6e4497c9c5f19be06e41ea Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Tue, 10 Jan 2023 16:13:14 +0100 Subject: [PATCH 007/468] Add mark as unread option for rooms --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ Riot/Modules/Common/Recents/RecentsViewController.m | 5 +++++ .../ActionProviders/RoomActionProvider.swift | 10 +++++++++- .../Services/RoomContextActionService.swift | 7 ++++++- .../Services/RoomContextActionServiceProtocol.swift | 1 + .../MatrixKit/Models/RoomList/MXKRecentCellData.m | 3 ++- Riot/Modules/Room/MXKRoomViewController.m | 5 +++++ changelog.d/7253.feature | 1 + 9 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changelog.d/7253.feature diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f1b90008b..628267178 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1993,6 +1993,7 @@ Tap the + to start adding people."; "home_context_menu_normal_priority" = "Normal priority"; "home_context_menu_leave" = "Leave"; "home_context_menu_mark_as_read" = "Mark as read"; +"home_context_menu_mark_as_unread" = "Mark as unread"; "home_syncing" = "Syncing"; // MARK: - Favourites diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 2008451ea..d1c597360 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2543,6 +2543,10 @@ public class VectorL10n: NSObject { public static var homeContextMenuMarkAsRead: String { return VectorL10n.tr("Vector", "home_context_menu_mark_as_read") } + /// Mark as unread + public static var homeContextMenuMarkAsUnread: String { + return VectorL10n.tr("Vector", "home_context_menu_mark_as_unread") + } /// Mute public static var homeContextMenuMute: String { return VectorL10n.tr("Vector", "home_context_menu_mute") diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 0a86bd3c0..04ff954d0 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -2471,6 +2471,11 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro editedRoomId = nil; } +-(void)roomContextActionServiceDidMarkedRoom:(id)service +{ + [self refreshRecentsTable]; +} + #pragma mark - RecentCellContextMenuProviderDelegate - (void)recentCellContextMenuProviderDidStartShowingPreview:(RecentCellContextMenuProvider *)menuProvider diff --git a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift index 7d12f8878..682c3c9a6 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift @@ -34,7 +34,7 @@ class RoomActionProvider: RoomActionProviderProtocol { var menu: UIMenu { if service.isRoomJoined { - var children = service.hasUnread ? [self.markAsReadAction] : [] + var children = service.hasUnread ? [self.markAsReadAction] : [self.markAsUnreadAction] children.append(contentsOf: [ self.directChatAction, self.notificationsAction, @@ -113,6 +113,14 @@ class RoomActionProvider: RoomActionProviderProtocol { self.service.markAsRead() } } + private var markAsUnreadAction: UIAction { + return UIAction( + title: VectorL10n.homeContextMenuMarkAsUnread, + image: UIImage(systemName: "envelope")) { [weak self] action in + guard let self = self else { return } + self.service.markAsUnread() + } + } private var leaveAction: UIAction { let image = UIImage(systemName: "rectangle.righthalf.inset.fill.arrow.right") diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift index c3abab55b..a9d07c27c 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift @@ -38,7 +38,7 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { self.room = room self.delegate = delegate self.isRoomJoined = room.summary?.isJoined ?? false - self.hasUnread = room.summary?.hasAnyUnread ?? false + self.hasUnread = (room.summary?.hasAnyUnread ?? false) || room.isMarkedAsUnread self.roomMembership = room.summary?.membership ?? .unknown self.session = room.mxSession self.unownedRoomService = UnownedRoomContextActionService(roomId: room.roomId, canonicalAlias: room.summary?.aliases?.first, session: self.session, delegate: delegate) @@ -108,6 +108,11 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { func markAsRead() { room.markAllAsRead() + self.delegate?.roomContextActionServiceDidMarkedRoom(self) + } + func markAsUnread() { + room.markAsUnread() + self.delegate?.roomContextActionServiceDidMarkedRoom(self) } // MARK: - Private diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift index d44213bd4..4ebd13d66 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift @@ -22,6 +22,7 @@ import Foundation func roomContextActionService(_ service: RoomContextActionServiceProtocol, showRoomNotificationSettingsForRoomWithId roomId: String) func roomContextActionServiceDidJoinRoom(_ service: RoomContextActionServiceProtocol) func roomContextActionServiceDidLeaveRoom(_ service: RoomContextActionServiceProtocol) + func roomContextActionServiceDidMarkedRoom(_ service: RoomContextActionServiceProtocol) } /// `RoomContextActionServiceProtocol` classes are meant to be called by a `RoomActionProviderProtocol` instance so it provides the implementation of the menu actions. diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m index 958e316fa..4e6ebbe80 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m @@ -63,7 +63,8 @@ - (BOOL)hasUnread { - return (roomSummary.localUnreadEventCount != 0); + bool isRoomUnread = [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId]; + return (roomSummary.localUnreadEventCount != 0 || isRoomUnread); } - (NSString *)roomIdentifier diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index de6a691f4..06fa9d618 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -370,6 +370,11 @@ [self.roomDataSource.room.summary markAllAsReadLocally]; [self updateCurrentEventIdAtTableBottom:YES]; + + if (!self.isContextPreview) + { + [self.roomDataSource.room unmarkAsUnread]; + } } - (void)viewWillDisappear:(BOOL)animated diff --git a/changelog.d/7253.feature b/changelog.d/7253.feature new file mode 100644 index 000000000..9bf47a493 --- /dev/null +++ b/changelog.d/7253.feature @@ -0,0 +1 @@ +Add mark as unread option for rooms \ No newline at end of file From 9b566419f0a69e3ec464e2d8c19b92861b2d6f13 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Jan 2023 16:50:20 +0000 Subject: [PATCH 008/468] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index ea1f94f28..7271fabb8 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.15 -CURRENT_PROJECT_VERSION = 1.9.15 +MARKETING_VERSION = 1.9.16 +CURRENT_PROJECT_VERSION = 1.9.16 From 09cc9b78bce5b5806c2dd06df9931ba1ae284a74 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Jan 2023 16:37:48 +0000 Subject: [PATCH 009/468] Fix Element Alpha workflow set-output is deprecated and the warning fails the secret check. Instead match ElementX by comparing the pull request repo to make sure it matches the workflow's repo. --- .github/workflows/release-alpha.yml | 16 ++-------------- changelog.d/pr-7256.build | 1 + 2 files changed, 3 insertions(+), 14 deletions(-) create mode 100644 changelog.d/pr-7256.build diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index f8d03f08b..e4f4671de 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -13,22 +13,10 @@ env: MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} jobs: - check-secret: - runs-on: macos-12 - outputs: - out-key: ${{ steps.out-key.outputs.defined }} - steps: - - id: out-key - env: - P12_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} - P12_PASSWORD_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} - if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}" - run: echo "::set-output name=defined::true" build: - # Run job if secrets are available (not available for forks). - needs: [check-secret] + # Don't run for forks as secrets are unavailable. if: | - needs.check-secret.outputs.out-key == 'true' && + github.event.pull_request.head.repo.full_name == github.repository && (github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build'))) diff --git a/changelog.d/pr-7256.build b/changelog.d/pr-7256.build new file mode 100644 index 000000000..9a2cd140b --- /dev/null +++ b/changelog.d/pr-7256.build @@ -0,0 +1 @@ +Fix Element Alpha workflow not being able to run. From 89561ea7252c974f78c84edb82c2f315a4981462 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 15:20:18 +0100 Subject: [PATCH 010/468] default link color in the RTE --- .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 7956ad107..b47215c25 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -44,7 +44,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel( textColor: ThemeService.shared().theme.colors.primaryContent, - linkColor: ThemeService.shared().theme.colors.accent, codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor ) private var viewModel: ComposerViewModelProtocol! @@ -299,7 +298,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background wysiwygViewModel.textColor = theme.colors.primaryContent - wysiwygViewModel.linkColor = theme.colors.accent + wysiwygViewModel.linkColor = .link wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor } From 727e6f8967afc86246cb9921b44a356f70d9d0d2 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 10 Jan 2023 16:57:13 +0100 Subject: [PATCH 011/468] Fixe the now playing info center while a voice broadcast is played --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../VoiceMessageMediaServiceProvider.swift | 19 +++++++++++++---- .../VoiceMessageNowPlayingInfoProvider.swift | 21 +++++++++++++++++++ .../VoiceBroadcastPlaybackViewModel.swift | 18 ++++++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1b97f791..7ba0e41cd 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2215,6 +2215,7 @@ Tap the + to start adding people."; "voice_broadcast_stop_alert_agree_button" = "Yes, stop"; "voice_broadcast_voip_cannot_start_title" = "Can’t start a call"; "voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call."; +"voice_broadcast_playback_lock_screen_placeholder" = "Voice broadcast"; // MARK: - Version check diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5730a6305..0e5553010 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9191,6 +9191,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLoadingError: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error") } + /// Voice broadcast + public static var voiceBroadcastPlaybackLockScreenPlaceholder: String { + return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder") + } /// Yes, stop public static var voiceBroadcastStopAlertAgreeButton: String { return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_agree_button") diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 54262f828..f1b1b0e7c 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -28,6 +28,7 @@ import MediaPlayer private var roomAvatarLoader: MXMediaLoader? private let audioPlayers: NSMapTable private let audioRecorders: NSHashTable + private let nowPlayingInfoProviders: NSMapTable private var displayLink: CADisplayLink! @@ -93,6 +94,7 @@ import MediaPlayer private override init() { audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) + nowPlayingInfoProviders = NSMapTable(valueOptions: .weakMemory) activeAudioPlayers = Set() super.init() @@ -123,6 +125,10 @@ import MediaPlayer pauseAllServicesExcept(nil) } + func setNowPlayingInfoProvider(_ provider: VoiceMessageNowPlayingInfoProvider, forPlayer player: VoiceMessageAudioPlayer) { + nowPlayingInfoProviders.setObject(provider, forKey: player) + } + // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -256,9 +262,14 @@ import MediaPlayer return } - let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() - nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder, - MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any, - MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any] + // If we have a NowPlayingInfoProvider for this player + if let nowPlayingInfoProvider = nowPlayingInfoProviders.object(forKey: audioPlayer) { + nowPlayingInfoProvider.updatePlayingInfoCenter(forPlayer: audioPlayer) + } else { + let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder, + MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any, + MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any] + } } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift new file mode 100644 index 000000000..c6ab193a9 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift @@ -0,0 +1,21 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc protocol VoiceMessageNowPlayingInfoProvider { + func updatePlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 1f5ac9872..1393e8529 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -16,6 +16,7 @@ import Combine import SwiftUI +import MediaPlayer // TODO: VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK // We need a VoiceBroadcastPlaybackServiceProtocol and VoiceBroadcastAggregatorProtocol @@ -302,6 +303,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // Init and start the player on the first chunk let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) audioPlayer.registerDelegate(self) + self.mediaServiceProvider.setNowPlayingInfoProvider(self, forPlayer: audioPlayer) audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) self.audioPlayer = audioPlayer @@ -482,3 +484,19 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { stopIfVoiceBroadcastOver() } } + +// MARK: - NowPlayingInfoProvider + +extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoProvider { + func updatePlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) { + guard audioPlayer != nil, audioPlayer === player else { + return + } + + let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder, + MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any, + MPNowPlayingInfoPropertyElapsedPlaybackTime: (state.bindings.progress / 1000.0) as Any, + MPNowPlayingInfoPropertyPlaybackRate: state.playbackState == .playing ? 1 : 0] + } +} From 70740b2249043cba1b4deae04a5e03ecc78e1aca Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 10 Jan 2023 18:13:22 +0100 Subject: [PATCH 012/468] Add Towncrier file --- changelog.d/pr-7257.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7257.bugfix diff --git a/changelog.d/pr-7257.bugfix b/changelog.d/pr-7257.bugfix new file mode 100644 index 000000000..c83bd783d --- /dev/null +++ b/changelog.d/pr-7257.bugfix @@ -0,0 +1 @@ +Voice Broacast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. From 640201b4d8016f2ae703d9917e6ad4d8d5012218 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 18:45:03 +0100 Subject: [PATCH 013/468] the behaviour is now the same as android for links, except for the the "(edited)" button which should be grey and this fix has made it blue, will check how to solve this but it might also need some design inputs regarding the "reply" label (which as android is blue, but both are very different from the ) --- Riot/Categories/MXKTableViewCellWithTextView.swift | 1 - .../Views/MessagesSearchResultAttachmentBubbleCell.m | 4 +--- .../Views/MessagesSearchResultTextMsgBubbleCell.m | 2 -- .../RoomCreation/RoomCreationCollapsedBubbleCell.m | 2 -- .../RoomMembership/RoomMembershipBubbleCell.m | 7 ------- .../RoomMembership/RoomMembershipCollapsedBubbleCell.m | 7 ------- .../TextMessage/Common/TextMessageBaseBubbleCell.swift | 8 -------- .../Incoming/Clear/RoomIncomingAttachmentBubbleCell.m | 2 -- .../RoomIncomingAttachmentWithPaginationTitleBubbleCell.m | 1 - .../RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m | 7 ------- .../Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m | 2 -- .../RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m | 1 - ...chmentWithPaginationTitleWithoutSenderNameBubbleCell.m | 6 ------ .../RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m | 7 ------- .../Incoming/Clear/RoomIncomingTextMsgBubbleCell.m | 2 -- .../RoomIncomingTextMsgWithPaginationTitleBubbleCell.m | 1 - ...extMsgWithPaginationTitleWithoutSenderNameBubbleCell.m | 7 ------- .../RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m | 2 -- .../RoomIncomingTextMsgWithoutSenderNameBubbleCell.m | 2 -- .../Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m | 2 -- .../RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m | 2 -- 21 files changed, 1 insertion(+), 74 deletions(-) diff --git a/Riot/Categories/MXKTableViewCellWithTextView.swift b/Riot/Categories/MXKTableViewCellWithTextView.swift index 9331b5fd6..03288d377 100644 --- a/Riot/Categories/MXKTableViewCellWithTextView.swift +++ b/Riot/Categories/MXKTableViewCellWithTextView.swift @@ -24,7 +24,6 @@ extension MXKTableViewCellWithTextView: Themable { func update(theme: Theme) { mxkTextView.backgroundColor = .clear mxkTextView.textColor = theme.textPrimaryColor - mxkTextView.tintColor = theme.tintColor backgroundColor = theme.backgroundColor contentView.backgroundColor = .clear } diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m index 561b0aac5..32e8edcd9 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m @@ -28,9 +28,7 @@ [super customizeTableViewCellRendering]; self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; - + [self updateUserNameColor]; } diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m index 40818f8a0..780d11efe 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m @@ -30,8 +30,6 @@ [self updateUserNameColor]; self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m index c06b34016..34e292dc3 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m @@ -26,8 +26,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m index 2fe69ff5f..21ae1b2ad 100644 --- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m @@ -37,13 +37,6 @@ xibPictureViewTopConstraintConstant = self.pictureViewTopConstraint.constant; } -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)prepareForReuse { [super prepareForReuse]; diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m index eac1df923..558ef7050 100644 --- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m @@ -26,13 +26,6 @@ @implementation RoomMembershipCollapsedBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)layoutSubviews { [super layoutSubviews]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift index 5a7d8984b..d38b5cb3c 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift @@ -51,14 +51,6 @@ class TextMessageBaseBubbleCell: SizableBaseRoomCell, RoomCellURLPreviewDisplaya override func setupMessageTextViewLongPressGesture() { // Do nothing, otherwise default setup prevent link tap } - - override func update(theme: Theme) { - super.update(theme: theme) - - if let messageTextView = self.messageTextView { - messageTextView.tintColor = theme.tintColor - } - } } // MARK: - RoomCellTimestampDisplayable diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m index 3dd06a1d8..6da4b9db1 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m index 2a7a96788..d40aea21c 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m index 30b096e91..2ca481673 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m @@ -23,13 +23,6 @@ @implementation RoomIncomingAttachmentWithoutSenderInfoBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - + (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth { CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m index 456305e9c..716e15c31 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m index d78362a65..4cb4f3761 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m index 46ea41861..57cf00691 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m @@ -22,11 +22,5 @@ @implementation RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m index 7021df5f6..ec91ee33d 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m @@ -24,13 +24,6 @@ @implementation RoomOutgoingAttachmentWithoutSenderInfoBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - - (void)render:(MXKCellData *)cellData { [super render:cellData]; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m index 28bbdb19c..288e54d0a 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index c66531dce..9e7bc37c3 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -30,7 +30,6 @@ [self updateUserNameColor]; self.paginationLabel.textColor = ThemeService.shared.theme.tintColor; self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor; - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } - (void)render:(MXKCellData *)cellData diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m index ae4126049..86862a0d0 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m @@ -22,11 +22,4 @@ @implementation RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell -- (void)customizeTableViewCellRendering -{ - [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; -} - @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index db4370dc5..245b17e77 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m index 8678dd894..3f5d73475 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m index a31a52029..fa50aa35f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m @@ -28,8 +28,6 @@ [super customizeTableViewCellRendering]; [self updateUserNameColor]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 0bc9f1de3..dfafa3df6 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -25,8 +25,6 @@ - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; - - self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } @end From 7a7d7c71b9601b46652f403aba6b6058aa1ba77b Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 21:29:02 +0100 Subject: [PATCH 014/468] fix --- Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift | 1 + Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 1 + Riot/Modules/MatrixKit/Utils/MXKTools.m | 2 ++ .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 1 - Riot/Utils/EventFormatter.m | 2 -- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift index de1b19821..e65f07ce1 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift @@ -51,6 +51,7 @@ class HTMLFormatter: NSObject { DTDefaultFontName: font.fontName, DTDefaultFontSize: font.pointSize, DTDefaultLinkDecoration: false, + DTDefaultLinkColor: UIColor.link, DTWillFlushBlockCallBack: sanitizeCallback ] options.merge(extraOptions) { (_, new) in new } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 21be58eb1..70c36ecd5 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1749,6 +1749,7 @@ static NSString *const kHTMLATagRegexPattern = @"( if (url.URL) { [str addAttribute:NSLinkAttributeName value:url.URL range:matchRange]; + [str addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:matchRange]; } } } diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 10cf491a6..a1c9d702a 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -1083,6 +1083,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // If the match is fully in the link, skip it if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length) { + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:linkMatch.range]; hasAlreadyLink = YES; break; } @@ -1097,6 +1098,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo NSString *link = [mutableAttributedString.string substringWithRange:match.range]; link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; [mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:match.range]; } }]; } diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index b47215c25..0fdfb1670 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -298,7 +298,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background wysiwygViewModel.textColor = theme.colors.primaryContent - wysiwygViewModel.linkColor = .link wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 654fab329..eb793b811 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -384,8 +384,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", [VectorL10n eventFormatterMessageEditedMention]] attributes:@{ NSLinkAttributeName: linkActionString, - // NOTE: Color is curretly overidden by UIText.tintColor as we use `NSLinkAttributeName`. - // If we use UITextView.linkTextAttributes to set link color we will also have the issue that color will be the same for all kind of links. NSForegroundColorAttributeName: self.editionMentionTextColor, NSFontAttributeName: self.editionMentionTextFont }]]; From 7dcb9ddbef85d693b33d2eff60fae453f7eb2986 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 21:39:09 +0100 Subject: [PATCH 015/468] this is required to enable custom colors for specific links --- Riot/Modules/MatrixKit/Views/MXKMessageTextView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m index 74132c23f..ca572733a 100644 --- a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m +++ b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m @@ -67,6 +67,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText { + self.linkTextAttributes = @{}; if (@available(iOS 15.0, *)) { [self flushPills]; } From 9505bec5218ee6d7804dfd82fd721d9f3835b2c5 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 23:40:35 +0100 Subject: [PATCH 016/468] done --- Riot/Modules/MatrixKit/Utils/MXKTools.m | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index a1c9d702a..d5722d961 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -46,6 +46,7 @@ static NSRegularExpression *eventIdRegex; static NSRegularExpression *httpLinksRegex; // A regex to find all HTML tags static NSRegularExpression *htmlTagsRegex; +static NSDataDetector *linkDetector; @implementation MXKTools @@ -60,7 +61,8 @@ static NSRegularExpression *htmlTagsRegex; eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil]; httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil]; - htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; + htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; + linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; }); } @@ -1037,6 +1039,23 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo { [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex]; } + + // This allows to check for normal url based links (like https://element.io) + // And set back the default link color + NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)]; + if (matches && matches.count > 0) + { + for (NSTextCheckingResult *match in matches) + { + NSRange matchRange = [match range]; + NSURL *matchUrl = [match URL]; + NSURLComponents *url = [[NSURLComponents new] initWithURL:matchUrl resolvingAgainstBaseURL:NO]; + if (url.URL) + { + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:matchRange]; + } + } + } } + (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex @@ -1083,6 +1102,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // If the match is fully in the link, skip it if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length) { + // but before we set the right color [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:linkMatch.range]; hasAlreadyLink = YES; break; From eb5a4e1fff8f0d2e009bbfb2364528b5ed02606f Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 23:46:59 +0100 Subject: [PATCH 017/468] changelog part 1 --- changelog.d/5148.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5148.bugfix diff --git a/changelog.d/5148.bugfix b/changelog.d/5148.bugfix new file mode 100644 index 000000000..7f1ddcb3e --- /dev/null +++ b/changelog.d/5148.bugfix @@ -0,0 +1 @@ +The (edited) tag for messages is now light grey like on web and Android. \ No newline at end of file From c4d8e3e326ed91f047c559178a2818a7b43da1a8 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 23:52:34 +0100 Subject: [PATCH 018/468] changelog --- changelog.d/5437.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5437.bugfix diff --git a/changelog.d/5437.bugfix b/changelog.d/5437.bugfix new file mode 100644 index 000000000..59c6185c5 --- /dev/null +++ b/changelog.d/5437.bugfix @@ -0,0 +1 @@ +HTML links should now be displayed in default system blue. \ No newline at end of file From d491b134ff2fa29d00ce36645c8187c7d80ac996 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 11 Jan 2023 23:56:46 +0100 Subject: [PATCH 019/468] changelog --- changelog.d/2292.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2292.change diff --git a/changelog.d/2292.change b/changelog.d/2292.change new file mode 100644 index 000000000..b1d61e979 --- /dev/null +++ b/changelog.d/2292.change @@ -0,0 +1 @@ +Links are now in blue like on web and Android. \ No newline at end of file From 82bf8782e647339bd0317c6c50155091492c510c Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 00:02:00 +0100 Subject: [PATCH 020/468] changelog --- changelog.d/2419.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2419.bugfix diff --git a/changelog.d/2419.bugfix b/changelog.d/2419.bugfix new file mode 100644 index 000000000..900d2135c --- /dev/null +++ b/changelog.d/2419.bugfix @@ -0,0 +1 @@ +Hyperlinks are now blue and should now be distinguishable from unsent messages in encrypted rooms. \ No newline at end of file From 643bd560873b6fa1e35c86b023ed65315aa6dccc Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 00:13:29 +0100 Subject: [PATCH 021/468] changelog --- changelog.d/7263.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7263.bugfix diff --git a/changelog.d/7263.bugfix b/changelog.d/7263.bugfix new file mode 100644 index 000000000..061f8a644 --- /dev/null +++ b/changelog.d/7263.bugfix @@ -0,0 +1 @@ +Timeline's tag and hyperlinks match now in colour Android and Web. \ No newline at end of file From 2554c987cb7dffab92996c2219abcdd9d6a8a180 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Thu, 12 Jan 2023 09:20:03 +0100 Subject: [PATCH 022/468] new icon with badge --- .../ContextMenu/ActionProviders/RoomActionProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift index 682c3c9a6..c019aae9c 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift @@ -116,7 +116,7 @@ class RoomActionProvider: RoomActionProviderProtocol { private var markAsUnreadAction: UIAction { return UIAction( title: VectorL10n.homeContextMenuMarkAsUnread, - image: UIImage(systemName: "envelope")) { [weak self] action in + image: UIImage(systemName: "envelope.badge")) { [weak self] action in guard let self = self else { return } self.service.markAsUnread() } From c3580bbfcbfe6a9b4d2c22c0ce50e4c331c886ff Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 11 Jan 2023 14:56:40 +0100 Subject: [PATCH 023/468] Fix NowPlayingInfoCenter for a live voice broadcast --- .../VoiceMessageMediaServiceProvider.swift | 40 +++++++++++++------ ... VoiceMessageNowPlayingInfoDelegate.swift} | 7 +++- .../VoiceBroadcastPlaybackViewModel.swift | 32 +++++++++++---- 3 files changed, 57 insertions(+), 22 deletions(-) rename Riot/Modules/Room/VoiceMessages/{VoiceMessageNowPlayingInfoProvider.swift => VoiceMessageNowPlayingInfoDelegate.swift} (72%) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index f1b1b0e7c..04a4ac7ec 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -28,7 +28,7 @@ import MediaPlayer private var roomAvatarLoader: MXMediaLoader? private let audioPlayers: NSMapTable private let audioRecorders: NSHashTable - private let nowPlayingInfoProviders: NSMapTable + private let nowPlayingInfoDelegates: NSMapTable private var displayLink: CADisplayLink! @@ -94,7 +94,7 @@ import MediaPlayer private override init() { audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) - nowPlayingInfoProviders = NSMapTable(valueOptions: .weakMemory) + nowPlayingInfoDelegates = NSMapTable(valueOptions: .weakMemory) activeAudioPlayers = Set() super.init() @@ -125,8 +125,12 @@ import MediaPlayer pauseAllServicesExcept(nil) } - func setNowPlayingInfoProvider(_ provider: VoiceMessageNowPlayingInfoProvider, forPlayer player: VoiceMessageAudioPlayer) { - nowPlayingInfoProviders.setObject(provider, forKey: player) + func registerNowPlayingInfoDelegate(_ delegate: VoiceMessageNowPlayingInfoDelegate, forPlayer player: VoiceMessageAudioPlayer) { + nowPlayingInfoDelegates.setObject(delegate, forKey: player) + } + + func deregisterNowPlayingInfoDelegate(forPlayer player: VoiceMessageAudioPlayer) { + nowPlayingInfoDelegates.removeObject(forKey: player) } // MARK: - VoiceMessageAudioPlayerDelegate @@ -140,16 +144,28 @@ import MediaPlayer func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if currentlyPlayingAudioPlayer == audioPlayer { - currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter() + // If we have a NowPlayingInfoDelegate for this player + let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) + + // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) + if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { + currentlyPlayingAudioPlayer = nil + tearDownRemoteCommandCenter(for: audioPlayer) + } } activeAudioPlayers.remove(audioPlayer) } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if currentlyPlayingAudioPlayer == audioPlayer { - currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter() + // If we have a NowPlayingInfoDelegate for this player + let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) + + // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) + if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { + currentlyPlayingAudioPlayer = nil + tearDownRemoteCommandCenter(for: audioPlayer) + } } activeAudioPlayers.remove(audioPlayer) } @@ -248,7 +264,7 @@ import MediaPlayer } } - private func tearDownRemoteCommandCenter() { + private func tearDownRemoteCommandCenter(for audioPlayer: VoiceMessageAudioPlayer) { displayLink.isPaused = true UIApplication.shared.endReceivingRemoteControlEvents() @@ -262,9 +278,9 @@ import MediaPlayer return } - // If we have a NowPlayingInfoProvider for this player - if let nowPlayingInfoProvider = nowPlayingInfoProviders.object(forKey: audioPlayer) { - nowPlayingInfoProvider.updatePlayingInfoCenter(forPlayer: audioPlayer) + // Checks if we have a delegate for this player, or if we should update the NowPlayingInfoCenter ourselves + if let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) { + nowPlayingInfoDelegate.updateNowPlayingInfoCenter(forPlayer: audioPlayer) } else { let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder, diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift similarity index 72% rename from Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift rename to Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift index c6ab193a9..6955b4c0f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift @@ -16,6 +16,9 @@ import Foundation -@objc protocol VoiceMessageNowPlayingInfoProvider { - func updatePlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) +@objc protocol VoiceMessageNowPlayingInfoDelegate { + + func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) + + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: VoiceMessageAudioPlayer) -> Bool } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 1393e8529..56e435757 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -303,7 +303,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // Init and start the player on the first chunk let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) audioPlayer.registerDelegate(self) - self.mediaServiceProvider.setNowPlayingInfoProvider(self, forPlayer: audioPlayer) + self.mediaServiceProvider.registerNowPlayingInfoDelegate(self, forPlayer: audioPlayer) audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) self.audioPlayer = audioPlayer @@ -472,6 +472,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { state.playbackState = .stopped state.playingState.isLive = false audioPlayer.deregisterDelegate(self) + self.mediaServiceProvider.deregisterNowPlayingInfoDelegate(forPlayer: audioPlayer) self.audioPlayer = nil } @@ -485,18 +486,33 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { } } -// MARK: - NowPlayingInfoProvider +// MARK: - VoiceMessageNowPlayingInfoDelegate -extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoProvider { - func updatePlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) { +extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoDelegate { + + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool { + guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else { + return true + } + + return state.broadcastState == .stopped + } + + func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) { guard audioPlayer != nil, audioPlayer === player else { return } let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() - nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder, - MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any, - MPNowPlayingInfoPropertyElapsedPlaybackTime: (state.bindings.progress / 1000.0) as Any, - MPNowPlayingInfoPropertyPlaybackRate: state.playbackState == .playing ? 1 : 0] + nowPlayingInfoCenter.nowPlayingInfo = [ + // Title + MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder, + // Buffering status (using the "artist" property to display it under the title) + MPMediaItemPropertyArtist: state.playbackState == .buffering ? VectorL10n.voiceBroadcastBuffering : "", + // Duration + MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any, + // Elapsed time + MPNowPlayingInfoPropertyElapsedPlaybackTime: (state.bindings.progress / 1000.0) as Any, + ] } } From dba5ff387fdbeba2731e8aa0aba15d148e67070d Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 10:41:52 +0100 Subject: [PATCH 024/468] fixed a test and some code improvements --- Riot/Modules/MatrixKit/Utils/MXKTools.m | 2 +- RiotTests/MatrixKitTests/MXKEventFormatterTests.m | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index d5722d961..fdb7f51f8 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -1043,7 +1043,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // This allows to check for normal url based links (like https://element.io) // And set back the default link color NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)]; - if (matches && matches.count > 0) + if (matches) { for (NSTextCheckingResult *match in matches) { diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index fbe801665..e08619d5d 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -414,14 +414,16 @@ NSString *s = @"Matrix HQ room is at https://matrix.to/#/room/#matrix:matrix.org."; NSAttributedString *as = [eventFormatter renderString:s forEvent:anEvent]; - __block NSUInteger ranges = 0; + __block bool hasLink = false; [as enumerateAttributesInRange:NSMakeRange(0, as.length) options:(0) usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { - - ranges++; + if (attrs[NSLinkAttributeName]) { + hasLink = true; + *stop = true; + } }]; - XCTAssertEqual(ranges, 1, @"There should be no link in this case. We let the UI manage the link"); + XCTAssertEqual(hasLink, false, @"There should be no link in this case. We let the UI manage the link"); } #pragma mark - Event sender/target info From 5b47b99dce053c53a40ffd4fa3ca32ae4b7349b2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 11 Jan 2023 18:40:56 +0100 Subject: [PATCH 025/468] =?UTF-8?q?Add=20poll=20history=20in=20room?= =?UTF-8?q?=E2=80=99s=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Config/BuildSettings.swift | 7 +++++++ .../Room/pollHistory.imageset/Contents.json | 12 ++++++++++++ .../Room/pollHistory.imageset/pollHistory.svg | 3 +++ Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 4 ++++ .../RoomInfoList/RoomInfoListViewController.swift | 10 ++++++++++ 7 files changed, 38 insertions(+) create mode 100644 Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 2f85f3c13..0c23cdd20 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -399,6 +399,13 @@ final class BuildSettings: NSObject { // MARK: - Polls static let pollsEnabled = true + static var pollsHistoryEnabled: Bool { + #if DEBUG + true + #else + false + #endif + } // MARK: - Location Sharing diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json new file mode 100644 index 000000000..fcc0b5765 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pollHistory.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg new file mode 100644 index 000000000..a0243252c --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1b97f791..b87d35a78 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -982,6 +982,7 @@ Tap the + to start adding people."; "room_details_title_for_dm" = "Details"; "room_details_people" = "Members"; "room_details_files" = "Uploads"; +"room_details_polls" = "Poll history"; "room_details_search" = "Search room"; "room_details_integrations" = "Integrations"; "room_details_settings" = "Settings"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index ed763a171..6dc47703a 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -285,6 +285,7 @@ internal class Asset: NSObject { internal static let modIcon = ImageAsset(name: "mod_icon") internal static let moreReactions = ImageAsset(name: "more_reactions") internal static let notifications = ImageAsset(name: "notifications") + internal static let pollHistory = ImageAsset(name: "pollHistory") internal static let reactionsMoreAction = ImageAsset(name: "reactions_more_action") internal static let roomAccessInfoHeaderIcon = ImageAsset(name: "room_access_info_header_icon") internal static let scrollup = ImageAsset(name: "scrollup") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5730a6305..36fd6b67a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5503,6 +5503,10 @@ public class VectorL10n: NSObject { public static var roomDetailsPhotoForDm: String { return VectorL10n.tr("Vector", "room_details_photo_for_dm") } + /// Poll history + public static var roomDetailsPolls: String { + return VectorL10n.tr("Vector", "room_details_polls") + } /// Suggest to space members public static var roomDetailsPromoteRoomSuggestTitle: String { return VectorL10n.tr("Vector", "room_details_promote_room_suggest_title") diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index d55f674fe..db245c111 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -174,6 +174,11 @@ final class RoomInfoListViewController: UIViewController { let rowMembers = Row(type: .default, icon: Asset.Images.userIcon.image, text: text, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .members)) } + + let rowPollHistory = Row(type: .default, icon: Asset.Images.pollHistory.image, text: VectorL10n.roomDetailsPolls, accessoryType: .disclosureIndicator) { + #warning("Add action") + } + let rowUploads = Row(type: .default, icon: Asset.Images.scrollup.image, text: VectorL10n.roomDetailsFiles, accessoryType: .disclosureIndicator) { self.viewModel.process(viewAction: .navigate(target: .uploads)) } @@ -193,6 +198,11 @@ final class RoomInfoListViewController: UIViewController { rows.append(rowIntegrations) } rows.append(rowMembers) + + if BuildSettings.pollsHistoryEnabled { + rows.append(rowPollHistory) + } + rows.append(rowUploads) if !viewData.isEncrypted { rows.append(rowSearch) From b927d6afc8274d886b0bea7685b90761d9287e2d Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 10:43:22 +0100 Subject: [PATCH 026/468] code improvement --- RiotTests/MatrixKitTests/MXKEventFormatterTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index e08619d5d..b21f57715 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -414,7 +414,7 @@ NSString *s = @"Matrix HQ room is at https://matrix.to/#/room/#matrix:matrix.org."; NSAttributedString *as = [eventFormatter renderString:s forEvent:anEvent]; - __block bool hasLink = false; + __block BOOL hasLink = false; [as enumerateAttributesInRange:NSMakeRange(0, as.length) options:(0) usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { if (attrs[NSLinkAttributeName]) { From b9bf4376dbf9c91a80445a5f0f1e3c5cb76d6c2a Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 11:43:29 +0100 Subject: [PATCH 027/468] Add poll history scaffolding --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 19 +++- .../RoomInfoList/RoomInfoListViewAction.swift | 1 + .../RoomInfoListViewController.swift | 2 +- .../Coordinator/PollHistoryCoordinator.swift | 76 +++++++++++++ .../MockPollHistoryScreenState.swift | 56 ++++++++++ .../Room/PollHistory/PollHistoryModels.swift | 67 ++++++++++++ .../PollHistory/PollHistoryViewModel.swift | 42 ++++++++ .../PollHistoryViewModelProtocol.swift | 22 ++++ .../Test/UI/PollHistoryUITests.swift | 38 +++++++ .../Test/Unit/PollHistoryViewModelTests.swift | 48 +++++++++ .../Room/PollHistory/View/PollHistory.swift | 102 ++++++++++++++++++ 11 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 44ddb735b..6da39162a 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -157,6 +157,10 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { return coordinator } + private func pollHistoryCoordinator() -> PollHistoryCoordinator { + return PollHistoryCoordinator(parameters: .init(promptType: .regular)) + } + private func showRoomDetails(with target: RoomInfoListTarget, animated: Bool) { switch target { case .integrations: @@ -174,8 +178,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { case .notifications: let coordinator = createRoomNotificationSettingsCoordinator() coordinator.start() - self.add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator, animated: true, popCompletion: nil) + push(coordinator: coordinator) + case .pollHistory: + let coordinator = pollHistoryCoordinator() + coordinator.start() + push(coordinator: coordinator) default: guard let tabIndex = target.tabIndex else { fatalError("No settings tab index for this target.") @@ -189,6 +196,14 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { navigationRouter.push(segmentedViewController, animated: animated, popCompletion: nil) } } + + private func push(coordinator: Coordinator & Presentable, animated: Bool = true) { + coordinator.start() + self.add(childCoordinator: coordinator) + navigationRouter.push(coordinator, animated: animated) { + self.remove(childCoordinator: coordinator) + } + } } // MARK: - RoomInfoListCoordinatorDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift index c6e0c8887..6383d4fb5 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift @@ -25,6 +25,7 @@ enum RoomInfoListTarget: Equatable { case integrations case search case notifications + case pollHistory var tabIndex: UInt? { switch self { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index db245c111..fdb34304d 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -176,7 +176,7 @@ final class RoomInfoListViewController: UIViewController { } let rowPollHistory = Row(type: .default, icon: Asset.Images.pollHistory.image, text: VectorL10n.roomDetailsPolls, accessoryType: .disclosureIndicator) { - #warning("Add action") + self.viewModel.process(viewAction: .navigate(target: .pollHistory)) } let rowUploads = Row(type: .default, icon: Asset.Images.scrollup.image, text: VectorL10n.roomDetailsFiles, accessoryType: .disclosureIndicator) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift new file mode 100644 index 000000000..86b9149e7 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -0,0 +1,76 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit +import SwiftUI + +struct PollHistoryCoordinatorParameters { + let promptType: PollHistoryPromptType +} + +final class PollHistoryCoordinator: Coordinator, Presentable { + private let parameters: PollHistoryCoordinatorParameters + private let pollHistoryHostingController: UIViewController + private var pollHistoryViewModel: PollHistoryViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((PollHistoryViewModelResult) -> Void)? + + init(parameters: PollHistoryCoordinatorParameters) { + self.parameters = parameters + + let viewModel = PollHistoryViewModel(promptType: parameters.promptType) + let view = PollHistory(viewModel: viewModel.context) + pollHistoryViewModel = viewModel + pollHistoryHostingController = VectorHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryHostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[PollHistoryCoordinator] did start.") + pollHistoryViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[PollHistoryCoordinator] PollHistoryViewModel did complete with result: \(result).") + self.completion?(result) + } + } + + func toPresentable() -> UIViewController { + pollHistoryHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift new file mode 100644 index 000000000..37d400e65 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockPollHistoryScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case promptType(PollHistoryPromptType) + + /// The associated screen + var screenType: Any.Type { + PollHistory.self + } + + /// A list of screen state definitions + static var allCases: [MockPollHistoryScreenState] { + // Each of the presence statuses + PollHistoryPromptType.allCases.map(MockPollHistoryScreenState.promptType) + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let promptType: PollHistoryPromptType + switch self { + case .promptType(let type): + promptType = type + } + let viewModel = PollHistoryViewModel(promptType: promptType) + + // can simulate service and viewModel actions here if needs be. + + return ( + [promptType, viewModel], + AnyView(PollHistory(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift new file mode 100644 index 000000000..7895fbcb2 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -0,0 +1,67 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +enum PollHistoryPromptType { + case regular + case upgrade +} + +extension PollHistoryPromptType: Identifiable, CaseIterable { + var id: Self { self } + + var title: String { + switch self { + case .regular: + return VectorL10n.roomCreationMakePublicPromptTitle + case .upgrade: + return VectorL10n.roomDetailsHistorySectionPromptTitle + } + } + + var image: ImageAsset { + switch self { + case .regular: + return Asset.Images.appSymbol + case .upgrade: + return Asset.Images.keyVerificationSuccessShield + } + } +} + +// MARK: View model + +enum PollHistoryViewModelResult { + case accept + case cancel +} + +// MARK: View + +struct PollHistoryViewState: BindableState { + var promptType: PollHistoryPromptType + var count: Int +} + +enum PollHistoryViewAction { + case incrementCount + case decrementCount + case accept + case cancel +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift new file mode 100644 index 000000000..c7a78bbe5 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias PollHistoryViewModelType = StateStoreViewModel + +class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol { + var completion: ((PollHistoryViewModelResult) -> Void)? + + init(promptType: PollHistoryPromptType, initialCount: Int = 0) { + super.init(initialViewState: PollHistoryViewState(promptType: promptType, count: 0)) + } + + // MARK: - Public + + override func process(viewAction: PollHistoryViewAction) { + switch viewAction { + case .accept: + completion?(.accept) + case .cancel: + completion?(.cancel) + case .incrementCount: + state.count += 1 + case .decrementCount: + state.count -= 1 + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift new file mode 100644 index 000000000..d116c0254 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol PollHistoryViewModelProtocol { + var completion: ((PollHistoryViewModelResult) -> Void)? { get set } + var context: PollHistoryViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift new file mode 100644 index 000000000..3b123c95d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -0,0 +1,38 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +class PollHistoryUITests: MockScreenTestCase { + func testPollHistoryPromptRegular() { + let promptType = PollHistoryPromptType.regular + app.goToScreenWithIdentifier(MockPollHistoryScreenState.promptType(promptType).title) + + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + + func testPollHistoryPromptUpgrade() { + let promptType = PollHistoryPromptType.upgrade + app.goToScreenWithIdentifier(MockPollHistoryScreenState.promptType(promptType).title) + + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift new file mode 100644 index 000000000..a388d9823 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -0,0 +1,48 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import RiotSwiftUI + +class PollHistoryViewModelTests: XCTestCase { + private enum Constants { + static let counterInitialValue = 0 + } + + var viewModel: PollHistoryViewModelProtocol! + var context: PollHistoryViewModelType.Context! + + override func setUpWithError() throws { + viewModel = PollHistoryViewModel(promptType: .regular, initialCount: Constants.counterInitialValue) + context = viewModel.context + } + + func testInitialState() { + XCTAssertEqual(context.viewState.count, Constants.counterInitialValue) + } + + func testCounter() throws { + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 1) + + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 2) + + context.send(viewAction: .decrementCount) + XCTAssertEqual(context.viewState.count, 1) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift new file mode 100644 index 000000000..3ba8f1ed8 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -0,0 +1,102 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollHistory: View { + @Environment(\.theme) private var theme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + @ObservedObject var viewModel: PollHistoryViewModel.Context + + var body: some View { + GeometryReader { geometry in + VStack { + ScrollView(showsIndicators: false) { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) + } + + buttons + .padding(.horizontal, horizontalPadding) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + } + } + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) + } + + /// The main content of the view to be shown in a scroll view. + var mainContent: some View { + VStack(spacing: 36) { + Text(viewModel.viewState.promptType.title) + .font(theme.fonts.title1B) + .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier("title") + + Image(viewModel.viewState.promptType.image.name) + .resizable() + .scaledToFit() + .frame(width: 100) + .foregroundColor(theme.colors.accent) + + HStack { + Text("Counter: \(viewModel.viewState.count)") + .foregroundColor(theme.colors.primaryContent) + + Button("-") { + viewModel.send(viewAction: .decrementCount) + } + + Button("+") { + viewModel.send(viewAction: .incrementCount) + } + } + .font(theme.fonts.title3) + } + } + + /// The action buttons shown at the bottom of the view. + var buttons: some View { + VStack { + Button { viewModel.send(viewAction: .accept) } label: { + Text("Accept") + .font(theme.fonts.bodySB) + } + .buttonStyle(PrimaryActionButtonStyle()) + + Button { viewModel.send(viewAction: .cancel) } label: { + Text("Cancel") + .font(theme.fonts.body) + .padding(.vertical, 12) + } + } + } +} + +// MARK: - Previews + +struct PollHistory_Previews: PreviewProvider { + static let stateRenderer = MockPollHistoryScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} From e7adb92b77047d79e18783fe65e25c2b0e045226 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 12:06:15 +0100 Subject: [PATCH 028/468] Cleanup scaffolding --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 6 +- .../Coordinator/PollHistoryCoordinator.swift | 10 ++- .../MockPollHistoryScreenState.swift | 23 +++--- .../Room/PollHistory/PollHistoryModels.swift | 57 +++++---------- .../PollHistory/PollHistoryViewModel.swift | 15 +--- .../Room/PollHistory/View/PollHistory.swift | 70 +------------------ 6 files changed, 39 insertions(+), 142 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 6da39162a..774e60f1e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -157,10 +157,6 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { return coordinator } - private func pollHistoryCoordinator() -> PollHistoryCoordinator { - return PollHistoryCoordinator(parameters: .init(promptType: .regular)) - } - private func showRoomDetails(with target: RoomInfoListTarget, animated: Bool) { switch target { case .integrations: @@ -180,7 +176,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { coordinator.start() push(coordinator: coordinator) case .pollHistory: - let coordinator = pollHistoryCoordinator() + let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active)) coordinator.start() push(coordinator: coordinator) default: diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 86b9149e7..336f034eb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -18,7 +18,7 @@ import CommonKit import SwiftUI struct PollHistoryCoordinatorParameters { - let promptType: PollHistoryPromptType + let mode: PollHistoryMode } final class PollHistoryCoordinator: Coordinator, Presentable { @@ -31,12 +31,12 @@ final class PollHistoryCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - var completion: ((PollHistoryViewModelResult) -> Void)? + var completion: (() -> Void)? init(parameters: PollHistoryCoordinatorParameters) { self.parameters = parameters - let viewModel = PollHistoryViewModel(promptType: parameters.promptType) + let viewModel = PollHistoryViewModel(mode: parameters.mode) let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) @@ -49,9 +49,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[PollHistoryCoordinator] did start.") pollHistoryViewModel.completion = { [weak self] result in - guard let self = self else { return } - MXLog.debug("[PollHistoryCoordinator] PollHistoryViewModel did complete with result: \(result).") - self.completion?(result) + self?.completion?() } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 37d400e65..778cbdf12 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -23,32 +23,31 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { // A case for each state you want to represent // with specific, minimal associated data that will allow you // mock that screen. - case promptType(PollHistoryPromptType) + case active + case past /// The associated screen var screenType: Any.Type { PollHistory.self } - /// A list of screen state definitions - static var allCases: [MockPollHistoryScreenState] { - // Each of the presence statuses - PollHistoryPromptType.allCases.map(MockPollHistoryScreenState.promptType) - } - /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let promptType: PollHistoryPromptType + let pollHistoryMode: PollHistoryMode + switch self { - case .promptType(let type): - promptType = type + case .active: + pollHistoryMode = .active + case .past: + pollHistoryMode = .past } - let viewModel = PollHistoryViewModel(promptType: promptType) + + let viewModel = PollHistoryViewModel(mode: pollHistoryMode) // can simulate service and viewModel actions here if needs be. return ( - [promptType, viewModel], + [pollHistoryMode, viewModel], AnyView(PollHistory(viewModel: viewModel.context) .addDependency(MockAvatarService.example)) ) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 7895fbcb2..2f0a0437f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -14,54 +14,31 @@ // limitations under the License. // -import Foundation - -// MARK: - Coordinator - -enum PollHistoryPromptType { - case regular - case upgrade -} - -extension PollHistoryPromptType: Identifiable, CaseIterable { - var id: Self { self } - - var title: String { - switch self { - case .regular: - return VectorL10n.roomCreationMakePublicPromptTitle - case .upgrade: - return VectorL10n.roomDetailsHistorySectionPromptTitle - } - } - - var image: ImageAsset { - switch self { - case .regular: - return Asset.Images.appSymbol - case .upgrade: - return Asset.Images.keyVerificationSuccessShield - } - } -} - // MARK: View model -enum PollHistoryViewModelResult { - case accept - case cancel +enum PollHistoryViewModelResult: Equatable { + #warning("e.g. show poll detail") } // MARK: View +enum PollHistoryMode { + case active + case past +} + +struct PollHistoryViewBindings { + var mode: PollHistoryMode +} + struct PollHistoryViewState: BindableState { - var promptType: PollHistoryPromptType - var count: Int + init(mode: PollHistoryMode) { + self.bindings = .init(mode: mode) + } + + var bindings: PollHistoryViewBindings } enum PollHistoryViewAction { - case incrementCount - case decrementCount - case accept - case cancel + #warning("e.g. show poll detail") } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index c7a78bbe5..6ce689d10 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -21,22 +21,13 @@ typealias PollHistoryViewModelType = StateStoreViewModel Void)? - init(promptType: PollHistoryPromptType, initialCount: Int = 0) { - super.init(initialViewState: PollHistoryViewState(promptType: promptType, count: 0)) + init(mode: PollHistoryMode) { + super.init(initialViewState: PollHistoryViewState(mode: mode)) } // MARK: - Public override func process(viewAction: PollHistoryViewAction) { - switch viewAction { - case .accept: - completion?(.accept) - case .cancel: - completion?(.cancel) - case .incrementCount: - state.count += 1 - case .decrementCount: - state.count -= 1 - } + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 3ba8f1ed8..9fe93d4a5 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -18,77 +18,13 @@ import SwiftUI struct PollHistory: View { @Environment(\.theme) private var theme - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - - private var horizontalPadding: CGFloat { - horizontalSizeClass == .regular ? 50 : 16 - } @ObservedObject var viewModel: PollHistoryViewModel.Context var body: some View { - GeometryReader { geometry in - VStack { - ScrollView(showsIndicators: false) { - mainContent - .padding(.top, 50) - .padding(.horizontal, horizontalPadding) - } - - buttons - .padding(.horizontal, horizontalPadding) - .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) - } - } - .background(theme.colors.background.ignoresSafeArea()) - .accentColor(theme.colors.accent) - } - - /// The main content of the view to be shown in a scroll view. - var mainContent: some View { - VStack(spacing: 36) { - Text(viewModel.viewState.promptType.title) - .font(theme.fonts.title1B) - .foregroundColor(theme.colors.primaryContent) - .accessibilityIdentifier("title") - - Image(viewModel.viewState.promptType.image.name) - .resizable() - .scaledToFit() - .frame(width: 100) - .foregroundColor(theme.colors.accent) - - HStack { - Text("Counter: \(viewModel.viewState.count)") - .foregroundColor(theme.colors.primaryContent) - - Button("-") { - viewModel.send(viewAction: .decrementCount) - } - - Button("+") { - viewModel.send(viewAction: .incrementCount) - } - } - .font(theme.fonts.title3) - } - } - - /// The action buttons shown at the bottom of the view. - var buttons: some View { - VStack { - Button { viewModel.send(viewAction: .accept) } label: { - Text("Accept") - .font(theme.fonts.bodySB) - } - .buttonStyle(PrimaryActionButtonStyle()) - - Button { viewModel.send(viewAction: .cancel) } label: { - Text("Cancel") - .font(theme.fonts.body) - .padding(.vertical, 12) - } - } + Text("Hello world") + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) } } From 126d8a19b205ae36effe5fef670ae99834ea8097 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 12:22:47 +0100 Subject: [PATCH 029/468] Add UI skeleton --- .../Room/PollHistory/View/PollHistory.swift | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 9fe93d4a5..49bf96209 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -21,10 +21,36 @@ struct PollHistory: View { @ObservedObject var viewModel: PollHistoryViewModel.Context + var bindings: PollHistoryViewBindings { + viewModel.viewState.bindings + } + var body: some View { - Text("Hello world") - .background(theme.colors.background.ignoresSafeArea()) - .accentColor(theme.colors.accent) + VStack { + Picker("abc", selection: .constant(PollHistoryMode.active)) { + Text("Active Polls") + .tag(PollHistoryMode.active) + + Text("Past Polls") + .tag(PollHistoryMode.past) + } + .pickerStyle(SegmentedPickerStyle()) + + ScrollView { + ForEach(0..<10) { index in + Text("Active poll number: \(index)") + } + + Button { + #warning("handle action") + } label: { + Text("Load more polls") + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) } } From ee4e1959c28a98852f57406ef22262b687b34fdd Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 13:35:12 +0100 Subject: [PATCH 030/468] removing some changelog entries that are similiar --- changelog.d/2292.change | 1 - changelog.d/2419.bugfix | 1 - changelog.d/5437.bugfix | 1 - changelog.d/7263.bugfix | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 changelog.d/2292.change delete mode 100644 changelog.d/2419.bugfix delete mode 100644 changelog.d/5437.bugfix diff --git a/changelog.d/2292.change b/changelog.d/2292.change deleted file mode 100644 index b1d61e979..000000000 --- a/changelog.d/2292.change +++ /dev/null @@ -1 +0,0 @@ -Links are now in blue like on web and Android. \ No newline at end of file diff --git a/changelog.d/2419.bugfix b/changelog.d/2419.bugfix deleted file mode 100644 index 900d2135c..000000000 --- a/changelog.d/2419.bugfix +++ /dev/null @@ -1 +0,0 @@ -Hyperlinks are now blue and should now be distinguishable from unsent messages in encrypted rooms. \ No newline at end of file diff --git a/changelog.d/5437.bugfix b/changelog.d/5437.bugfix deleted file mode 100644 index 59c6185c5..000000000 --- a/changelog.d/5437.bugfix +++ /dev/null @@ -1 +0,0 @@ -HTML links should now be displayed in default system blue. \ No newline at end of file diff --git a/changelog.d/7263.bugfix b/changelog.d/7263.bugfix index 061f8a644..425e2dd95 100644 --- a/changelog.d/7263.bugfix +++ b/changelog.d/7263.bugfix @@ -1 +1 @@ -Timeline's tag and hyperlinks match now in colour Android and Web. \ No newline at end of file +Timeline's links and hyperlinks match now the blue colour of Android and Web. \ No newline at end of file From e99d363ae01fc9eda4be1deafebd1f6501d24d50 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 14:33:50 +0100 Subject: [PATCH 031/468] adding an underline to the Re-request encryption keys message --- Riot/Utils/EventFormatter.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index eb793b811..643696b2b 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -359,7 +359,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; attributes:@{ NSLinkAttributeName: linkActionString, NSForegroundColorAttributeName: self.sendingTextColor, - NSFontAttributeName: self.encryptedMessagesTextFont + NSFontAttributeName: self.encryptedMessagesTextFont, + NSUnderlineStyleAttributeName: [NSNumber numberWithInt:NSUnderlineStyleSingle] }]]; [attributedStringWithRerequestMessage appendAttributedString: From c8bf27eb4096fc5c8b82ee0dfc4d9e3d67607465 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 15:45:16 +0100 Subject: [PATCH 032/468] Add SegmentedPicker --- .../Room/PollHistory/View/PollHistory.swift | 42 ++++++++----- .../PollHistory/View/SegmentedPicker.swift | 61 +++++++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 49bf96209..4e76539b5 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -27,28 +27,38 @@ struct PollHistory: View { var body: some View { VStack { - Picker("abc", selection: .constant(PollHistoryMode.active)) { - Text("Active Polls") - .tag(PollHistoryMode.active) - - Text("Past Polls") - .tag(PollHistoryMode.past) + HStack { + SegmentedPicker( + segments: [ + ("Active Polls", PollHistoryMode.active), + ("Past Pools", PollHistoryMode.past) + ], + selection: $viewModel.mode, + interSegmentSpacing: 14 + ) + Spacer() } - .pickerStyle(SegmentedPickerStyle()) ScrollView { - ForEach(0..<10) { index in - Text("Active poll number: \(index)") - } - - Button { - #warning("handle action") - } label: { - Text("Load more polls") + LazyVStack(spacing: 32) { + ForEach(0..<10) { index in + Text("Active poll number: \(index)") + } + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + #warning("handle action") + } label: { + Text("Load more polls") + } + .frame(maxWidth: .infinity, alignment: .leading) } + .padding(.top, 32) } - .frame(maxWidth: .infinity, maxHeight: .infinity) } + .padding(.horizontal, 16) + .padding(.vertical, 32) + .frame(maxWidth: .infinity, maxHeight: .infinity) .background(theme.colors.background.ignoresSafeArea()) .accentColor(theme.colors.accent) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift new file mode 100644 index 000000000..d60388ad6 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -0,0 +1,61 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SegmentedPicker: View { + private let segments: [(String, Tag)] + private let selection: Binding + private let interSegmentSpacing: CGFloat + + @Environment(\.theme) private var theme + + init(segments: [(String, Tag)], selection: Binding, interSegmentSpacing: CGFloat) { + self.segments = segments + self.selection = selection + self.interSegmentSpacing = interSegmentSpacing + } + + var body: some View { + HStack(spacing: interSegmentSpacing) { + ForEach(segments, id: \.1) { text, tag in + let isSelectedSegment = tag == selection.wrappedValue + + Button { + selection.wrappedValue = tag + } label: { + Text(text) + .font(isSelectedSegment ? theme.fonts.headline : theme.fonts.body) + .underline(isSelectedSegment) + } + .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent) + } + } + } +} + +struct SegmentedPicker_Previews: PreviewProvider { + static var previews: some View { + SegmentedPicker( + segments: [ + ("Segment 1", "1"), + ("Segment 2", "2") + ], + selection: .constant("1"), + interSegmentSpacing: 14 + ) + } +} From 18cb71def71d3ac1359929500723e47175943c87 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 12 Jan 2023 15:55:06 +0100 Subject: [PATCH 033/468] Remove strong references on audio players used for voicebroadcast --- .../Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 04a4ac7ec..24ff08d0d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -94,7 +94,7 @@ import MediaPlayer private override init() { audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) - nowPlayingInfoDelegates = NSMapTable(valueOptions: .weakMemory) + nowPlayingInfoDelegates = NSMapTable(keyOptions: .weakMemory, valueOptions: .weakMemory) activeAudioPlayers = Set() super.init() From 95e8c596d74bb09b2d8611e37b7cc656ce9a18b1 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 12 Jan 2023 16:00:43 +0100 Subject: [PATCH 034/468] Fix Towncrier file --- changelog.d/pr-7257.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/pr-7257.bugfix b/changelog.d/pr-7257.bugfix index c83bd783d..8a41b21c8 100644 --- a/changelog.d/pr-7257.bugfix +++ b/changelog.d/pr-7257.bugfix @@ -1 +1 @@ -Voice Broacast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. +Voice Broadcast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. From dd28c01f9cb49e233ab94de73d943d82694e3887 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 16:20:38 +0100 Subject: [PATCH 035/468] added the "links" color from figma --- DesignKit/Source/ColorValues.swift | 2 ++ DesignKit/Source/Colors.swift | 4 ++++ DesignKit/Source/ColorsSwiftUI.swift | 7 +++++-- DesignKit/Source/ColorsUIkit.swift | 3 +++ DesignKit/Variants/Colors/Dark/DarkColors.swift | 1 + DesignKit/Variants/Colors/Light/LightColors.swift | 1 + 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift index 338d1cfe8..4e967ab05 100644 --- a/DesignKit/Source/ColorValues.swift +++ b/DesignKit/Source/ColorValues.swift @@ -48,5 +48,7 @@ public struct ColorValues: Colors { public let ems: UIColor + public let links: UIColor + public let namesAndAvatars: [UIColor] } diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift index bf3e9abd3..bea9b0706 100644 --- a/DesignKit/Source/Colors.swift +++ b/DesignKit/Source/Colors.swift @@ -67,6 +67,10 @@ public protocol Colors { /// Global color: The EMS brand's purple colour. var ems: ColorType { get } + /// - Links + /// - Hyperlinks + var links: ColorType { get } + /// - Names in chat timeline /// - Avatars default states that include first name letter var namesAndAvatars: [ColorType] { get } diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift index ea3ca6779..bb25d025f 100644 --- a/DesignKit/Source/ColorsSwiftUI.swift +++ b/DesignKit/Source/ColorsSwiftUI.swift @@ -21,7 +21,7 @@ import SwiftUI Struct for holding colors for use in SwiftUI. */ public struct ColorSwiftUI: Colors { - + public let accent: Color public let alert: Color @@ -48,8 +48,10 @@ public struct ColorSwiftUI: Colors { public var ems: Color - public let namesAndAvatars: [Color] + public let links: Color + public let namesAndAvatars: [Color] + init(values: ColorValues) { accent = Color(values.accent) alert = Color(values.alert) @@ -64,6 +66,7 @@ public struct ColorSwiftUI: Colors { navigation = Color(values.navigation) background = Color(values.background) ems = Color(values.ems) + links = Color(values.links) namesAndAvatars = values.namesAndAvatars.map({ Color($0) }) } } diff --git a/DesignKit/Source/ColorsUIkit.swift b/DesignKit/Source/ColorsUIkit.swift index 3add385c3..5ca20ab0b 100644 --- a/DesignKit/Source/ColorsUIkit.swift +++ b/DesignKit/Source/ColorsUIkit.swift @@ -45,6 +45,8 @@ import UIKit public let navigation: UIColor public let background: UIColor + + public let links: UIColor public let namesAndAvatars: [UIColor] @@ -61,6 +63,7 @@ import UIKit tile = values.tile navigation = values.navigation background = values.background + links = values.links namesAndAvatars = values.namesAndAvatars } } diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift index 88bd12ff3..21394475c 100644 --- a/DesignKit/Variants/Colors/Dark/DarkColors.swift +++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift @@ -34,6 +34,7 @@ public class DarkColors { navigation: UIColor(rgb:0x21262C), background: UIColor(rgb:0x15191E), ems: UIColor(rgb: 0x7E69FF), + links: UIColor(rgb: 0x0086E6), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift index 93cb3eadb..f8fa0e8e3 100644 --- a/DesignKit/Variants/Colors/Light/LightColors.swift +++ b/DesignKit/Variants/Colors/Light/LightColors.swift @@ -35,6 +35,7 @@ public class LightColors { navigation: UIColor(rgb:0xF4F6FA), background: UIColor(rgb:0xFFFFFF), ems: UIColor(rgb: 0x7E69FF), + links: UIColor(rgb: 0x0086E6), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), From 30fcb3a1dee8fbd038ad791b370e64470eddb19e Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 16:45:43 +0100 Subject: [PATCH 036/468] added the links color wherever possible --- .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.h | 6 ++++++ .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 3 ++- .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 2 ++ Riot/Utils/EventFormatter.m | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h index 776b79927..999da7034 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h @@ -366,6 +366,12 @@ typedef enum : NSUInteger { */ @property (nonatomic) UIColor *sendingTextColor; +/** + Color used to display links and hyperlinks contentt. + Default is [UIColor linkColor]. + */ +@property (nonatomic) UIColor *linksColor; + /** Color used to display error text. Default is red. diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 70c36ecd5..cee8dccec 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -89,6 +89,7 @@ static NSString *const kHTMLATagRegexPattern = @"( _encryptingTextColor = [UIColor lightGrayColor]; _sendingTextColor = [UIColor lightGrayColor]; _errorTextColor = [UIColor redColor]; + _linksColor = [UIColor linkColor]; _htmlBlockquoteBorderColor = [MXKTools colorWithRGBValue:0xDDDDDD]; _defaultTextFont = [UIFont systemFontOfSize:14]; @@ -1749,7 +1750,7 @@ static NSString *const kHTMLATagRegexPattern = @"( if (url.URL) { [str addAttribute:NSLinkAttributeName value:url.URL range:matchRange]; - [str addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:matchRange]; + [str addAttribute:NSForegroundColorAttributeName value:self.linksColor range:matchRange]; } } } diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 0fdfb1670..013602843 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -44,6 +44,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel( textColor: ThemeService.shared().theme.colors.primaryContent, + linkColor: ThemeService.shared().theme.colors.links, codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor ) private var viewModel: ComposerViewModelProtocol! @@ -298,6 +299,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background wysiwygViewModel.textColor = theme.colors.primaryContent + wysiwygViewModel.linkColor = theme.colors.links wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 643696b2b..079a00f77 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -486,6 +486,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; self.bingTextColor = ThemeService.shared.theme.noticeColor; self.encryptingTextColor = ThemeService.shared.theme.textPrimaryColor; self.sendingTextColor = ThemeService.shared.theme.textPrimaryColor; + self.linksColor = ThemeService.shared.theme.colors.links; self.errorTextColor = ThemeService.shared.theme.textPrimaryColor; self.showEditionMention = YES; self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor; From 3d8781889c149cbf5e24cb8d1783ef47cd573ac3 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 16:52:11 +0100 Subject: [PATCH 037/468] Add PollListItem --- .../Room/PollHistory/View/PollHistory.swift | 8 ++- .../Room/PollHistory/View/PollListItem.swift | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 4e76539b5..f3311b73f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -38,11 +38,12 @@ struct PollHistory: View { ) Spacer() } + .padding(.horizontal, 16) ScrollView { LazyVStack(spacing: 32) { ForEach(0..<10) { index in - Text("Active poll number: \(index)") + PollListItem(data: .init(startDate: Date(), question: "Poll question number \(index)")) } .frame(maxWidth: .infinity, alignment: .leading) @@ -53,11 +54,11 @@ struct PollHistory: View { } .frame(maxWidth: .infinity, alignment: .leading) } + .padding(.horizontal, 16) .padding(.top, 32) } } - .padding(.horizontal, 16) - .padding(.vertical, 32) + .padding(.top, 32) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(theme.colors.background.ignoresSafeArea()) .accentColor(theme.colors.accent) @@ -68,6 +69,7 @@ struct PollHistory: View { struct PollHistory_Previews: PreviewProvider { static let stateRenderer = MockPollHistoryScreenState.stateRenderer + static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift new file mode 100644 index 000000000..984cb2b68 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -0,0 +1,56 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollListData { + let startDate: Date + let question: String +} + +struct PollListItem: View { + @Environment(\.theme) private var theme + + private let data: PollListData + + init(data: PollListData) { + self.data = data + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(data.startDate.description) + .foregroundColor(theme.colors.tertiaryContent) + .font(theme.fonts.caption1) + + HStack(spacing: 8) { + Image(uiImage: Asset.Images.pollHistory.image) + .resizable() + .frame(width: 16, height: 16) + + Text(data.question) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + } + } + } +} + +struct PollListItem_Previews: PreviewProvider { + static var previews: some View { + PollListItem(data: .init(startDate: .init(), question: "Did you like this poll?")) + } +} From 7b7f1c8e16f675fb396758570a4c8ea5694287c9 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 17:07:02 +0100 Subject: [PATCH 038/468] removing useless ios 13 checks --- Riot/Managers/Theme/Themes/DarkTheme.swift | 10 ++-------- Riot/Managers/Theme/Themes/DefaultTheme.swift | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 9dbba85c7..269b387fa 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -168,14 +168,8 @@ class DarkTheme: NSObject, Theme { searchBar.backgroundImage = UIImage() // Remove top and bottom shadow searchBar.tintColor = self.tintColor - if #available(iOS 13.0, *) { - searchBar.searchTextField.backgroundColor = self.searchBackgroundColor - searchBar.searchTextField.textColor = self.searchPlaceholderColor - } else { - if let searchBarTextField = searchBar.vc_searchTextField { - searchBarTextField.textColor = self.searchPlaceholderColor - } - } + searchBar.searchTextField.backgroundColor = self.searchBackgroundColor + searchBar.searchTextField.textColor = self.searchPlaceholderColor } func applyStyle(onTextField texField: UITextField) { diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index d5d309bf3..97c701975 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -177,14 +177,8 @@ class DefaultTheme: NSObject, Theme { return } - if #available(iOS 13.0, *) { - searchBar.searchTextField.backgroundColor = self.searchBackgroundColor - searchBar.searchTextField.textColor = self.searchPlaceholderColor - } else { - if let searchBarTextField = searchBar.vc_searchTextField { - searchBarTextField.textColor = self.searchPlaceholderColor - } - } + searchBar.searchTextField.backgroundColor = self.searchBackgroundColor + searchBar.searchTextField.textColor = self.searchPlaceholderColor } func applyStyle(onTextField texField: UITextField) { From f64fdee444665604f41910938a44eee47289ba6f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 17:09:44 +0100 Subject: [PATCH 039/468] Add localizations --- Riot/Assets/en.lproj/Vector.strings | 6 ++++++ Riot/Generated/Strings.swift | 12 ++++++++++++ .../Room/PollHistory/PollHistoryModels.swift | 2 +- .../Room/PollHistory/View/PollHistory.swift | 17 +++++++++++++---- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b87d35a78..1650228d4 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2292,6 +2292,12 @@ Tap the + to start adding people."; "space_detail_nav_title" = "Space detail"; "space_invite_nav_title" = "Space invite"; +// MARK: - Polls history + +"poll_history_title" = "Poll history"; +"poll_history_active_segment_title" = "Active polls"; +"poll_history_past_segment_title" = "Past polls"; + // MARK: - Polls "poll_edit_form_create_poll" = "Create poll"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 36fd6b67a..dea531fc9 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4839,6 +4839,18 @@ public class VectorL10n: NSObject { public static var pollEditFormUpdateFailureTitle: String { return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title") } + /// Active polls + public static var pollHistoryActiveSegmentTitle: String { + return VectorL10n.tr("Vector", "poll_history_active_segment_title") + } + /// Past polls + public static var pollHistoryPastSegmentTitle: String { + return VectorL10n.tr("Vector", "poll_history_past_segment_title") + } + /// Poll history + public static var pollHistoryTitle: String { + return VectorL10n.tr("Vector", "poll_history_title") + } /// Due to decryption errors, some votes may not be counted public static var pollTimelineDecryptionError: String { return VectorL10n.tr("Vector", "poll_timeline_decryption_error") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 2f0a0437f..a1bf4f343 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -22,7 +22,7 @@ enum PollHistoryViewModelResult: Equatable { // MARK: View -enum PollHistoryMode { +enum PollHistoryMode: CaseIterable { case active case past } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index f3311b73f..6d46c05e9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -29,10 +29,7 @@ struct PollHistory: View { VStack { HStack { SegmentedPicker( - segments: [ - ("Active Polls", PollHistoryMode.active), - ("Past Pools", PollHistoryMode.past) - ], + segments: PollHistoryMode.allCases.map { ($0.segmentTitle, $0) }, selection: $viewModel.mode, interSegmentSpacing: 14 ) @@ -62,6 +59,18 @@ struct PollHistory: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(theme.colors.background.ignoresSafeArea()) .accentColor(theme.colors.accent) + .navigationTitle(VectorL10n.pollHistoryTitle) + } +} + +extension PollHistoryMode { + var segmentTitle: String { + switch self { + case .active: + return VectorL10n.pollHistoryActiveSegmentTitle + case .past: + return VectorL10n.pollHistoryPastSegmentTitle + } } } From b2b2771ca8783908d539f046e0dd8bf447cbc96b Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 12 Jan 2023 17:54:44 +0100 Subject: [PATCH 040/468] replaced UIColor link with ThemeService links color everywhere it was used, and included the ThemeService in NSE and SiriIntents --- .../MatrixKit/Utils/EventFormatter/HTMLFormatter.swift | 2 +- Riot/Modules/MatrixKit/Utils/MXKTools.m | 6 +++--- RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h | 2 ++ RiotNSE/target.yml | 2 ++ SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h | 1 + SiriIntents/target.yml | 2 ++ 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift index e65f07ce1..6c7e43a90 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift @@ -51,7 +51,7 @@ class HTMLFormatter: NSObject { DTDefaultFontName: font.fontName, DTDefaultFontSize: font.pointSize, DTDefaultLinkDecoration: false, - DTDefaultLinkColor: UIColor.link, + DTDefaultLinkColor: ThemeService.shared().theme.colors.links, DTWillFlushBlockCallBack: sanitizeCallback ] options.merge(extraOptions) { (_, new) in new } diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index fdb7f51f8..9ba006a89 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -1052,7 +1052,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo NSURLComponents *url = [[NSURLComponents new] initWithURL:matchUrl resolvingAgainstBaseURL:NO]; if (url.URL) { - [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:matchRange]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:matchRange]; } } } @@ -1103,7 +1103,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length) { // but before we set the right color - [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:linkMatch.range]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:linkMatch.range]; hasAlreadyLink = YES; break; } @@ -1118,7 +1118,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo NSString *link = [mutableAttributedString.string substringWithRange:match.range]; link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; [mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range]; - [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor linkColor] range:match.range]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:match.range]; } }]; } diff --git a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h index 6409af92f..5db20c745 100644 --- a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h +++ b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h @@ -23,4 +23,6 @@ #import "BuildInfo.h" +#import "ThemeService.h" + #endif /* RiotNSE_Bridging_Header_h */ diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index 415f8447a..ae27022c3 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -78,3 +78,5 @@ targets: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK + - path: ../Riot/Managers/Theme + - path: ../Riot/Categories/UIColor.swift diff --git a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h index ca6d81962..01aeb20d1 100644 --- a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h +++ b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h @@ -16,3 +16,4 @@ #import "MatrixKit-Bridging-Header.h" #import "BuildInfo.h" +#import "ThemeService.h" diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 5b63a95b3..324497cf3 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -66,3 +66,5 @@ targets: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK + - path: ../Riot/Managers/Theme + - path: ../Riot/Categories/UIColor.swift From 912f2a64e471e6b5d501753eb581333cd7036d40 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 17:59:23 +0100 Subject: [PATCH 041/468] Add poll history service --- .../Coordinator/PollHistoryCoordinator.swift | 3 +- .../MockPollHistoryScreenState.swift | 2 +- .../Room/PollHistory/PollHistoryModels.swift | 3 +- .../PollHistory/PollHistoryViewModel.swift | 33 +++++++++++++++-- .../MatrixSDK/PollHistoryService.swift | 24 ++++++++++++ .../Service/Mock/MockPollHistoryService.swift | 27 ++++++++++++++ .../Service/PollHistoryServiceProtocol.swift | 19 ++++++++++ .../Room/PollHistory/View/PollHistory.swift | 11 ++++-- .../Room/PollHistory/View/PollListItem.swift | 37 ++++++++++++++----- 9 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 336f034eb..0cdf3629a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -36,7 +36,8 @@ final class PollHistoryCoordinator: Coordinator, Presentable { init(parameters: PollHistoryCoordinatorParameters) { self.parameters = parameters - let viewModel = PollHistoryViewModel(mode: parameters.mode) + #warning("replace with the real service after that it's done") + let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: MockPollHistoryService()) let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 778cbdf12..e85384c4b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -42,7 +42,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollHistoryMode = .past } - let viewModel = PollHistoryViewModel(mode: pollHistoryMode) + let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: MockPollHistoryService()) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index a1bf4f343..c8960add0 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -37,8 +37,9 @@ struct PollHistoryViewState: BindableState { } var bindings: PollHistoryViewBindings + var polls: [PollListData] = [] } enum PollHistoryViewAction { - #warning("e.g. show poll detail") + case viewAppeared } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 6ce689d10..0addb8ef9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -18,16 +18,43 @@ import SwiftUI typealias PollHistoryViewModelType = StateStoreViewModel -class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol { +final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol { + private let pollService: PollHistoryServiceProtocol + private var fetchingTask: Task? { + didSet { + oldValue?.cancel() + } + } + var completion: ((PollHistoryViewModelResult) -> Void)? - init(mode: PollHistoryMode) { + init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) { + self.pollService = pollService super.init(initialViewState: PollHistoryViewState(mode: mode)) } // MARK: - Public override func process(viewAction: PollHistoryViewAction) { - + switch viewAction { + case .viewAppeared: + fetchingTask = fetchPolls() + } + } +} + +private extension PollHistoryViewModel { + func fetchPolls() -> Task { + Task { + let polls = try await pollService.fetchHistory() + + guard Task.isCancelled == false else { + return + } + + await MainActor.run { + state.polls = polls + } + } } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift new file mode 100644 index 000000000..a2dcf256a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -0,0 +1,24 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import MatrixSDK +import Foundation + +final class PollHistoryService: PollHistoryServiceProtocol { + func fetchHistory() async throws -> [PollListData] { + [] + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift new file mode 100644 index 000000000..62015b8a3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -0,0 +1,27 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +final class MockPollHistoryService: PollHistoryServiceProtocol { + func fetchHistory() async throws -> [PollListData] { + (1..<10) + .map { index in + PollListData(startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), question: "Do you like the poll number \(index)?") + } + .sorted { poll1, poll2 in + poll1.startDate > poll2.startDate + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift new file mode 100644 index 000000000..4bb9b43b5 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -0,0 +1,19 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +protocol PollHistoryServiceProtocol { + func fetchHistory() async throws -> [PollListData] +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 6d46c05e9..9de37b9c0 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -39,8 +39,10 @@ struct PollHistory: View { ScrollView { LazyVStack(spacing: 32) { - ForEach(0..<10) { index in - PollListItem(data: .init(startDate: Date(), question: "Poll question number \(index)")) + let enumeratedPolls = Array(viewModel.viewState.polls.enumerated()) + + ForEach(enumeratedPolls, id: \.offset) { _, pollData in + PollListItem(pollData: pollData) } .frame(maxWidth: .infinity, alignment: .leading) @@ -60,10 +62,13 @@ struct PollHistory: View { .background(theme.colors.background.ignoresSafeArea()) .accentColor(theme.colors.accent) .navigationTitle(VectorL10n.pollHistoryTitle) + .onAppear { + viewModel.send(viewAction: .viewAppeared) + } } } -extension PollHistoryMode { +private extension PollHistoryMode { var segmentTitle: String { switch self { case .active: diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 984cb2b68..dff98ba0a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -24,33 +24,52 @@ struct PollListData { struct PollListItem: View { @Environment(\.theme) private var theme - private let data: PollListData + private let pollData: PollListData - init(data: PollListData) { - self.data = data + init(pollData: PollListData) { + self.pollData = pollData } var body: some View { VStack(alignment: .leading, spacing: 12) { - Text(data.startDate.description) + Text(pollData.formattedDate) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) - HStack(spacing: 8) { + HStack(alignment: .firstTextBaseline, spacing: 8) { Image(uiImage: Asset.Images.pollHistory.image) .resizable() .frame(width: 16, height: 16) - Text(data.question) + Text(pollData.question) .foregroundColor(theme.colors.primaryContent) .font(theme.fonts.body) + .lineLimit(2) } } } } -struct PollListItem_Previews: PreviewProvider { - static var previews: some View { - PollListItem(data: .init(startDate: .init(), question: "Did you like this poll?")) +private extension PollListData { + var formattedDate: String { + DateFormatter.shortDateFormatter.string(from: startDate) + } +} + +private extension DateFormatter { + static let shortDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.dateStyle = .short + formatter.timeZone = .init(identifier: "UTC") + return formatter + }() +} + +// MARK: - Previews + +struct PollListItem_Previews: PreviewProvider { + static var previews: some View { + PollListItem(pollData: .init(startDate: .init(), question: "Did you like this poll?")) } } From 977013937ab0f9438d723deb80e9df3c48d8668e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 12 Jan 2023 18:25:06 +0100 Subject: [PATCH 042/468] Handle empty poll list case --- Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 8 +++ .../MockPollHistoryScreenState.swift | 11 +++- .../Service/Mock/MockPollHistoryService.swift | 16 +++--- .../Room/PollHistory/View/PollHistory.swift | 52 ++++++++++++------- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1650228d4..62f25672c 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2297,6 +2297,8 @@ Tap the + to start adding people."; "poll_history_title" = "Poll history"; "poll_history_active_segment_title" = "Active polls"; "poll_history_past_segment_title" = "Past polls"; +"poll_history_no_active_poll_text" = "There are no active polls in this room"; +"poll_history_no_past_poll_text" = "There are no past polls in this room"; // MARK: - Polls diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index dea531fc9..e0be60cee 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4843,6 +4843,14 @@ public class VectorL10n: NSObject { public static var pollHistoryActiveSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_active_segment_title") } + /// There are no active polls in this room + public static var pollHistoryNoActivePollText: String { + return VectorL10n.tr("Vector", "poll_history_no_active_poll_text") + } + /// There are no past polls in this room + public static var pollHistoryNoPastPollText: String { + return VectorL10n.tr("Vector", "poll_history_no_past_poll_text") + } /// Past polls public static var pollHistoryPastSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_past_segment_title") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index e85384c4b..08b33293b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -25,6 +25,8 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { // mock that screen. case active case past + case activeEmpty + case pastEmpty /// The associated screen var screenType: Any.Type { @@ -34,15 +36,22 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { let pollHistoryMode: PollHistoryMode + let pollService = MockPollHistoryService() switch self { case .active: pollHistoryMode = .active case .past: pollHistoryMode = .past + case .activeEmpty: + pollHistoryMode = .active + pollService.pollListData = [] + case .pastEmpty: + pollHistoryMode = .past + pollService.pollListData = [] } - let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: MockPollHistoryService()) + let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 62015b8a3..67d312181 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -15,13 +15,15 @@ // final class MockPollHistoryService: PollHistoryServiceProtocol { + var pollListData: [PollListData] = (1..<10) + .map { index in + PollListData(startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), question: "Do you like the poll number \(index)?") + } + .sorted { poll1, poll2 in + poll1.startDate > poll2.startDate + } + func fetchHistory() async throws -> [PollListData] { - (1..<10) - .map { index in - PollListData(startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), question: "Do you like the poll number \(index)?") - } - .sorted { poll1, poll2 in - poll1.startDate > poll2.startDate - } + pollListData } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 9de37b9c0..fa000b901 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -37,24 +37,10 @@ struct PollHistory: View { } .padding(.horizontal, 16) - ScrollView { - LazyVStack(spacing: 32) { - let enumeratedPolls = Array(viewModel.viewState.polls.enumerated()) - - ForEach(enumeratedPolls, id: \.offset) { _, pollData in - PollListItem(pollData: pollData) - } - .frame(maxWidth: .infinity, alignment: .leading) - - Button { - #warning("handle action") - } label: { - Text("Load more polls") - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.horizontal, 16) - .padding(.top, 32) + if viewModel.viewState.polls.isEmpty { + noPollsView + } else { + pollListView } } .padding(.top, 32) @@ -66,6 +52,36 @@ struct PollHistory: View { viewModel.send(viewAction: .viewAppeared) } } + + private var pollListView: some View { + ScrollView { + LazyVStack(spacing: 32) { + let enumeratedPolls = Array(viewModel.viewState.polls.enumerated()) + + ForEach(enumeratedPolls, id: \.offset) { _, pollData in + PollListItem(pollData: pollData) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + #warning("handle action") + } label: { + Text("Load more polls") + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 32) + .padding(.horizontal, 16) + } + } + + private var noPollsView: some View { + Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + } } private extension PollHistoryMode { From 9896fb124b77cc717648c9c7cbcd5f8655f8c985 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 12 Jan 2023 18:08:37 +0100 Subject: [PATCH 043/468] Fix how is resent a voice broadcast chunk --- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 30 ++++++++++++++++--- changelog.d/7261.bugfix | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7261.bugfix diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index d8f55bf14..d89262a0e 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2185,10 +2185,32 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self removeEventWithEventId:eventId]; if (event.isVoiceMessage) { - NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; - NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; - - [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; + // Check if it is an actual voice message or a voicebroadcast chunk + if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) { + // VoiceBroadcast chunk + NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; + NSDictionary* additionalContentParams = @{ + kMXEventRelationRelatesToKey: event.content[kMXEventRelationRelatesToKey], + VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] + }; + [_room sendVoiceMessage:localFileURL + additionalContentParams:additionalContentParams + mimeType:mimetype + duration:duration.doubleValue + samples:nil + threadId:self.threadId + localEcho:nil + success:success + failure:failure + keepActualFilename:false]; + + } else { + // Voice message + NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; + NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; + + [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; + } } else { [self sendAudioFile:localFileURL mimeType:mimetype success:success failure:failure]; } diff --git a/changelog.d/7261.bugfix b/changelog.d/7261.bugfix new file mode 100644 index 000000000..3594c5980 --- /dev/null +++ b/changelog.d/7261.bugfix @@ -0,0 +1 @@ +Voice Broadcast: VoiceBroadcast chunks are no longer resent as voice messages From b549f5e4d87c2bc2f550799068f92aacd4006ad7 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 10:49:43 +0100 Subject: [PATCH 044/468] Add ui tests --- .../Modules/Common/Mock/MockAppScreens.swift | 3 +- .../Test/UI/PollHistoryUITests.swift | 30 ++++++------ .../Test/Unit/PollHistoryViewModelTests.swift | 48 ------------------- .../Room/PollHistory/View/PollHistory.swift | 1 + .../Room/PollHistory/View/PollListItem.swift | 1 + 5 files changed, 20 insertions(+), 63 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index eaee22e73..ba1d91e52 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -72,6 +72,7 @@ enum MockAppScreens { MockComposerScreenState.self, MockComposerCreateActionListScreenState.self, MockComposerLinkActionScreenState.self, - MockVoiceBroadcastPlaybackScreenState.self + MockVoiceBroadcastPlaybackScreenState.self, + MockPollHistoryScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 3b123c95d..a31377603 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -18,21 +18,23 @@ import RiotSwiftUI import XCTest class PollHistoryUITests: MockScreenTestCase { - func testPollHistoryPromptRegular() { - let promptType = PollHistoryPromptType.regular - app.goToScreenWithIdentifier(MockPollHistoryScreenState.promptType(promptType).title) - - let title = app.staticTexts["title"] - XCTAssert(title.exists) - XCTAssertEqual(title.label, promptType.title) + func testPollHistoryHasContent() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) } - func testPollHistoryPromptUpgrade() { - let promptType = PollHistoryPromptType.upgrade - app.goToScreenWithIdentifier(MockPollHistoryScreenState.promptType(promptType).title) - - let title = app.staticTexts["title"] - XCTAssert(title.exists) - XCTAssertEqual(title.label, promptType.title) + func testPollHistoryShowsEmptyScreen() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeEmpty.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift deleted file mode 100644 index a388d9823..000000000 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -@testable import RiotSwiftUI - -class PollHistoryViewModelTests: XCTestCase { - private enum Constants { - static let counterInitialValue = 0 - } - - var viewModel: PollHistoryViewModelProtocol! - var context: PollHistoryViewModelType.Context! - - override func setUpWithError() throws { - viewModel = PollHistoryViewModel(promptType: .regular, initialCount: Constants.counterInitialValue) - context = viewModel.context - } - - func testInitialState() { - XCTAssertEqual(context.viewState.count, Constants.counterInitialValue) - } - - func testCounter() throws { - context.send(viewAction: .incrementCount) - XCTAssertEqual(context.viewState.count, 1) - - context.send(viewAction: .incrementCount) - XCTAssertEqual(context.viewState.count, 2) - - context.send(viewAction: .decrementCount) - XCTAssertEqual(context.viewState.count, 1) - } -} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index fa000b901..5bbb60ad6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -81,6 +81,7 @@ struct PollHistory: View { .foregroundColor(theme.colors.secondaryContent) .frame(maxHeight: .infinity) .padding(.horizontal, 16) + .accessibilityLabel("PollHistory.emptyText") } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index dff98ba0a..58669552c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -45,6 +45,7 @@ struct PollListItem: View { .foregroundColor(theme.colors.primaryContent) .font(theme.fonts.body) .lineLimit(2) + .accessibilityLabel("PollListItem.title") } } } From bd85e7aa2037944f9dfca95a6c4ebc8a99bc82a8 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 10:54:56 +0100 Subject: [PATCH 045/468] Add changelog.d file --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 1 - .../Coordinator/PollHistoryCoordinator.swift | 20 ------------------- changelog.d/pr-7267.change | 1 + 3 files changed, 1 insertion(+), 21 deletions(-) create mode 100644 changelog.d/pr-7267.change diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 774e60f1e..046c20c79 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -194,7 +194,6 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { } private func push(coordinator: Coordinator & Presentable, animated: Bool = true) { - coordinator.start() self.add(childCoordinator: coordinator) navigationRouter.push(coordinator, animated: animated) { self.remove(childCoordinator: coordinator) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 0cdf3629a..b9129a6e9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -26,9 +26,6 @@ final class PollHistoryCoordinator: Coordinator, Presentable { private let pollHistoryHostingController: UIViewController private var pollHistoryViewModel: PollHistoryViewModelProtocol - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - // Must be used only internally var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? @@ -41,8 +38,6 @@ final class PollHistoryCoordinator: Coordinator, Presentable { let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryHostingController) } // MARK: - Public @@ -57,19 +52,4 @@ final class PollHistoryCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { pollHistoryHostingController } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - loadingIndicator = nil - } } diff --git a/changelog.d/pr-7267.change b/changelog.d/pr-7267.change new file mode 100644 index 000000000..cab02bc6a --- /dev/null +++ b/changelog.d/pr-7267.change @@ -0,0 +1 @@ +Polls: add UI for active poll history. From 5b99d71824c494f9256c66383b5f0d23861cfcdc Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 12:57:21 +0100 Subject: [PATCH 046/468] Inject AvatarViewMode as EnvironmentObject --- .../MockAnalyticsPromptScreenState.swift | 2 +- .../Avatar/Service/MatrixSDK/AvatarService.swift | 4 ---- .../Modules/Common/Avatar/View/AvatarImage.swift | 4 ++-- .../Common/Avatar/View/SpaceAvatarImage.swift | 4 ++-- .../Common/Avatar/ViewModel/AvatarViewModel.swift | 12 +++++++++++- .../DependencyContainerKey.swift | 15 --------------- .../InfoSheet/MockInfoSheetScreenState.swift | 2 +- .../LiveLocationSharingViewerCoordinator.swift | 2 +- .../Coordinator/LocationSharingCoordinator.swift | 2 +- .../LocationSharingScreenState.swift | 2 +- .../StaticLocationViewingCoordinator.swift | 3 ++- .../MockStaticLocationViewingScreenState.swift | 2 +- .../Avatar/MockOnboardingAvatarScreenState.swift | 2 +- .../MockOnboardingCelebrationScreenState.swift | 2 +- .../MockOnboardingUseCaseScreenState.swift | 2 +- .../RoomNotificationSettingsCoordinator.swift | 2 +- .../View/RoomNotificationSettings.swift | 4 ++-- .../View/RoomNotificationSettingsHeader.swift | 2 +- .../MockRoomAccessTypeChooserScreenState.swift | 2 +- .../Coordinator/RoomUpgradeCoordinator.swift | 2 +- .../RoomUpgrade/MockRoomUpgradeScreenState.swift | 2 +- .../Coordinator/UserSuggestionCoordinator.swift | 4 ++-- .../UserSuggestionScreenState.swift | 2 +- .../View/UserSuggestionListItem.swift | 2 +- .../VoiceBroadcastPlaybackCoordinator.swift | 2 +- .../VoiceBroadcastRecorderCoordinator.swift | 3 ++- .../MatrixItemChooserCoordinator.swift | 5 +++-- .../MockMatrixItemChooserScreenState.swift | 2 +- .../View/MatrixItemChooserListRow.swift | 2 +- .../SpaceCreationEmailInvitesCoordinator.swift | 2 +- ...MockSpaceCreationEmailInvitesScreenState.swift | 2 +- .../SpaceCreationMenuCoordinator.swift | 2 +- .../SpaceCreationPostProcessCoordinator.swift | 2 +- .../MockSpaceCreationPostProcessScreenState.swift | 2 +- .../SpaceCreationRoomsCoordinator.swift | 2 +- .../Mock/MockSpaceCreationRoomsScreenState.swift | 2 +- .../SpaceCreationSettingsCoordinator.swift | 2 +- .../MockSpaceCreationSettingsScreenState.swift | 2 +- .../Coordinator/SpaceSelectorCoordinator.swift | 2 +- .../Coordinator/SpaceSettingsCoordinator.swift | 3 ++- .../MockSpaceSettingsScreenState.swift | 2 +- .../MockTemplateSimpleScreenScreenState.swift | 2 +- .../TemplateUserProfileCoordinator.swift | 2 +- .../MockTemplateUserProfileScreenState.swift | 2 +- .../View/TemplateUserProfileHeader.swift | 2 +- .../Coordinator/TemplateRoomChatCoordinator.swift | 3 ++- .../MockTemplateRoomChatScreenState.swift | 2 +- .../View/TemplateRoomChatBubbleView.swift | 2 +- .../Coordinator/TemplateRoomListCoordinator.swift | 2 +- .../MockTemplateRoomListScreenState.swift | 2 +- .../View/TemplateRoomListRow.swift | 2 +- .../MockUserSessionsOverviewScreenState.swift | 2 +- 52 files changed, 70 insertions(+), 74 deletions(-) diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 7d222aa31..559c67d17 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -47,7 +47,7 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { return ( [promptType, viewModel], AnyView(AnalyticsPrompt(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift index 02369980c..ea4488244 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift @@ -32,10 +32,6 @@ class AvatarService: AvatarServiceProtocol { private let mediaManager: MXMediaManager - static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol { - AvatarService(mediaManager: mediaManager) - } - init(mediaManager: MXMediaManager) { self.mediaManager = mediaManager } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index 4f51f574a..ff7950111 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -20,7 +20,7 @@ import SwiftUI struct AvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.dependencies) var dependencies: DependencyContainer - @StateObject var viewModel = AvatarViewModel() + @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? var matrixItemId: String @@ -95,7 +95,7 @@ struct AvatarImage_Previews: PreviewProvider { AvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge) } } - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index 8e967fe18..c6fe8da0b 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -20,7 +20,7 @@ import SwiftUI struct SpaceAvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.dependencies) var dependencies: DependencyContainer - @StateObject var viewModel = AvatarViewModel() + @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? var matrixItemId: String @@ -99,7 +99,7 @@ struct LiveAvatarImage_Previews: PreviewProvider { SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge) } } - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 433fb9cba..f6372c14f 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -20,10 +20,14 @@ import Foundation /// Simple ViewModel that supports loading an avatar image class AvatarViewModel: InjectableObject, ObservableObject { - @Inject var avatarService: AvatarServiceProtocol + private let avatarService: AvatarServiceProtocol @Published private(set) var viewState = AvatarViewState.empty + init(avatarService: AvatarServiceProtocol) { + self.avatarService = avatarService + } + private var cancellables = Set() /// Load an avatar @@ -58,3 +62,9 @@ class AvatarViewModel: InjectableObject, ObservableObject { .store(in: &cancellables) } } + +extension AvatarViewModel { + static func withMockedServices() -> AvatarViewModel { + .init(avatarService: MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift index 4bde8956e..9e5403b25 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift @@ -31,18 +31,3 @@ extension EnvironmentValues { set { self[DependencyContainerKey.self] = newValue } } } - -extension View { - /// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container. - /// - /// Important: When adding a dependency to cast it to the type in which it will be injected. - /// So if adding `MockDependency` but type at injection is `Dependency` remember to cast - /// to `Dependency` first. - /// - Parameter dependency: The dependency to add. - /// - Returns: The wrapped view that now includes the dependency. - func addDependency(_ dependency: T) -> some View { - transformEnvironment(\.dependencies) { container in - container.register(dependency: dependency) - } - } -} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift index 62d86a681..b34c744c3 100644 --- a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift +++ b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift @@ -51,7 +51,7 @@ enum MockInfoSheetScreenState: MockScreenState, CaseIterable { return ( [model, viewModel], AnyView(InfoSheet(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift index fc5c6b87a..4bd1f7c52 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift @@ -53,7 +53,7 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { service: service ) let view = LiveLocationSharingViewer(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) liveLocationSharingViewerViewModel = viewModel liveLocationSharingViewerHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift index ea3e1b908..9cd5853c7 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -86,7 +86,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { ) let view = LocationSharingView(context: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) locationSharingViewModel = viewModel locationSharingHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift index 3b86f4e89..c59e0bf4b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift @@ -34,6 +34,6 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { isLiveLocationSharingEnabled: true, service: locationSharingService) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) - .addDependency(MockAvatarService.example))) + .environmentObject(AvatarViewModel.withMockedServices()))) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift index b125fbcd6..bf938dac8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift @@ -56,7 +56,8 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { coordinateType: parameters.coordinateType ) let view = StaticLocationView(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) + staticLocationViewingViewModel = viewModel staticLocationViewingHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift index 2ea0f0aec..4430c36c2 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift @@ -50,6 +50,6 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable { return ([viewModel], AnyView(StaticLocationView(viewModel: viewModel.context) - .addDependency(MockAvatarService.example))) + .environmentObject(AvatarViewModel.withMockedServices()))) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift index 3c972b602..73fb8e668 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift @@ -57,7 +57,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingAvatarScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift index d911f8249..e0bca18c1 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift @@ -39,7 +39,7 @@ enum MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift index ed580b834..c1ac46265 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift @@ -45,7 +45,7 @@ enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index e0d909898..9a25f8689 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -51,7 +51,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin ) let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager) let view = RoomNotificationSettings(viewModel: viewModel, presentedModally: presentedModally) - .addDependency(avatarService) + .environmentObject(AvatarViewModel(avatarService: avatarService)) let viewController = VectorHostingController(rootView: view) roomNotificationSettingsViewModel = viewModel roomNotificationSettingsViewController = viewController diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift index 3034f50db..20a6406e3 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift @@ -85,13 +85,13 @@ struct RoomNotificationSettings_Previews: PreviewProvider { NavigationView { RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true) .navigationBarTitleDisplayMode(.inline) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } NavigationView { RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true) .navigationBarTitleDisplayMode(.inline) .theme(ThemeIdentifier.dark) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift index 20066b961..835c9bdd7 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift @@ -43,6 +43,6 @@ struct RoomNotificationSettingsHeader_Previews: PreviewProvider { static let name = "Element" static var previews: some View { RoomNotificationSettingsHeader(avatarData: MockAvatarInput.example, displayName: name) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift index fd8e74103..9f937d435 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift @@ -51,7 +51,7 @@ enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(RoomAccessTypeChooser(viewModel: viewModel.context, roomName: "Room Name") - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift index bc89e53cb..32564e69d 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift @@ -45,7 +45,7 @@ final class RoomUpgradeCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeService(session: parameters.session, roomId: parameters.roomId, parentSpaceId: parameters.parentSpaceId, versionOverride: parameters.versionOverride)) let view = RoomUpgrade(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) roomUpgradeViewModel = viewModel roomUpgradeHostingController = VectorHostingController(rootView: view) roomUpgradeHostingController.view.backgroundColor = .clear diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift index bca1f0ea7..4529c0eff 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift @@ -49,7 +49,7 @@ enum MockRoomUpgradeScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(RoomUpgrade(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index c3812b5e8..c6d86a655 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -61,7 +61,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) userSuggestionViewModel = viewModel userSuggestionHostingController = VectorHostingController(rootView: view) @@ -105,7 +105,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { private func calculateViewHeight() -> CGFloat { let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) let controller = VectorHostingController(rootView: view) guard let view = controller.view else { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index f8a8acade..a0ed20268 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -37,7 +37,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { return ( [service, listViewModel], AnyView(UserSuggestionListWithInput(viewModel: viewModel) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index 0d3328b33..862e7573d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -55,6 +55,6 @@ struct UserSuggestionListItem: View { struct UserSuggestionHeader_Previews: PreviewProvider { static var previews: some View { UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 3f5e55c6e..bc0d123f4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -66,7 +66,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { let view = VoiceBroadcastPlaybackView(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) return VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index 77d4c394a..409266d15 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -61,7 +61,8 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) + return VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift index 467c69eb9..0f3155c9f 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift @@ -70,11 +70,12 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable { let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail, selectionHeader: parameters.selectionHeader) matrixItemChooserViewModel = viewModel if let viewProvider = parameters.viewProvider { - let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + let view = viewProvider.view(with: viewModel.context) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) matrixItemChooserHostingController = VectorHostingController(rootView: view) } else { let view = MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) matrixItemChooserHostingController = VectorHostingController(rootView: view) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index e18476c24..b2db1b340 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -61,7 +61,7 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift index 1d67f2a72..883fd6e8e 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift @@ -70,6 +70,6 @@ struct MatrixItemChooserListRow: View { struct MatrixItemChooserListRow_Previews: PreviewProvider { static var previews: some View { TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift index 28792056f..d7a31ede5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift @@ -42,7 +42,7 @@ final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable { let service = SpaceCreationEmailInvitesService(session: parameters.session) let viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: parameters.creationParams, service: service) let view = SpaceCreationEmailInvites(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationEmailInvitesViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift index 0b70a8746..add3eb6ed 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift @@ -64,7 +64,7 @@ enum MockSpaceCreationEmailInvitesScreenState: MockScreenState, CaseIterable { return ( [viewModel], AnyView(SpaceCreationEmailInvites(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift index 8d590936d..e3bcd6c9c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationMenuCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationMenuViewModel(navTitle: parameters.navTitle, creationParams: parameters.creationParams, title: parameters.title, detail: parameters.detail, options: parameters.options) let view = SpaceCreationMenu(viewModel: viewModel.context, showBackButton: parameters.showBackButton) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationMenuViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift index a1838458d..e385e30cb 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParams)) let view = SpaceCreationPostProcess(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationPostProcessViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift index 27c003b71..f3c18f543 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift @@ -54,7 +54,7 @@ enum MockSpaceCreationPostProcessScreenState: MockScreenState { return ( [service, viewModel], AnyView(SpaceCreationPostProcess(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift index 2e94feec2..3e7ef4b4e 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift @@ -41,7 +41,7 @@ final class SpaceCreationRoomsCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceCreationRoomsViewModel(creationParameters: parameters.creationParams) let view = SpaceCreationRooms(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationRoomsViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift index 3ef06a64f..f23e463e2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift @@ -55,7 +55,7 @@ enum MockSpaceCreationRoomsScreenState: MockScreenState, CaseIterable { return ( [viewModel], AnyView(SpaceCreationRooms(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift index 5077e4657..a1ca35a42 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift @@ -48,7 +48,7 @@ final class SpaceCreationSettingsCoordinator: Coordinator, Presentable { let service = SpaceCreationSettingsService(roomName: parameters.creationParameters.name ?? "", userDefinedAddress: parameters.creationParameters.userDefinedAddress, session: parameters.session) let viewModel = SpaceCreationSettingsViewModel(spaceCreationSettingsService: service, creationParameters: parameters.creationParameters) let view = SpaceCreationSettings(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) spaceCreationSettingsViewModel = viewModel let hostingController = VectorHostingController(rootView: view) hostingController.isNavigationBarHidden = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift index 8738289a9..2942a43b4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift @@ -59,7 +59,7 @@ enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(SpaceCreationSettings(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift index 771ad0b8f..530f33eb9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift @@ -62,7 +62,7 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable { let service = SpaceSelectorService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, showHomeSpace: parameters.showHomeSpace, selectedSpaceId: parameters.selectedSpaceId) let viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: parameters.showCancel) let view = SpaceSelector(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) self.viewModel = viewModel let hostingViewController = VectorHostingController(rootView: view) hostingViewController.hidesBackTitleWhenPushed = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift index 0224ae5cf..6748ee5a7 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift @@ -48,7 +48,8 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: SpaceSettingsService(session: parameters.session, spaceId: parameters.spaceId)) let view = SpaceSettings(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) + spaceSettingsViewModel = viewModel let controller = VectorHostingController(rootView: view) controller.enableNavigationBarScrollEdgeAppearance = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift index f18482638..342cf520f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift @@ -79,7 +79,7 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(SpaceSettings(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift index 5db4451ec..ffb4af8ac 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift @@ -50,7 +50,7 @@ enum MockTemplateSimpleScreenScreenState: MockScreenState, CaseIterable { return ( [promptType, viewModel], AnyView(TemplateSimpleScreen(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index b3098629b..73dc2d987 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -37,7 +37,7 @@ final class TemplateUserProfileCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) let view = TemplateUserProfile(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) templateUserProfileViewModel = viewModel templateUserProfileHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift index 08082aec5..27e9d215b 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift @@ -55,7 +55,7 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateUserProfile(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift index a87e06605..f8e9bfe28 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift @@ -46,6 +46,6 @@ struct TemplateUserProfileHeader: View { struct TemplateUserProfileHeader_Previews: PreviewProvider { static var previews: some View { TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index bca98511f..afe669ec8 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -33,7 +33,8 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room)) let view = TemplateRoomChat(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.room.mxSession.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.room.mxSession.mediaManager))) + templateRoomChatViewModel = viewModel templateRoomChatHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift index 8ec8bced0..f9a74abae 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift @@ -56,7 +56,7 @@ enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateRoomChat(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift index 70495bd25..24df8b7e0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift @@ -58,6 +58,6 @@ struct TemplateRoomChatBubbleView_Previews: PreviewProvider { ) static var previews: some View { TemplateRoomChatBubbleView(bubble: bubble) - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift index 26bbbbb99..263e878c0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift @@ -33,7 +33,7 @@ final class TemplateRoomListCoordinator: Coordinator, Presentable { self.parameters = parameters let viewModel = TemplateRoomListViewModel(templateRoomListService: TemplateRoomListService(session: parameters.session)) let view = TemplateRoomList(viewModel: viewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager))) templateRoomListViewModel = viewModel templateRoomListHostingController = VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift index 8b8c0e2a3..59ad3fc8e 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift @@ -47,7 +47,7 @@ enum MockTemplateRoomListScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateRoomList(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift index a8db7b8ba..06841bbb2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift @@ -42,6 +42,6 @@ struct TemplateRoomListRow: View { struct TemplateRoomListRow_Previews: PreviewProvider { static var previews: some View { TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice") - .addDependency(MockAvatarService.example) + .environmentObject(AvatarViewModel.withMockedServices()) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift index 175a7d9a0..e09586b83 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift @@ -56,7 +56,7 @@ enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(UserSessionsOverview(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } From 73036f41bcea57efba448692d0b49607f7cf4f8a Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 12:58:49 +0100 Subject: [PATCH 047/468] Delete DependencyInjection folder --- .../Common/Avatar/View/AvatarImage.swift | 2 - .../Common/Avatar/View/SpaceAvatarImage.swift | 2 - .../Avatar/ViewModel/AvatarViewModel.swift | 2 +- .../DependencyContainer.swift | 46 ------------------- .../DependencyContainerKey.swift | 33 ------------- .../Common/DependencyInjection/Inject.swift | 43 ----------------- .../DependencyInjection/Injectable.swift | 30 ------------ .../InjectableObject.swift | 22 --------- 8 files changed, 1 insertion(+), 179 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift delete mode 100644 RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift delete mode 100644 RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift delete mode 100644 RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift delete mode 100644 RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index ff7950111..b143d4d30 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -19,7 +19,6 @@ import SwiftUI struct AvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI - @Environment(\.dependencies) var dependencies: DependencyContainer @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? @@ -43,7 +42,6 @@ struct AvatarImage: View { .frame(maxWidth: CGFloat(size.rawValue), maxHeight: CGFloat(size.rawValue)) .clipShape(Circle()) .onAppear { - viewModel.inject(dependencies: dependencies) viewModel.loadAvatar( mxContentUri: mxContentUri, matrixItemId: matrixItemId, diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index c6fe8da0b..2662831e1 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -19,7 +19,6 @@ import SwiftUI struct SpaceAvatarImage: View { @Environment(\.theme) var theme: ThemeSwiftUI - @Environment(\.dependencies) var dependencies: DependencyContainer @EnvironmentObject var viewModel: AvatarViewModel var mxContentUri: String? @@ -59,7 +58,6 @@ struct SpaceAvatarImage: View { ) }) .onAppear { - viewModel.inject(dependencies: dependencies) viewModel.loadAvatar( mxContentUri: mxContentUri, matrixItemId: matrixItemId, diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index f6372c14f..10055738d 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -19,7 +19,7 @@ import DesignKit import Foundation /// Simple ViewModel that supports loading an avatar image -class AvatarViewModel: InjectableObject, ObservableObject { +final class AvatarViewModel: ObservableObject { private let avatarService: AvatarServiceProtocol @Published private(set) var viewState = AvatarViewState.empty diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift deleted file mode 100644 index d09fa87f4..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// Used for storing and resolving dependencies at runtime. -struct DependencyContainer { - // Stores the dependencies with type information removed. - private var dependencyStore: [String: Any] = [:] - - /// Resolve a dependency by type. - /// - /// Given a particular `Type` (Inferred from return type), - /// generate a key and retrieve from storage. - /// - /// - Returns: The resolved dependency. - func resolve() -> T { - let key = String(describing: T.self) - guard let t = dependencyStore[key] as? T else { - fatalError("No provider registered for type \(T.self)") - } - return t - } - - /// Register a dependency. - /// - /// Given a dependency, generate a key from it's `Type` and save in storage. - /// - Parameter dependency: The dependency to register. - mutating func register(dependency: T) { - let key = String(describing: T.self) - dependencyStore[key] = dependency - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift deleted file mode 100644 index 9e5403b25..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import SwiftUI - -/// An Environment Key for retrieving runtime dependencies. -/// -/// Dependencies are to be injected into `ObservableObjects` -/// that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View). -private struct DependencyContainerKey: EnvironmentKey { - static let defaultValue = DependencyContainer() -} - -extension EnvironmentValues { - var dependencies: DependencyContainer { - get { self[DependencyContainerKey.self] } - set { self[DependencyContainerKey.self] = newValue } - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift deleted file mode 100644 index d45907eeb..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// A property wrapped used to inject from the dependency container on the instance, to instance properties. -/// -/// ``` -/// @Inject var someClass: SomeClass -/// ``` -@propertyWrapper struct Inject { - static subscript(_enclosingInstance instance: T, - wrapped wrappedKeyPath: ReferenceWritableKeyPath, - storage storageKeyPath: ReferenceWritableKeyPath) -> Value { - get { - // Resolve dependencies from enclosing instance's `dependencies` property - let v: Value = instance.dependencies.resolve() - return v - } - set { - fatalError("Only subscript get is supported for injection") - } - } - - @available(*, unavailable, message: "This property wrapper can only be applied to classes") - var wrappedValue: Value { - get { fatalError("wrappedValue get not used") } - set { fatalError("wrappedValue set not used. \(newValue)") } - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift deleted file mode 100644 index b05b966e4..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// A protocol for classes that can be injected with a dependency container -protocol Injectable: AnyObject { - var dependencies: DependencyContainer! { get set } -} - -extension Injectable { - /// Used to inject the dependency container into an Injectable. - /// - Parameter dependencies: The `DependencyContainer` to inject. - func inject(dependencies: DependencyContainer) { - self.dependencies = dependencies - } -} diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift deleted file mode 100644 index eab3cdcdf..000000000 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// Class that can be extended that supports injection and the `@Inject` property wrapper. -open class InjectableObject: Injectable { - var dependencies: DependencyContainer! -} From b6689bdde33e698d87bc40722d1fef0a856dacc5 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 13:32:25 +0100 Subject: [PATCH 048/468] Add changelog.d file --- changelog.d/pr-7268.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7268.bugfix diff --git a/changelog.d/pr-7268.bugfix b/changelog.d/pr-7268.bugfix new file mode 100644 index 000000000..b6af7cd57 --- /dev/null +++ b/changelog.d/pr-7268.bugfix @@ -0,0 +1 @@ +Fix a crash caused by the missing Avatar Service dependency. From 1c9d998daec48774f5b53b206f61ddbc3e2b2d4e Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 13 Jan 2023 15:29:51 +0100 Subject: [PATCH 049/468] Code cleanup --- .../MatrixKit/Models/Room/MXKRoomDataSource.h | 2 ++ .../MatrixKit/Models/Room/MXKRoomDataSource.m | 35 +++++++------------ Riot/Modules/Room/RoomViewController.m | 2 +- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 9928892f2..87aabe50b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -572,6 +572,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; Once complete, this local echo will be replaced by the event saved by the homeserver. @param audioFileLocalURL the local filesystem path of the audio file to send. + @param additionalContentParams (optional) the additional parameters to the content. @param mimeType (optional) the mime type of the file. Defaults to `audio/ogg` @param duration the length of the voice message in milliseconds @param samples an array of floating point values normalized to [0, 1], boxed within NSNumbers @@ -580,6 +581,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param failure A block object called when the operation fails. */ - (void)sendVoiceMessage:(NSURL *)audioFileLocalURL + additionalContentParams:(NSDictionary*)additionalContentParams mimeType:mimeType duration:(NSUInteger)duration samples:(NSArray *)samples diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index d89262a0e..5f33af838 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1998,6 +1998,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } - (void)sendVoiceMessage:(NSURL *)audioFileLocalURL + additionalContentParams:(NSDictionary *)additionalContentParams mimeType:mimeType duration:(NSUInteger)duration samples:(NSArray *)samples @@ -2006,7 +2007,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendVoiceMessage:audioFileLocalURL mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; + [_room sendVoiceMessage:audioFileLocalURL additionalContentParams:additionalContentParams mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; if (localEchoEvent) { @@ -2185,32 +2186,20 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self removeEventWithEventId:eventId]; if (event.isVoiceMessage) { - // Check if it is an actual voice message or a voicebroadcast chunk - if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) { - // VoiceBroadcast chunk - NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; - NSDictionary* additionalContentParams = @{ + // Voice message + NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; + NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; + + // Additional content params in case it is a voicebroacast chunk + NSDictionary* additionalContentParams = nil; + if (event.content[kMXEventRelationRelatesToKey] != nil && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) { + additionalContentParams = @{ kMXEventRelationRelatesToKey: event.content[kMXEventRelationRelatesToKey], VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] }; - [_room sendVoiceMessage:localFileURL - additionalContentParams:additionalContentParams - mimeType:mimetype - duration:duration.doubleValue - samples:nil - threadId:self.threadId - localEcho:nil - success:success - failure:failure - keepActualFilename:false]; - - } else { - // Voice message - NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; - NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; - - [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; } + + [self sendVoiceMessage:localFileURL additionalContentParams:additionalContentParams mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; } else { [self sendAudioFile:localFileURL mimeType:mimetype success:success failure:failure]; } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7e9873209..87b7e858f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7926,7 +7926,7 @@ static CGSize kThreadListBarButtonItemImageSize; samples:(NSArray *)samples completion:(void (^)(BOOL))completion { - [self.roomDataSource sendVoiceMessage:url mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { + [self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); completion(YES); } failure:^(NSError *error) { From 6ebb037b2a16e7d333e991783a125c6abd424ad0 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:15:06 +0100 Subject: [PATCH 050/468] Remove unused code --- RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 5bbb60ad6..725ff2e80 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -21,10 +21,6 @@ struct PollHistory: View { @ObservedObject var viewModel: PollHistoryViewModel.Context - var bindings: PollHistoryViewBindings { - viewModel.viewState.bindings - } - var body: some View { VStack { HStack { From ed4fb00e8877452c43a3d408346e4c4c03939165 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:17:45 +0100 Subject: [PATCH 051/468] Cleanup layout --- .../Room/PollHistory/View/PollHistory.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 725ff2e80..b527dfb28 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -23,14 +23,12 @@ struct PollHistory: View { var body: some View { VStack { - HStack { - SegmentedPicker( - segments: PollHistoryMode.allCases.map { ($0.segmentTitle, $0) }, - selection: $viewModel.mode, - interSegmentSpacing: 14 - ) - Spacer() - } + SegmentedPicker( + segments: PollHistoryMode.allCases.map { ($0.segmentTitle, $0) }, + selection: $viewModel.mode, + interSegmentSpacing: 14 + ) + .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) if viewModel.viewState.polls.isEmpty { From e2247ae5a1e46e41b922c4cd95d4a6861c949db1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:22:35 +0100 Subject: [PATCH 052/468] Add a ScaledMetric --- RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 58669552c..373ea0202 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -25,6 +25,7 @@ struct PollListItem: View { @Environment(\.theme) private var theme private let pollData: PollListData + @ScaledMetric private var imageSize = 16 init(pollData: PollListData) { self.pollData = pollData @@ -39,7 +40,7 @@ struct PollListItem: View { HStack(alignment: .firstTextBaseline, spacing: 8) { Image(uiImage: Asset.Images.pollHistory.image) .resizable() - .frame(width: 16, height: 16) + .frame(width: imageSize, height: imageSize) Text(pollData.question) .foregroundColor(theme.colors.primaryContent) From 098f957a5133cd365874cb08e162d88ceec8a6b7 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:30:16 +0100 Subject: [PATCH 053/468] Add accessibility identifiers in SegmentedPicker --- .../Room/PollHistory/Test/UI/PollHistoryUITests.swift | 6 +++++- .../Modules/Room/PollHistory/View/SegmentedPicker.swift | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index a31377603..720e49c23 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -23,18 +23,22 @@ class PollHistoryUITests: MockScreenTestCase { let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] + let selectedSegment = app.buttons["\(VectorL10n.pollHistoryActiveSegmentTitle)-selected"] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertTrue(items.exists) XCTAssertFalse(emptyText.exists) + XCTAssertTrue(selectedSegment.exists) } func testPollHistoryShowsEmptyScreen() { - app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeEmpty.title) + app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title) let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] + let selectedSegment = app.buttons["\(VectorL10n.pollHistoryPastSegmentTitle)-selected"] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertFalse(items.exists) XCTAssertTrue(emptyText.exists) + XCTAssertTrue(selectedSegment.exists) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift index d60388ad6..0293aa99b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -42,6 +42,7 @@ struct SegmentedPicker: View { .underline(isSelectedSegment) } .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent) + .accessibilityLabel(text + (isSelectedSegment ? "-selected" : "")) } } } From 4c6be5a2411b9b780bb551885a7ef1e0b0724b03 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:38:29 +0100 Subject: [PATCH 054/468] Refactor SegmentedPicker --- .../Room/PollHistory/View/PollHistory.swift | 6 ++-- .../PollHistory/View/SegmentedPicker.swift | 33 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index b527dfb28..b1208f547 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -24,7 +24,7 @@ struct PollHistory: View { var body: some View { VStack { SegmentedPicker( - segments: PollHistoryMode.allCases.map { ($0.segmentTitle, $0) }, + segments: PollHistoryMode.allCases, selection: $viewModel.mode, interSegmentSpacing: 14 ) @@ -79,8 +79,8 @@ struct PollHistory: View { } } -private extension PollHistoryMode { - var segmentTitle: String { +extension PollHistoryMode: CustomStringConvertible { + var description: String { switch self { case .active: return VectorL10n.pollHistoryActiveSegmentTitle diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift index 0293aa99b..d669d67ad 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -16,14 +16,14 @@ import SwiftUI -struct SegmentedPicker: View { - private let segments: [(String, Tag)] - private let selection: Binding +struct SegmentedPicker: View { + private let segments: [Segment] + private let selection: Binding private let interSegmentSpacing: CGFloat @Environment(\.theme) private var theme - init(segments: [(String, Tag)], selection: Binding, interSegmentSpacing: CGFloat) { + init(segments: [Segment], selection: Binding, interSegmentSpacing: CGFloat) { self.segments = segments self.selection = selection self.interSegmentSpacing = interSegmentSpacing @@ -31,18 +31,18 @@ struct SegmentedPicker: View { var body: some View { HStack(spacing: interSegmentSpacing) { - ForEach(segments, id: \.1) { text, tag in - let isSelectedSegment = tag == selection.wrappedValue + ForEach(segments, id: \.hashValue) { segment in + let isSelectedSegment = segment == selection.wrappedValue Button { - selection.wrappedValue = tag + selection.wrappedValue = segment } label: { - Text(text) + Text(segment.description) .font(isSelectedSegment ? theme.fonts.headline : theme.fonts.body) .underline(isSelectedSegment) } .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent) - .accessibilityLabel(text + (isSelectedSegment ? "-selected" : "")) + .accessibilityLabel(segment.description + (isSelectedSegment ? "-selected" : "")) } } } @@ -52,10 +52,19 @@ struct SegmentedPicker_Previews: PreviewProvider { static var previews: some View { SegmentedPicker( segments: [ - ("Segment 1", "1"), - ("Segment 2", "2") + "Segment 1", + "Segment 2" ], - selection: .constant("1"), + selection: .constant("Segment 1"), + interSegmentSpacing: 14 + ) + + SegmentedPicker( + segments: [ + "Segment 1", + "Segment 2" + ], + selection: .constant("Segment 2"), interSegmentSpacing: 14 ) } From 2a5bcf9f44fbb834c8dd88c0e4b1338bf4f011ee Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 13 Jan 2023 16:40:40 +0100 Subject: [PATCH 055/468] Update build setting --- Config/BuildSettings.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 0c23cdd20..d4b57871a 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -399,13 +399,7 @@ final class BuildSettings: NSObject { // MARK: - Polls static let pollsEnabled = true - static var pollsHistoryEnabled: Bool { - #if DEBUG - true - #else - false - #endif - } + static var pollsHistoryEnabled: Bool = false // MARK: - Location Sharing From 175baee5f6c20740ba806dbec01488f7ce9f5a0f Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 5 Jan 2023 15:56:04 +0100 Subject: [PATCH 056/468] Rich Text Composer: Enable bulleted & numbered lists support --- .../xcshareddata/swiftpm/Package.resolved | 3 +- Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 8 +++ .../Room/Composer/Model/ComposerModels.swift | 62 +++++++++++++------ .../Composer/View/FormattingToolbar.swift | 30 ++++----- changelog.d/7238.feature | 1 + project.yml | 2 +- 7 files changed, 72 insertions(+), 36 deletions(-) create mode 100644 changelog.d/7238.feature diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 102f87715..3d0b1278d 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "534ee5bae5e8de69ed398937b5edb7b5f21551d2" + "revision" : "2f101426d9df13b830e87a5e6f0ac672e8118ca0", + "version" : "0.15.0" } }, { diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1b97f791..cd852aeda 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2559,6 +2559,8 @@ To enable access, tap Settings> Location and select Always"; "wysiwyg_composer_format_action_strikethrough" = "Apply underline format"; "wysiwyg_composer_format_action_link" = "Apply link format"; "wysiwyg_composer_format_action_inline_code" = "Apply inline code format"; +"wysiwyg_composer_format_action_unordered_list" = "Toggle bulleted list"; +"wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list"; // Links "wysiwyg_composer_link_action_text" = "Text"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5730a6305..111f81061 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9359,6 +9359,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionLink: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_link") } + /// Toggle numbered list + public static var wysiwygComposerFormatActionOrderedList: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_ordered_list") + } /// Apply underline format public static var wysiwygComposerFormatActionStrikethrough: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough") @@ -9367,6 +9371,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionUnderline: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline") } + /// Toggle bulleted list + public static var wysiwygComposerFormatActionUnorderedList: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_unordered_list") + } /// Create a link public static var wysiwygComposerLinkActionCreateTitle: String { return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_create_title") diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index bc2e8771d..d63c096cd 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -35,6 +35,8 @@ enum FormatType { case underline case strikethrough case inlineCode + case unorderedList + case orderedList case link } @@ -54,14 +56,18 @@ extension FormatItem { return Asset.Images.bold.name case .italic: return Asset.Images.italic.name - case .strikethrough: - return Asset.Images.strikethrough.name case .underline: return Asset.Images.underlined.name - case .link: - return Asset.Images.link.name + case .strikethrough: + return Asset.Images.strikethrough.name case .inlineCode: return Asset.Images.code.name + case .unorderedList: + return Asset.Images.bulletList.name + case .orderedList: + return Asset.Images.numberedList.name + case .link: + return Asset.Images.link.name } } @@ -71,14 +77,18 @@ extension FormatItem { return "boldButton" case .italic: return "italicButton" - case .strikethrough: - return "strikethroughButton" case .underline: return "underlineButton" - case .link: - return "linkButton" + case .strikethrough: + return "strikethroughButton" case .inlineCode: return "inlineCodeButton" + case .unorderedList: + return "unorderedListButton" + case .orderedList: + return "orderedListButton" + case .link: + return "linkButton" } } @@ -88,14 +98,18 @@ extension FormatItem { return VectorL10n.wysiwygComposerFormatActionBold case .italic: return VectorL10n.wysiwygComposerFormatActionItalic - case .strikethrough: - return VectorL10n.wysiwygComposerFormatActionStrikethrough case .underline: return VectorL10n.wysiwygComposerFormatActionUnderline - case .link: - return VectorL10n.wysiwygComposerFormatActionLink + case .strikethrough: + return VectorL10n.wysiwygComposerFormatActionStrikethrough case .inlineCode: return VectorL10n.wysiwygComposerFormatActionInlineCode + case .unorderedList: + return VectorL10n.wysiwygComposerFormatActionUnorderedList + case .orderedList: + return VectorL10n.wysiwygComposerFormatActionOrderedList + case .link: + return VectorL10n.wysiwygComposerFormatActionLink } } } @@ -108,14 +122,18 @@ extension FormatType { return .bold case .italic: return .italic - case .strikethrough: - return .strikeThrough case .underline: return .underline - case .link: - return .link + case .strikethrough: + return .strikeThrough case .inlineCode: return .inlineCode + case .unorderedList: + return .unorderedList + case .orderedList: + return .orderedList + case .link: + return .link } } @@ -127,14 +145,18 @@ extension FormatType { return .bold case .italic: return .italic - case .strikethrough: - return .strikeThrough case .underline: return .underline - case .link: - return .link + case .strikethrough: + return .strikeThrough + case .unorderedList: + return .unorderedList + case .orderedList: + return .orderedList case .inlineCode: return .inlineCode + case .link: + return .link } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift index d8670ee0c..e7d59a989 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift @@ -32,21 +32,23 @@ struct FormattingToolbar: View { var formatAction: (FormatType) -> Void var body: some View { - HStack(spacing: 4) { - ForEach(formatItems) { item in - Button { - formatAction(item.type) - } label: { - Image(item.icon) - .renderingMode(.template) - .foregroundColor(getForegroundColor(for: item)) + ScrollView(.horizontal) { + HStack(spacing: 4) { + ForEach(formatItems) { item in + Button { + formatAction(item.type) + } label: { + Image(item.icon) + .renderingMode(.template) + .foregroundColor(getForegroundColor(for: item)) + } + .disabled(item.state == .disabled) + .frame(width: 44, height: 44) + .background(getBackgroundColor(for: item)) + .cornerRadius(8) + .accessibilityIdentifier(item.accessibilityIdentifier) + .accessibilityLabel(item.accessibilityLabel) } - .disabled(item.state == .disabled) - .frame(width: 44, height: 44) - .background(getBackgroundColor(for: item)) - .cornerRadius(8) - .accessibilityIdentifier(item.accessibilityIdentifier) - .accessibilityLabel(item.accessibilityLabel) } } } diff --git a/changelog.d/7238.feature b/changelog.d/7238.feature new file mode 100644 index 000000000..173f1195e --- /dev/null +++ b/changelog.d/7238.feature @@ -0,0 +1 @@ +Rich Text Composer: Enable bulleted/numbered lists support diff --git a/project.yml b/project.yml index f2c4dbc23..7d0096d51 100644 --- a/project.yml +++ b/project.yml @@ -53,7 +53,7 @@ packages: branch: main WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - revision: 534ee5bae5e8de69ed398937b5edb7b5f21551d2 + version: 0.15.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 66bfdf895a37d68ef4ed5e8f0d075e3ecd697295 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Mon, 16 Jan 2023 11:44:40 +0100 Subject: [PATCH 057/468] Rich Text Composer: Enable quote & code blocks support --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../code_block.imageset/Contents.json | 23 ++++++++++ .../code_block.imageset/code_block.png | Bin 0 -> 493 bytes .../code_block.imageset/code_block@2x.png | Bin 0 -> 649 bytes .../code_block.imageset/code_block@3x.png | Bin 0 -> 953 bytes Riot/Assets/en.lproj/Vector.strings | 4 ++ Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 8 ++++ .../Room/Composer/Model/ComposerModels.swift | 42 +++++++++++++----- changelog.d/7271.feature | 1 + project.yml | 2 +- 11 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png create mode 100644 Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png create mode 100644 Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png create mode 100644 changelog.d/7271.feature diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3d0b1278d..1cc4e6de4 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "2f101426d9df13b830e87a5e6f0ac672e8118ca0", - "version" : "0.15.0" + "revision" : "e63034b57eeec1164e6cfcccf817f3b764b56a83", + "version" : "0.17.0" } }, { diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json new file mode 100644 index 000000000..bcb234ccc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "code_block.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "code_block@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "code_block@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png new file mode 100644 index 0000000000000000000000000000000000000000..a70342a591b96912c2c7d0fa1ccc02f0c3e2e430 GIT binary patch literal 493 zcmV%XoH!HrD}xP%)3H{b|8LKiNYNay(m zfsg{FvEW93Ni);;Ufllg zWqWA0kJmjPEAZ58msR<4E#E9S367A|Q87OfP!EzMR}g$5VMhHS;fO_i6%!}2R6u?D zEIB8#FN?!yOr34>BKM8ALSg}up+6qk`?-si%)5lPlT9peZI${wRQc_)I}*LSm)1g$ za-Lf`DbVBHpA8XKYv-rk!~%-Rj-H7`{Zd0`n!IkExvTQTR*@1Rk|vATOWe1l2{SIa zLdP0v7p3khaU|fs7p*1w5Axi5kOpoo`9*;s7>dlEM?i%GDo6?UE>DNc@LgBROeG z>ey0@m@_s%9+yQL2jsAIj2fHUv_0*bTa9f(BZBx|?wp|~gE zD`uCy(|z!Ohvaq2s$w}Ngk60sf`3jh&@{n7(*y%e6AaX0_&jn}B(}g3uAkyn{S@Lqo7iTlD!0HO*#HVMYo2neG(oJ*a9AH6h18d?LB>bw% zL&S;Jv^G)8K&f~x{m}Zh`^rWIZOo^#Un)>S95pcbi}9w1{fJ|HFsUzOM>wfvNhpSyin^N z*kppg9z0j&x_l4_wQu4w6KtSel}7{z*|Dv7!RjegG{JSwZB-6WpESmWc)_|FT?yEi z68WmznODX0SG57hKK8eW-3Djt0TV&4#0z0X)-=ID(*y%e6AUy>;Z02X3nqR@-lpII z`W6!sEcCIQfhDLhe{8xaOY{ZNsJC-dNOu3SV#`OQSk!kYh0=}gyX2+KOS@wp+yxKd jb@PHqjf>&~0SEj7>wg-`p8M^s00000NkvXXu0mjfAGsgQ literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..21bd83678de90b26f05c9b880a1671e48d592ffa GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG z=+U|c2;0%p@!cok8 zNjSvTF)?^0>yix%oAf+FUWnZgSJh!Cof;yV%4zFZ&HRG*l61;tlPu}ob%GNGK0P^W zu#NTPZ!^Z*$wu?l(*E2QW^fc>d3djBvexXkpSgJ0qS?MGNvt_ga?z9{{M3WQ^{W62QCGDI#d*kEx6@^-{n);s$;{7G-rxCk_JBnde3D0Mip7$O9n-`%W{ z)VO_ZfBn^*7eRq17k3}~b=vx<8oT=PZTuB!!h7d)g`@{fd(-_O{DQ_l*)RHQWlqJH zef{wM>A{cFs~*ZG=xKH+OqxC(JAid zzCH8eea9gy9X-k1apBXRPTiB;3nGe+9hvgPHNeL*xyWAC}Om-5(k?LF&&=hlV`2(zEC0-xA=kKDXtq`^sml z7JuKe_vbGrub`-a#T&0V2b@p#e)Qx*H4}e@E@ys&!wEY?Hsc-(mXeX zA667mHT-Q}->cZpG0(GK^`e}6b(n1Qma^qGFV1>(_bs)16S1X#ORl+7hJ0%1<6kaO z0YcKVO5b?1D=EEBLXR!mJNCwPE?cSi!zoin zrbuAJ>#SFGI(HmC$|ms3eLfueN+f0ajsPktXOt$EG!WlmFu< Location and select Always"; "wysiwyg_composer_format_action_inline_code" = "Apply inline code format"; "wysiwyg_composer_format_action_unordered_list" = "Toggle bulleted list"; "wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list"; +"wysiwyg_composer_format_action_code_block" = "Toggle code block"; +"wysiwyg_composer_format_action_quote" = "Toggle quote"; + + // Links "wysiwyg_composer_link_action_text" = "Text"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index ed763a171..5f3ef86c5 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -110,6 +110,7 @@ internal class Asset: NSObject { internal static let strikethrough = ImageAsset(name: "Strikethrough") internal static let underlined = ImageAsset(name: "Underlined") internal static let bulletList = ImageAsset(name: "bullet_list") + internal static let codeBlock = ImageAsset(name: "code_block") internal static let indentDecrease = ImageAsset(name: "indent_decrease") internal static let maximiseComposer = ImageAsset(name: "maximise_composer") internal static let minimiseComposer = ImageAsset(name: "minimise_composer") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 111f81061..920857f28 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9347,6 +9347,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionBold: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_bold") } + /// Toggle code block + public static var wysiwygComposerFormatActionCodeBlock: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_code_block") + } /// Apply inline code format public static var wysiwygComposerFormatActionInlineCode: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code") @@ -9363,6 +9367,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionOrderedList: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_ordered_list") } + /// Toggle quote + public static var wysiwygComposerFormatActionQuote: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_quote") + } /// Apply underline format public static var wysiwygComposerFormatActionStrikethrough: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough") diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index d63c096cd..5525e9940 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -34,9 +34,11 @@ enum FormatType { case italic case underline case strikethrough - case inlineCode case unorderedList case orderedList + case inlineCode + case codeBlock + case quote case link } @@ -60,12 +62,16 @@ extension FormatItem { return Asset.Images.underlined.name case .strikethrough: return Asset.Images.strikethrough.name - case .inlineCode: - return Asset.Images.code.name case .unorderedList: return Asset.Images.bulletList.name case .orderedList: return Asset.Images.numberedList.name + case .inlineCode: + return Asset.Images.code.name + case .codeBlock: + return Asset.Images.codeBlock.name + case .quote: + return Asset.Images.quote.name case .link: return Asset.Images.link.name } @@ -81,12 +87,16 @@ extension FormatItem { return "underlineButton" case .strikethrough: return "strikethroughButton" - case .inlineCode: - return "inlineCodeButton" case .unorderedList: return "unorderedListButton" case .orderedList: return "orderedListButton" + case .inlineCode: + return "inlineCodeButton" + case .codeBlock: + return "codeBlockButton" + case .quote: + return "quoteButton" case .link: return "linkButton" } @@ -102,12 +112,16 @@ extension FormatItem { return VectorL10n.wysiwygComposerFormatActionUnderline case .strikethrough: return VectorL10n.wysiwygComposerFormatActionStrikethrough - case .inlineCode: - return VectorL10n.wysiwygComposerFormatActionInlineCode case .unorderedList: return VectorL10n.wysiwygComposerFormatActionUnorderedList case .orderedList: return VectorL10n.wysiwygComposerFormatActionOrderedList + case .inlineCode: + return VectorL10n.wysiwygComposerFormatActionInlineCode + case .codeBlock: + return VectorL10n.wysiwygComposerFormatActionCodeBlock + case .quote: + return VectorL10n.wysiwygComposerFormatActionQuote case .link: return VectorL10n.wysiwygComposerFormatActionLink } @@ -126,12 +140,16 @@ extension FormatType { return .underline case .strikethrough: return .strikeThrough - case .inlineCode: - return .inlineCode case .unorderedList: return .unorderedList case .orderedList: return .orderedList + case .inlineCode: + return .inlineCode + case .codeBlock: + return .codeBlock + case .quote: + return .quote case .link: return .link } @@ -155,6 +173,10 @@ extension FormatType { return .orderedList case .inlineCode: return .inlineCode + case .codeBlock: + return .codeBlock + case .quote: + return .quote case .link: return .link } @@ -189,5 +211,3 @@ final class LinkActionWrapper: NSObject { super.init() } } - - diff --git a/changelog.d/7271.feature b/changelog.d/7271.feature new file mode 100644 index 000000000..f2ae089f9 --- /dev/null +++ b/changelog.d/7271.feature @@ -0,0 +1 @@ +Rich Text Composer: Enable quote & code blocks support diff --git a/project.yml b/project.yml index 7d0096d51..c2de639ef 100644 --- a/project.yml +++ b/project.yml @@ -53,7 +53,7 @@ packages: branch: main WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 0.15.0 + version: 0.17.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 4b684b20a315c6e3b5a509ea1aa0346e582452f2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 16 Jan 2023 12:01:24 +0100 Subject: [PATCH 058/468] Improve accessibility --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../Room/PollHistory/Test/UI/PollHistoryUITests.swift | 6 ++++-- .../Modules/Room/PollHistory/View/SegmentedPicker.swift | 3 ++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 62f25672c..41cca1e70 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -96,6 +96,7 @@ // Accessibility "accessibility_checkbox_label" = "checkbox"; "accessibility_button_label" = "button"; +"accessibility_selected" = "selected"; // MARK: Onboarding "onboarding_splash_register_button_title" = "Create account"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e0be60cee..37dfa9f53 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -27,6 +27,10 @@ public class VectorL10n: NSObject { public static var accessibilityCheckboxLabel: String { return VectorL10n.tr("Vector", "accessibility_checkbox_label") } + /// selected + public static var accessibilitySelected: String { + return VectorL10n.tr("Vector", "accessibility_selected") + } /// Unable to verify email address. Please check your email and click on the link it contains. Once this is done, click continue public static var accountEmailValidationError: String { return VectorL10n.tr("Vector", "account_email_validation_error") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 720e49c23..3fcc05c7b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -23,11 +23,12 @@ class PollHistoryUITests: MockScreenTestCase { let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] - let selectedSegment = app.buttons["\(VectorL10n.pollHistoryActiveSegmentTitle)-selected"] + let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertTrue(items.exists) XCTAssertFalse(emptyText.exists) XCTAssertTrue(selectedSegment.exists) + XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) } func testPollHistoryShowsEmptyScreen() { @@ -35,10 +36,11 @@ class PollHistoryUITests: MockScreenTestCase { let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] - let selectedSegment = app.buttons["\(VectorL10n.pollHistoryPastSegmentTitle)-selected"] + let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertFalse(items.exists) XCTAssertTrue(emptyText.exists) XCTAssertTrue(selectedSegment.exists) + XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift index d669d67ad..520a649c7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -42,7 +42,8 @@ struct SegmentedPicker: View { .underline(isSelectedSegment) } .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent) - .accessibilityLabel(segment.description + (isSelectedSegment ? "-selected" : "")) + .accessibilityLabel(segment.description) + .accessibilityValue(isSelectedSegment ? VectorL10n.accessibilitySelected : "") } } } From 0feaec0be34ca29725e0c4423f978323027ad48f Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Fri, 13 Jan 2023 15:00:59 +0000 Subject: [PATCH 059/468] Add labs settings for crypto v2 --- Config/CommonConfiguration.swift | 6 ++ Riot/Assets/en.lproj/Vector.strings | 3 + Riot/Generated/Strings.swift | 12 ++++ Riot/Managers/Settings/RiotSettings.swift | 6 ++ .../Modules/Settings/SettingsViewController.m | 58 ++++++++++++++++++- changelog.d/pr-7272.change | 1 + 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr-7272.change diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index fee3796ff..f3172a710 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,6 +91,12 @@ class CommonConfiguration: NSObject, Configurable { MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature + + #if DEBUG + if sdkOptions.isCryptoSDKAvailable { + sdkOptions.enableCryptoSDK = RiotSettings.shared.enableCryptoSDK + } + #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1b97f791..bf1b69746 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -803,6 +803,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; +"settings_labs_enable_crypto_sdk" = "Enable new rust-based Crypto SDK"; +"settings_labs_confirm_crypto_sdk" = "This action cannot be undone"; +"settings_labs_disable_crypto_sdk" = "Crypto SDK is enabled. To disable please reinstall the app"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5730a6305..3de9542c2 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7547,10 +7547,18 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } + /// This action cannot be undone + public static var settingsLabsConfirmCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") + } /// Create conference calls with jitsi public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } + /// Crypto SDK is enabled. To disable please reinstall the app + public static var settingsLabsDisableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") + } /// End-to-End Encryption public static var settingsLabsE2eEncryption: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") @@ -7563,6 +7571,10 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } + /// Enable new rust-based Crypto SDK + public static var settingsLabsEnableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") + } /// Live location sharing - share current location (active development, and temporarily, locations persist in room history) public static var settingsLabsEnableLiveLocationSharing: String { return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 591f3968a..d9e64a1af 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -191,6 +191,12 @@ final class RiotSettings: NSObject { /// Flag indicating if the voice broadcast feature is enabled @UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults) var enableVoiceBroadcast + + #if DEBUG + /// Flag indicating if we are using rust-based `MatrixCryptoSDK` instead of `MatrixSDK`'s internal crypto module + @UserDefault(key: "enableCryptoSDK", defaultValue: false, storage: defaults) + var enableCryptoSDK + #endif // MARK: Calls diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 54bb95d34..90533cfd1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -176,7 +176,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_NEW_SESSION_MANAGER, LABS_ENABLE_NEW_CLIENT_INFO_FEATURE, LABS_ENABLE_WYSIWYG_COMPOSER, - LABS_ENABLE_VOICE_BROADCAST + LABS_ENABLE_VOICE_BROADCAST, + LABS_ENABLE_CRYPTO_SDK }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -587,6 +588,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; + #if DEBUG + if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable) + { + [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; + } + #endif + [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; @@ -2583,6 +2591,23 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = labelAndSwitchCell; } + else + { + #if DEBUG + if (row == LABS_ENABLE_CRYPTO_SDK) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; + labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; + labelAndSwitchCell.mxkSwitch.on = isEnabled; + [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + #endif + } } else if (section == SECTION_TAG_SECURITY) { @@ -3354,6 +3379,37 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } +#if DEBUG +- (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender +{ + BOOL isEnabled = sender.isOn; + MXWeakify(self); + + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:nil + message:VectorL10n.settingsLabsConfirmCryptoSdk + preferredStyle:UIAlertControllerStyleAlert]; + + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + + [sender setOn:NO animated:YES]; + }]]; + + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + RiotSettings.shared.enableCryptoSDK = isEnabled; + MXSDKOptions.sharedInstance.enableCryptoSDK = isEnabled; + [[AppDelegate theDelegate] reloadMatrixSessions:YES]; + }]]; + + [self presentViewController:confirmationAlert animated:YES completion:nil]; + currentAlert = confirmationAlert; +} +#endif + - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; diff --git a/changelog.d/pr-7272.change b/changelog.d/pr-7272.change new file mode 100644 index 000000000..04a8dcc2e --- /dev/null +++ b/changelog.d/pr-7272.change @@ -0,0 +1 @@ +CryptoSDK: Add labs settings to enable Crypto SDK From a65e02b7b9777dc9e6316acc57a361a088c37679 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 12 Jan 2023 16:53:31 +0100 Subject: [PATCH 060/468] Improved voice broadcast completion detection during playback --- .../VoiceBroadcastAggregator.swift | 8 ++++- .../VoiceBroadcastPlaybackViewModel.swift | 32 +++++++++++++++++-- changelog.d/pr-7273.change | 1 + 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 changelog.d/pr-7273.change diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index f9afbadc1..1741dc14c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -53,6 +53,8 @@ public class VoiceBroadcastAggregator { private var voiceBroadcastInfoStartEventContent: VoiceBroadcastInfo! private var voiceBroadcastSenderId: String! + public private(set) var voiceBroadcastLastChunkSequence: Int = 0 + private var referenceEventsListener: Any? private var events: [MXEvent] = [] @@ -168,7 +170,10 @@ public class VoiceBroadcastAggregator { let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else { return } - + // For .pause and .stopped, if there is a lastChunkSequence (ie its value is > 0), we store it + if [.stopped, .paused].contains(state), voiceBroadcastInfo.lastChunkSequence > 0 { + self.voiceBroadcastLastChunkSequence = voiceBroadcastInfo.lastChunkSequence + } self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state) } } @@ -187,6 +192,7 @@ public class VoiceBroadcastAggregator { } self.events.removeAll() + self.voiceBroadcastLastChunkSequence = 0 let filteredChunk = response.chunk.filter { event in event.sender == self.voiceBroadcastSenderId && diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 1f5ac9872..9f96a0d52 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -43,7 +43,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private var reloadVoiceBroadcastChunkQueue: Bool = false private var seekToChunkTime: TimeInterval? + private var lastChunkAddedToPlayer: UInt = 0 + private var isPlayingLastChunk: Bool { + // We can't play the last chunk if the brodcast is not stopped + guard state.broadcastState == .stopped else { + return false + } + let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) guard let chunkDuration = chunks.last?.duration else { return false @@ -168,11 +175,24 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func stopIfVoiceBroadcastOver() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver") + var shouldStop = false + // Check if the broadcast is over before stopping everything - // If not, the player should not stopped. The view state must be move to buffering - if state.broadcastState == .stopped, isPlayingLastChunk { + if state.broadcastState == .stopped { + // If we known the last chunk sequence, use it to check if we need to stop + // Note: it's possible to be in .stopped state and to still have a last chunk sequence at 0 (old versions or a crash during recording). In this case, we use isPlayingLastChunk as a fallback solution + if voiceBroadcastAggregator.voiceBroadcastLastChunkSequence > 0 { + // we should stop only if we have already added the last chunk to the player + shouldStop = (lastChunkAddedToPlayer == voiceBroadcastAggregator.voiceBroadcastLastChunkSequence) + } else { + shouldStop = isPlayingLastChunk + } + } + + if shouldStop { stop() } else { + // If not, the player should not stopped. The view state must be move to buffering state.playbackState = .buffering } } @@ -200,6 +220,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func seek(to seekTime: Float) { // Flush the chunks queue and the current audio player playlist + lastChunkAddedToPlayer = 0 voiceBroadcastChunkQueue = [] reloadVoiceBroadcastChunkQueue = isProcessingVoiceBroadcastChunk audioPlayer?.removeAllPlayerItems() @@ -294,7 +315,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic guard result.eventIdentifier == chunk.attachment.eventId else { return } - + self.lastChunkAddedToPlayer = max(self.lastChunkAddedToPlayer, chunk.sequence) self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) // Instanciate audioPlayer if needed. @@ -436,6 +457,11 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { // Handle the live icon appearance state.playingState.isLive = isLivePlayback + + // Handle the case where the playback state is .buffering and the new broadcast state is .stopped + if didReceiveState == .stopped, self.state.playbackState == .buffering { + stopIfVoiceBroadcastOver() + } } func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { diff --git a/changelog.d/pr-7273.change b/changelog.d/pr-7273.change new file mode 100644 index 000000000..2c3d47b2e --- /dev/null +++ b/changelog.d/pr-7273.change @@ -0,0 +1 @@ +Voice Broadcast: Improved detection of voice broadcast completion during playback. From 9fbf774c8dc09df5a15bbfd9b62f3e362928494e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 16 Jan 2023 14:45:21 +0100 Subject: [PATCH 061/468] Fix build error --- .../Modules/Room/PollHistory/MockPollHistoryScreenState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 08b33293b..c00ccf528 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -58,7 +58,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { return ( [pollHistoryMode, viewModel], AnyView(PollHistory(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } From 8185255b0e48c14f38aafa54f8ce11f93e5c6d40 Mon Sep 17 00:00:00 2001 From: Philippe Loriaux Date: Mon, 16 Jan 2023 14:47:48 +0100 Subject: [PATCH 062/468] Remove "Leave" button on Room details screen --- .../Settings/RoomSettingsViewController.m | 90 +------------------ 1 file changed, 1 insertion(+), 89 deletions(-) diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index d042a5f18..d958bd2e8 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -52,8 +52,7 @@ enum ROOM_SETTINGS_MAIN_SECTION_ROW_TOPIC, ROOM_SETTINGS_MAIN_SECTION_ROW_TAG, ROOM_SETTINGS_MAIN_SECTION_ROW_DIRECT_CHAT, - ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS, - ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE + ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS }; enum @@ -515,7 +514,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS]; } - [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE]; [tmpSections addObject:sectionMain]; if (RiotSettings.shared.roomSettingsScreenAllowChangingAccessSettings) @@ -2325,22 +2323,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti cell = favoriteCell; } } - else if (row == ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE) - { - MXKTableViewCellWithButton *leaveCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier] forIndexPath:indexPath]; - - NSString* title = [VectorL10n leave]; - - [leaveCell.mxkButton setTitle:title forState:UIControlStateNormal]; - [leaveCell.mxkButton setTitle:title forState:UIControlStateHighlighted]; - [leaveCell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; - leaveCell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; - - [leaveCell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; - [leaveCell.mxkButton addTarget:self action:@selector(onLeave:) forControlEvents:UIControlEventTouchUpInside]; - - cell = leaveCell; - } } else if (section == SECTION_TAG_ACCESS) { @@ -3196,76 +3178,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti #pragma mark - actions -- (void)onLeave:(id)sender -{ - // Prompt user before leaving the room - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - NSString *title, *message; - if ([self.mainSession roomWithRoomId:self.roomId].isDirect) - { - title = [VectorL10n roomParticipantsLeavePromptTitleForDm]; - message = [VectorL10n roomParticipantsLeavePromptMsgForDm]; - } - else - { - title = [VectorL10n roomParticipantsLeavePromptTitle]; - message = [VectorL10n roomParticipantsLeavePromptMsg]; - } - - currentAlert = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n leave] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - [self startActivityIndicator]; - [self->mxRoom leave:^{ - - if (self.delegate) { - [self.delegate roomSettingsViewControllerDidLeaveRoom:self]; - } else { - [[LegacyAppDelegate theDelegate] restoreInitialDisplay:nil]; - } - - } failure:^(NSError *error) { - - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomSettingsViewController] Leave room failed"); - // Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"RoomSettingsVCLeaveAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; -} - - (void)onRoomAvatarTap:(UITapGestureRecognizer *)recognizer { SingleImagePickerPresenter *singleImagePickerPresenter = [[SingleImagePickerPresenter alloc] initWithSession:self.mainSession]; From 286cb45533bbdd9c4c1e740860461f905d51747b Mon Sep 17 00:00:00 2001 From: Philippe Loriaux Date: Mon, 16 Jan 2023 15:02:33 +0100 Subject: [PATCH 063/468] Add Towncrier file --- changelog.d/pr-7275.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7275.change diff --git a/changelog.d/pr-7275.change b/changelog.d/pr-7275.change new file mode 100644 index 000000000..6435f2f71 --- /dev/null +++ b/changelog.d/pr-7275.change @@ -0,0 +1 @@ +Remove "Leave" button on Room details screen From d492f83be1938354e6d8dff80932599c53b064a3 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 16 Jan 2023 12:31:51 +0100 Subject: [PATCH 064/468] Live voice broadcast should not appear in Info Center while playing --- Riot/Modules/Application/LegacyAppDelegate.m | 3 +++ .../VoiceMessageMediaServiceProvider.swift | 26 ++++++++++++++++--- .../VoiceMessageNowPlayingInfoDelegate.swift | 2 ++ .../VoiceBroadcastPlaybackCoordinator.swift | 9 ++++++- .../VoiceBroadcastPlaybackProvider.swift | 6 +++++ .../VoiceBroadcastPlaybackViewModel.swift | 20 +++++++++++--- 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 15cdb8be1..de9e397c3 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -618,6 +618,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Pause Voice Broadcast recording if needed [VoiceBroadcastRecorderProvider.shared pauseRecording]; + + // Pause Voice Broadcast playing if needed + [VoiceBroadcastPlaybackProvider.shared pausePlayingInProgressVoiceBroadcast]; } - (void)applicationWillEnterForeground:(UIApplication *)application diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 24ff08d0d..c6dc8b8b4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -138,7 +138,14 @@ import MediaPlayer func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { currentlyPlayingAudioPlayer = audioPlayer activeAudioPlayers.insert(audioPlayer) - setUpRemoteCommandCenter() + + let shouldSetupRemoteCommandCenter = nowPlayingInfoDelegates.object(forKey: audioPlayer)?.shouldSetupRemoteCommandCenter(audioPlayer: audioPlayer) ?? true + if shouldSetupRemoteCommandCenter { + setUpRemoteCommandCenter() + } else { + // clean up the remote command center + tearDownRemoteCommandCenter() + } pauseAllServicesExcept(audioPlayer) } @@ -150,7 +157,7 @@ import MediaPlayer // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter(for: audioPlayer) + tearDownRemoteCommandCenter() } } activeAudioPlayers.remove(audioPlayer) @@ -164,7 +171,7 @@ import MediaPlayer // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it) if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true { currentlyPlayingAudioPlayer = nil - tearDownRemoteCommandCenter(for: audioPlayer) + tearDownRemoteCommandCenter() } } activeAudioPlayers.remove(audioPlayer) @@ -264,13 +271,24 @@ import MediaPlayer } } - private func tearDownRemoteCommandCenter(for audioPlayer: VoiceMessageAudioPlayer) { + private func tearDownRemoteCommandCenter() { displayLink.isPaused = true UIApplication.shared.endReceivingRemoteControlEvents() let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() nowPlayingInfoCenter.nowPlayingInfo = nil + nowPlayingInfoCenter.playbackState = .stopped + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = false + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.isEnabled = false + commandCenter.pauseCommand.removeTarget(nil) + commandCenter.skipForwardCommand.isEnabled = false + commandCenter.skipForwardCommand.removeTarget(nil) + commandCenter.skipBackwardCommand.isEnabled = false + commandCenter.skipBackwardCommand.removeTarget(nil) } private func updateNowPlayingInfoCenter() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift index 6955b4c0f..959f8ca91 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift @@ -20,5 +20,7 @@ import Foundation func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) + func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: VoiceMessageAudioPlayer) -> Bool } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 3f5e55c6e..4369419bc 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -80,8 +80,15 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } func endVoiceBroadcast() {} - + func pausePlaying() { viewModel.context.send(viewAction: .pause) } + + func pausePlayingInProgressVoiceBroadcast() { + // Pause the playback if we are playing a live voice broadcast (or waiting for more chunks) + if [.playing, .buffering].contains(viewModel.context.viewState.playbackState), viewModel.context.viewState.broadcastState != .stopped { + viewModel.context.send(viewAction: .pause) + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 3f28ab081..94c10eea4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -78,6 +78,12 @@ import Foundation } } + @objc public func pausePlayingInProgressVoiceBroadcast() { + coordinatorsForEventIdentifiers.forEach { _, coordinator in + coordinator.pausePlayingInProgressVoiceBroadcast() + } + } + private func handleEvent(event: MXEvent, direction: MXTimelineDirection, customObject: Any?) { if direction == .backwards { // ignore backwards events diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 56e435757..5269f67c2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -490,12 +490,22 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoDelegate { + func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool { + guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else { + return false + } + + // we should setup the remote command center only for ended voice broadcast because we won't get new chunk if the app is in background. + return state.broadcastState == .stopped + } + func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool { guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else { return true } - return state.broadcastState == .stopped + // we should disconnect from the now playing info center if the playback is stopped or if the broadcast is in progress + return state.playbackState == .stopped || state.broadcastState != .stopped } func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) { @@ -503,12 +513,16 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoDelegate { return } + // Don't update the NowPlayingInfoCenter for live broadcasts + guard state.broadcastState == .stopped else { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + return + } + let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() nowPlayingInfoCenter.nowPlayingInfo = [ // Title MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder, - // Buffering status (using the "artist" property to display it under the title) - MPMediaItemPropertyArtist: state.playbackState == .buffering ? VectorL10n.voiceBroadcastBuffering : "", // Duration MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any, // Elapsed time From 763d7d1a8a4d6f647a6c7aeedeabe15f6fafcfac Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 16 Jan 2023 16:00:18 +0100 Subject: [PATCH 065/468] Make sure we store the last block sequence sent (even if it's 0) --- .../VoiceBroadcastSDK/VoiceBroadcastAggregator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index 1741dc14c..39264b42c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -170,8 +170,8 @@ public class VoiceBroadcastAggregator { let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else { return } - // For .pause and .stopped, if there is a lastChunkSequence (ie its value is > 0), we store it - if [.stopped, .paused].contains(state), voiceBroadcastInfo.lastChunkSequence > 0 { + // For .pause and .stopped, update the last chunk sequence + if [.stopped, .paused].contains(state) { self.voiceBroadcastLastChunkSequence = voiceBroadcastInfo.lastChunkSequence } self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state) From 4dbe54bad1def888aa624891891d100c109530e3 Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Mon, 16 Jan 2023 18:15:49 +0000 Subject: [PATCH 066/468] Unexpected live voice broadcast (#7269) --- Riot/Utils/EventFormatter.m | 112 ++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 079a00f77..21cd496dd 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -546,8 +546,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } #pragma mark - MXRoomSummaryUpdating -- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState { - +- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState +{ // Do not display voice broadcast chunk in last message. if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType]) { @@ -555,31 +555,94 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } // Update last message if we have a voice broadcast in the room. - if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) + MXEvent *lastVoiceBroadcastInfoEvent = [self lastVoiceBroadcastInfoEventWithEvent:event roomState:roomState]; + if (lastVoiceBroadcastInfoEvent != nil) { - return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:event roomState:roomState]; - } - else - { - MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; - if (stateEvent && ![VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]) + MXEvent *voiceBroadcastInfoStartedEvent = [self voiceBroadcastInfoStartedEventWithEvent:lastVoiceBroadcastInfoEvent + roomId:summary.roomId + session:session]; + if (voiceBroadcastInfoStartedEvent != nil + && !(voiceBroadcastInfoStartedEvent.isRedactedEvent || [voiceBroadcastInfoStartedEvent.eventId isEqualToString:event.redacts])) { - return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:stateEvent roomState:roomState]; + return [self session:session + updateRoomSummary:summary +withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent + voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent roomState:roomState]; } } BOOL updated = [super session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState]; - if (updated) { + if (updated) + { // Force the default text color for the last message (cancel highlighted message color) NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText]; - [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor range:NSMakeRange(0, lastEventDescription.length)]; + [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor + range:NSMakeRange(0, lastEventDescription.length)]; summary.lastMessage.attributedText = lastEventDescription; } return updated; } + +- (MXEvent *)lastVoiceBroadcastInfoEventWithEvent:(MXEvent *)event roomState:(MXRoomState *)roomState +{ + MXEvent *voiceBroadcastInfoEvent = nil; + VoiceBroadcastInfo *info = nil; + if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) + { + info = [VoiceBroadcastInfo modelFromJSON: event.content]; + + if (info != nil) + { + voiceBroadcastInfoEvent = event; + } + } + else + { + MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; + if (stateEvent != nil) + { + info = [VoiceBroadcastInfo modelFromJSON: stateEvent.content]; + if (info != nil && ![VoiceBroadcastInfo isStoppedFor:info.state]) + { + voiceBroadcastInfoEvent = stateEvent; + } + } + } + + return voiceBroadcastInfoEvent; +} + +- (MXEvent *)voiceBroadcastInfoStartedEventWithEvent:(MXEvent *)voiceBroadcastInfoEvent roomId:(NSString *)roomId session:(MXSession *)session +{ + VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: voiceBroadcastInfoEvent.content]; + if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state]) + { + return voiceBroadcastInfoEvent; + } + else + { + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + + __block MXEvent *voiceBroadcastInfoStartedEvent; + + [session eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId success:^(MXEvent *resultEvent) { + voiceBroadcastInfoStartedEvent = resultEvent; + dispatch_group_leave(group); + } failure:^(NSError *error) { + MXLogErrorDetails(@"[EventFormatter] Fetch eventWithEventId with error = %@", error.description); + dispatch_group_leave(group); + }]; + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + + return voiceBroadcastInfoStartedEvent; + } +} + - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray *)stateEvents roomState:(MXRoomState *)roomState { BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState]; @@ -603,18 +666,29 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; return updated; } -- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent roomState:(MXRoomState *)roomState +- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent voiceBroadcastInfoStartedEvent:(MXEvent *)voiceBroadcastInfoStartedEvent roomState:(MXRoomState *)roomState { - [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:stateEvent]]; - if (summary.lastMessage.others == nil) + BOOL isStoppedVoiceBroadcast = [VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]; + + if ([summary.lastMessage.eventId isEqualToString:voiceBroadcastInfoStartedEvent.eventId]) { - summary.lastMessage.others = [NSMutableDictionary dictionary]; + if (!isStoppedVoiceBroadcast) + { + return NO; + } + } + else + { + [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:voiceBroadcastInfoStartedEvent]]; + if (summary.lastMessage.others == nil) + { + summary.lastMessage.others = [NSMutableDictionary dictionary]; + } } - summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES]; NSAttributedString *attachmentString = nil; UIColor *textColor; - if ([VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state]) + if (isStoppedVoiceBroadcast) { textColor = ThemeService.shared.theme.textSecondaryColor; NSString *senderDisplayName; @@ -627,6 +701,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; senderDisplayName = [self senderDisplayNameForEvent:stateEvent withRoomState:roomState]; summary.lastMessage.text = [VectorL10n noticeVoiceBroadcastEnded:senderDisplayName]; } + summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES]; } else { @@ -638,6 +713,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastLive; + summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:voiceBroadcastInfoStartedEvent withTime:YES]; } // Compute the attribute text message From 69e5f4b72c428745a46347d9d4d8de6fd7c91fac Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 17 Jan 2023 11:00:08 +0100 Subject: [PATCH 067/468] Fix voicebroadcast playback slider on seek --- .../VoiceBroadcastPlaybackViewModel.swift | 41 +++++++++++++------ changelog.d/7252.bugfix | 1 + 2 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 changelog.d/7252.bugfix diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index fcd58b1d9..ffe713e73 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -60,6 +60,20 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration) } + private var playingChunk: VoiceBroadcastChunk? { + guard let currentAudioPlayerUrl = audioPlayer?.currentUrl, + let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in + result.url == currentAudioPlayerUrl + })?.eventIdentifier else { + return nil + } + + let playingChunk = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in + chunk.attachment.eventId == playingEventId + }) + return playingChunk + } + private var isLivePlayback: Bool { return (!isPlaybackInitialized || isPlayingLastChunk) && (state.broadcastState == .started || state.broadcastState == .resumed) } @@ -392,20 +406,19 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } @objc private func handleDisplayLinkTick() { - guard let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in - result.url == audioPlayer?.currentUrl - })?.eventIdentifier, - let playingSequence = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in - chunk.attachment.eventId == playingEventId - })?.sequence else { + guard let playingSequence = self.playingChunk?.sequence else { return } - - let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in - chunk.sequence < playingSequence - }.reduce(0) { $0 + $1.duration}) + (audioPlayer?.currentTime.rounded() ?? 0) * 1000 - - state.bindings.progress = Float(progress) + + // Get the audioPlayer current time, which is the elapsed time in the currently playing media item. + // Note: if the audioPlayer is not ready (eg. after a seek), its currentTime will be 0 and we shouldn't update the progress to avoid visual glitches. + let currentTime = audioPlayer?.currentTime ?? .zero + if currentTime > 0 { + let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in + chunk.sequence < playingSequence + }.reduce(0) { $0 + $1.duration}) + currentTime * 1000 + state.bindings.progress = Float(progress) + } updateUI() } @@ -467,7 +480,6 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { } func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { - updateDuration() if state.playbackState != .stopped, !isActuallyPaused { @@ -486,11 +498,13 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { state.playbackState = .playing state.playingState.isLive = isLivePlayback isPlaybackInitialized = true + displayLink.isPaused = false } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { state.playbackState = .paused state.playingState.isLive = false + displayLink.isPaused = true } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -500,6 +514,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { audioPlayer.deregisterDelegate(self) self.mediaServiceProvider.deregisterNowPlayingInfoDelegate(forPlayer: audioPlayer) self.audioPlayer = nil + displayLink.isPaused = true } func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { diff --git a/changelog.d/7252.bugfix b/changelog.d/7252.bugfix new file mode 100644 index 000000000..0e823509f --- /dev/null +++ b/changelog.d/7252.bugfix @@ -0,0 +1 @@ +Voice Broadcast: Fixed an issue where the voice broadcast audio player progress bar behaved unexpectedly. From 947f67ec93bfaa7dd4810b9d22a8bfbd20d4e01e Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Tue, 17 Jan 2023 14:49:36 +0000 Subject: [PATCH 068/468] Handle a connection issue when we try to start a new voice broadcast (#7276) --- Riot/Assets/en.lproj/Vector.strings | 4 +++- Riot/Generated/Strings.swift | 8 ++++++++ Riot/Modules/Room/RoomViewController.m | 14 ++++++++++---- .../VoiceBroadcastSDK/VoiceBroadcastService.swift | 5 ++++- changelog.d/7234.change | 1 + 5 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 changelog.d/7234.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c14597906..576df0110 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2208,6 +2208,7 @@ Tap the + to start adding people."; "voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one."; "voice_broadcast_already_in_progress_message" = "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; +"voice_broadcast_playback_lock_screen_placeholder" = "Voice broadcast"; "voice_broadcast_live" = "Live"; "voice_broadcast_tile" = "Voice broadcast"; "voice_broadcast_time_left" = "%@ left"; @@ -2217,7 +2218,8 @@ Tap the + to start adding people."; "voice_broadcast_stop_alert_agree_button" = "Yes, stop"; "voice_broadcast_voip_cannot_start_title" = "Can’t start a call"; "voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call."; -"voice_broadcast_playback_lock_screen_placeholder" = "Voice broadcast"; +"voice_broadcast_connection_error_title" = "Connection error"; +"voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later."; // MARK: - Version check diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index c9bb71b43..6d5ef8e9a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9207,6 +9207,14 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBuffering: String { return VectorL10n.tr("Vector", "voice_broadcast_buffering") } + /// Unfortunately we’re unable to start a recording right now. Please try again later. + public static var voiceBroadcastConnectionErrorMessage: String { + return VectorL10n.tr("Vector", "voice_broadcast_connection_error_message") + } + /// Connection error + public static var voiceBroadcastConnectionErrorTitle: String { + return VectorL10n.tr("Vector", "voice_broadcast_connection_error_title") + } /// Live public static var voiceBroadcastLive: String { return VectorL10n.tr("Vector", "voice_broadcast_live") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 87b7e858f..a1a0b6b6c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2454,13 +2454,19 @@ static CGSize kThreadListBarButtonItemImageSize; // Prevents listening a VB when recording a new one [VoiceBroadcastPlaybackProvider.shared pausePlaying]; + // Check connectivity + if ([AppDelegate theDelegate].isOffline) + { + [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]]; + return; + } + // Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room [session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) { if (voiceBroadcastService) { - [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { - - } failure:^(NSError * _Nonnull error) { - + [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { } failure:^(NSError * _Nonnull error) { + [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]]; + [session tearDownVoiceBroadcastService]; }]; } else diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index 8c707eab2..c41d5d37d 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -186,7 +186,7 @@ public class VoiceBroadcastService: NSObject { return } - self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), + let httpOperation = self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), content: stateEventContent, stateKey: stateKey) { [weak self] response in guard let self = self else { return } @@ -199,6 +199,9 @@ public class VoiceBroadcastService: NSObject { } taskCompleted() } + + // No retry to send the request + httpOperation.maxNumberOfTries = 0 } } } diff --git a/changelog.d/7234.change b/changelog.d/7234.change new file mode 100644 index 000000000..52af7685d --- /dev/null +++ b/changelog.d/7234.change @@ -0,0 +1 @@ +Handle a connection issue when we try to start a new voice broadcast. From a898d6fda89f8205d10d2bc0d16c002a7457b283 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 17 Jan 2023 16:44:56 +0100 Subject: [PATCH 069/468] Improve PollListItem to support ended polls --- .../MockPollHistoryScreenState.swift | 4 +- .../Room/PollHistory/PollHistoryModels.swift | 3 +- .../PollHistory/PollHistoryViewModel.swift | 19 ++++- .../Service/Mock/MockPollHistoryService.swift | 27 +++++-- .../Room/PollHistory/View/PollHistory.swift | 3 + .../Room/PollHistory/View/PollListItem.swift | 70 ++++++++++++++++++- 6 files changed, 113 insertions(+), 13 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index c00ccf528..65d393957 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -45,10 +45,10 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollHistoryMode = .past case .activeEmpty: pollHistoryMode = .active - pollService.pollListData = [] + pollService.activePollsData = [] case .pastEmpty: pollHistoryMode = .past - pollService.pollListData = [] + pollService.pastPollsData = [] } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index c8960add0..93ef30819 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -33,7 +33,7 @@ struct PollHistoryViewBindings { struct PollHistoryViewState: BindableState { init(mode: PollHistoryMode) { - self.bindings = .init(mode: mode) + bindings = .init(mode: mode) } var bindings: PollHistoryViewBindings @@ -42,4 +42,5 @@ struct PollHistoryViewState: BindableState { enum PollHistoryViewAction { case viewAppeared + case segmentDidChange } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 0addb8ef9..4199251da 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -20,6 +20,7 @@ typealias PollHistoryViewModelType = StateStoreViewModel? { didSet { oldValue?.cancel() @@ -39,6 +40,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel switch viewAction { case .viewAppeared: fetchingTask = fetchPolls() + case .segmentDidChange: + updatePolls() } } } @@ -53,8 +56,22 @@ private extension PollHistoryViewModel { } await MainActor.run { - state.polls = polls + self.polls = polls + updatePolls() } } } + + func updatePolls() { + let renderedPolls: [PollListData] + + switch context.mode { + case .active: + renderedPolls = polls.filter { $0.winningOption == nil } + case .past: + renderedPolls = polls.filter { $0.winningOption != nil } + } + + state.polls = renderedPolls + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 67d312181..62796963d 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -15,15 +15,30 @@ // final class MockPollHistoryService: PollHistoryServiceProtocol { - var pollListData: [PollListData] = (1..<10) + var activePollsData: [PollListData] = (1..<10) .map { index in - PollListData(startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), question: "Do you like the poll number \(index)?") - } - .sorted { poll1, poll2 in - poll1.startDate > poll2.startDate + PollListData( + startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), + question: "Do you like the active poll number \(index)?", + numberOfVotes: 30, + winningOption: nil + ) } + var pastPollsData: [PollListData] = (1..<10) + .map { index in + PollListData( + startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), + question: "Do you like the past poll number \(index)?", + numberOfVotes: 30, + winningOption: .init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true) + ) + } + func fetchHistory() async throws -> [PollListData] { - pollListData + (activePollsData + pastPollsData) + .sorted { poll1, poll2 in + poll1.startDate > poll2.startDate + } } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index b1208f547..cd9001a56 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -45,6 +45,9 @@ struct PollHistory: View { .onAppear { viewModel.send(viewAction: .viewAppeared) } + .onChange(of: viewModel.mode) { newValue in + viewModel.send(viewAction: .segmentDidChange) + } } private var pollListView: some View { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 373ea0202..98c5f06bd 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -19,6 +19,8 @@ import SwiftUI struct PollListData { let startDate: Date let question: String + let numberOfVotes: UInt + let winningOption: TimelinePollAnswerOption? } struct PollListItem: View { @@ -36,18 +38,62 @@ struct PollListItem: View { Text(pollData.formattedDate) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) - + HStack(alignment: .firstTextBaseline, spacing: 8) { Image(uiImage: Asset.Images.pollHistory.image) .resizable() .frame(width: imageSize, height: imageSize) - + Text(pollData.question) .foregroundColor(theme.colors.primaryContent) .font(theme.fonts.body) .lineLimit(2) .accessibilityLabel("PollListItem.title") } + + if pollData.winningOption != nil { + optionView(winningOption: pollData.winningOption!) + } + } + } + + private var clipShape: some Shape { + RoundedRectangle(cornerRadius: 4.0) + } + + private func optionView(winningOption: TimelinePollAnswerOption) -> some View { + VStack(alignment: .leading, spacing: 12.0) { + HStack(alignment: .top, spacing: 8.0) { + Text(pollData.winningOption!.text) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + + Spacer() + + votesText(winningOption: winningOption) + } + + ProgressView(value: Double(winningOption.count), + total: Double(pollData.numberOfVotes)) + .progressViewStyle(LinearProgressViewStyle()) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8.0) + .padding(.top, 12.0) + .padding(.bottom, 12.0) + .clipShape(clipShape) + .overlay(clipShape.stroke(theme.colors.accent, lineWidth: 1.0)) + .accentColor(theme.colors.accent) + } + + private func votesText(winningOption: TimelinePollAnswerOption) -> some View { + Label { + Text(winningOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(winningOption.count))) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.accent) + } icon: { + Image(uiImage: Asset.Images.pollWinnerIcon.image) } } } @@ -72,6 +118,24 @@ private extension DateFormatter { struct PollListItem_Previews: PreviewProvider { static var previews: some View { - PollListItem(pollData: .init(startDate: .init(), question: "Did you like this poll?")) + Group { + let pollData1 = PollListData( + startDate: .init(), + question: "Do you like polls?", + numberOfVotes: 30, + winningOption: .init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true) + ) + + PollListItem(pollData: pollData1) + + let pollData2 = PollListData( + startDate: .init(), + question: "Do you like polls?", + numberOfVotes: 30, + winningOption: nil) + + PollListItem(pollData: pollData2) + } + .padding() } } From 0b5e009f23b7f851c69a6446446891a7654b953c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 17 Jan 2023 16:54:18 +0100 Subject: [PATCH 070/468] Add results view --- .../Room/PollHistory/View/PollListItem.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 98c5f06bd..c2447fa1e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -52,7 +52,10 @@ struct PollListItem: View { } if pollData.winningOption != nil { - optionView(winningOption: pollData.winningOption!) + VStack(alignment: .leading, spacing: 12) { + optionView(winningOption: pollData.winningOption!) + resultView + } } } } @@ -96,6 +99,14 @@ struct PollListItem: View { Image(uiImage: Asset.Images.pollWinnerIcon.image) } } + + private var resultView: some View { + let text = pollData.numberOfVotes == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.numberOfVotes)) + + return Text(text) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + } } private extension PollListData { From 6d2584713a9a5a6a8b7354bc761f42ddd74e1e6c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 17 Jan 2023 17:35:44 +0100 Subject: [PATCH 071/468] Add ui tests --- .../Test/UI/PollHistoryUITests.swift | 26 +++++++++++++++++-- .../Room/PollHistory/View/PollHistory.swift | 2 +- .../Room/PollHistory/View/PollListItem.swift | 1 + 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 3fcc05c7b..5867b88e8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -18,29 +18,51 @@ import RiotSwiftUI import XCTest class PollHistoryUITests: MockScreenTestCase { - func testPollHistoryHasContent() { + func testActivePollHistoryHasContent() { app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title) let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle] + let winningOption = app.staticTexts["PollListData.winningOption"] + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertTrue(items.exists) XCTAssertFalse(emptyText.exists) XCTAssertTrue(selectedSegment.exists) XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertFalse(winningOption.exists) } - func testPollHistoryShowsEmptyScreen() { + func testPastPollHistoryHasContent() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.past.title) + let title = app.navigationBars.firstMatch.identifier + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] + let winningOption = app.staticTexts["PollListData.winningOption"] + + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertTrue(selectedSegment.exists) + XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertTrue(winningOption.exists) + } + + func testPastPollHistoryIsEmpty() { app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title) let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] + let winningOption = app.staticTexts["PollListData.winningOption"] + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertFalse(items.exists) XCTAssertTrue(emptyText.exists) XCTAssertTrue(selectedSegment.exists) XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertFalse(winningOption.exists) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index cd9001a56..7cf9bb45a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -45,7 +45,7 @@ struct PollHistory: View { .onAppear { viewModel.send(viewAction: .viewAppeared) } - .onChange(of: viewModel.mode) { newValue in + .onChange(of: viewModel.mode) { _ in viewModel.send(viewAction: .segmentDidChange) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index c2447fa1e..b810f098f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -70,6 +70,7 @@ struct PollListItem: View { Text(pollData.winningOption!.text) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) + .accessibilityLabel("PollListData.winningOption") Spacer() From 282caa93d0fa597709bdb112e4f3f79dceb4ceea Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 17 Jan 2023 17:42:14 +0100 Subject: [PATCH 072/468] Add changelog.d file --- changelog.d/pr-7278.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7278.change diff --git a/changelog.d/pr-7278.change b/changelog.d/pr-7278.change new file mode 100644 index 000000000..f7254ebb9 --- /dev/null +++ b/changelog.d/pr-7278.change @@ -0,0 +1 @@ +Polls: poll history UI for past polls. From a3628bf62237db397db097f5143d05f7f53338eb Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 17 Jan 2023 18:10:35 +0100 Subject: [PATCH 073/468] save button improvement and tests updated --- .../xcshareddata/swiftpm/Package.resolved | 3 ++- .../Model/ComposerLinkActionModel.swift | 27 +++++++++++++------ .../Test/UI/ComposerLinkActionUITests.swift | 12 ++++----- .../ComposerLinkActionViewModelTests.swift | 27 +++++++------------ project.yml | 2 +- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 102f87715..34484b0a7 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "534ee5bae5e8de69ed398937b5edb7b5f21551d2" + "revision" : "6927cb878376136c4a03d919b689af8dfbdad080", + "version" : "0.19.0" } }, { diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift index b47c5bcd5..fdf92cab5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift @@ -59,20 +59,31 @@ extension ComposerLinkActionViewState { } var isSaveButtonDisabled: Bool { - guard isValidLink else { return true } + guard !bindings.linkUrl.isEmpty else { return true } switch linkAction { case .createWithText: return bindings.text.isEmpty - default: return false + case .create: return false + case .edit: return !bindings.hasEditedUrl } } - - private var isValidLink: Bool { - guard let url = URL(string: bindings.linkUrl) else { return false } - return UIApplication.shared.canOpenURL(url) - } } struct ComposerLinkActionBindings { var text: String - var linkUrl: String + + private let initialLinkUrl: String + fileprivate var hasEditedUrl = false + var linkUrl: String { + didSet { + if !hasEditedUrl && linkUrl != initialLinkUrl { + hasEditedUrl = true + } + } + } + + init(text: String, linkUrl: String) { + self.text = text + self.linkUrl = linkUrl + self.initialLinkUrl = linkUrl + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift index f30dacf30..c18405951 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift @@ -29,9 +29,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase { let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) linkTextField.tap() - linkTextField.typeText("invalid url") - XCTAssertFalse(saveButton.isEnabled) - linkTextField.clearAndTypeText("https://element.io") + linkTextField.clearAndTypeText("element.io") XCTAssertTrue(saveButton.isEnabled) } @@ -47,7 +45,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase { let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) linkTextField.tap() - linkTextField.typeText("https://element.io") + linkTextField.typeText("element.io") XCTAssertFalse(saveButton.isEnabled) textTextField.tap() textTextField.typeText("test") @@ -60,13 +58,15 @@ final class ComposerLinkActionUITests: MockScreenTestCase { XCTAssertTrue(app.buttons[VectorL10n.cancel].exists) let saveButton = app.buttons[VectorL10n.save] XCTAssertTrue(saveButton.exists) - XCTAssertTrue(saveButton.isEnabled) + XCTAssertFalse(saveButton.isEnabled) XCTAssertFalse(app.textFields["textTextField"].exists) let linkTextField = app.textFields["linkTextField"] XCTAssertTrue(linkTextField.exists) let value = linkTextField.value as? String XCTAssertEqual(value, "https://element.io") - linkTextField.clearAndTypeText("invalid url") + linkTextField.clearAndTypeText("") XCTAssertFalse(saveButton.isEnabled) + linkTextField.clearAndTypeText("matrix.org") + XCTAssertTrue(saveButton.isEnabled) } } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift index 40ad27358..2407eccc4 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift @@ -53,29 +53,20 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testEditDefaultState() { - let link = "https://element.io" + let link = "element.io" setUp(with: .edit(link: link)) XCTAssertEqual(context.viewState.bindings.text, "") XCTAssertEqual(context.viewState.bindings.linkUrl, link) - XCTAssertFalse(context.viewState.isSaveButtonDisabled) + XCTAssertTrue(context.viewState.isSaveButtonDisabled) XCTAssertTrue(context.viewState.shouldDisplayRemoveButton) XCTAssertFalse(context.viewState.shouldDisplayTextField) XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionEditTitle) } - func testUrlValidityCheck() { - setUp(with: .create) - XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "invalid url" - XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "https://element.io" - XCTAssertFalse(context.viewState.isSaveButtonDisabled) - } - func testTextNotEmptyCheck() { setUp(with: .createWithText) XCTAssertTrue(context.viewState.isSaveButtonDisabled) - context.linkUrl = "https://element.io" + context.linkUrl = "element.io" XCTAssertTrue(context.viewState.isSaveButtonDisabled) context.text = "text" XCTAssertFalse(context.viewState.isSaveButtonDisabled) @@ -92,7 +83,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testRemoveAction() { - setUp(with: .edit(link: "https://element.io")) + setUp(with: .edit(link: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value @@ -107,7 +98,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { viewModel.callback = { value in result = value } - let link = "https://element.io" + let link = "element.io" context.linkUrl = link context.send(viewAction: .save) XCTAssertEqual(result, .performOperation(.setLink(urlString: link))) @@ -119,7 +110,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { viewModel.callback = { value in result = value } - let link = "https://element.io" + let link = "element.io" context.linkUrl = link let text = "test" context.text = text @@ -128,13 +119,15 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testSaveActionForEdit() { - setUp(with: .edit(link: "https://element.io")) + setUp(with: .edit(link: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value } - let link = "https://matrix.org" + XCTAssertTrue(context.viewState.isSaveButtonDisabled) + let link = "matrix.org" context.linkUrl = link + XCTAssertFalse(context.viewState.isSaveButtonDisabled) context.send(viewAction: .save) XCTAssertEqual(result, .performOperation(.setLink(urlString: link))) } diff --git a/project.yml b/project.yml index f2c4dbc23..fe9af33a8 100644 --- a/project.yml +++ b/project.yml @@ -53,7 +53,7 @@ packages: branch: main WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - revision: 534ee5bae5e8de69ed398937b5edb7b5f21551d2 + version: 0.19.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 55306d74aea854f677cf2f88670048edf14c1528 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 17 Jan 2023 18:32:36 +0100 Subject: [PATCH 074/468] changelog --- changelog.d/7279.change | 1 + changelog.d/7280.change | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/7279.change create mode 100644 changelog.d/7280.change diff --git a/changelog.d/7279.change b/changelog.d/7279.change new file mode 100644 index 000000000..c605f8920 --- /dev/null +++ b/changelog.d/7279.change @@ -0,0 +1 @@ +Rich Text Editor: https:// or mailto: scheme is automatically added when creating a link if no scheme is specified. diff --git a/changelog.d/7280.change b/changelog.d/7280.change new file mode 100644 index 000000000..d387d563d --- /dev/null +++ b/changelog.d/7280.change @@ -0,0 +1 @@ +Rich Text Editor: Adding a link over a blank selection, prompts the user to create a new link with new text to replace such selection. From 63d8c4f6b436281a58046e88a61f7dcf76dd9910 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 10:12:03 +0100 Subject: [PATCH 075/468] Fix accessibility id --- RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index b810f098f..335d62c39 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -70,7 +70,7 @@ struct PollListItem: View { Text(pollData.winningOption!.text) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) - .accessibilityLabel("PollListData.winningOption") + .accessibilityIdentifier("PollListData.winningOption") Spacer() From 129140b7293093b4ac171042a58371fcd3046faa Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 11:22:40 +0100 Subject: [PATCH 076/468] Add new localisation for ended poll replies --- Riot/Assets/en.lproj/Vector.strings | 2 ++ Riot/Generated/Strings.swift | 4 ++++ .../MXKSendReplyEventStringLocalizer.swift | 20 +++++++++++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 95531c6da..e2d45c94e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2368,6 +2368,8 @@ Tap the + to start adding people."; "poll_timeline_ended_text" = "Ended the poll"; +"poll_timeline_reply_ended_poll" = "Ended poll"; + // MARK: - Location sharing "location_sharing_title" = "Location"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 105a5d70e..e09874809 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4883,6 +4883,10 @@ public class VectorL10n: NSObject { public static var pollTimelineOneVote: String { return VectorL10n.tr("Vector", "poll_timeline_one_vote") } + /// Ended poll + public static var pollTimelineReplyEndedPoll: String { + return VectorL10n.tr("Vector", "poll_timeline_reply_ended_poll") + } /// Final results based on %lu votes public static func pollTimelineTotalFinalResults(_ p1: Int) -> String { return VectorL10n.tr("Vector", "poll_timeline_total_final_results", p1) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift index f22c779fd..4c942bccd 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift @@ -18,34 +18,38 @@ import Foundation class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalizerProtocol { func senderSentAnImage() -> String { - return VectorL10n.messageReplyToSenderSentAnImage + VectorL10n.messageReplyToSenderSentAnImage } func senderSentAVideo() -> String { - return VectorL10n.messageReplyToSenderSentAVideo + VectorL10n.messageReplyToSenderSentAVideo } func senderSentAnAudioFile() -> String { - return VectorL10n.messageReplyToSenderSentAnAudioFile + VectorL10n.messageReplyToSenderSentAnAudioFile } func senderSentAVoiceMessage() -> String { - return VectorL10n.messageReplyToSenderSentAVoiceMessage + VectorL10n.messageReplyToSenderSentAVoiceMessage } func senderSentAFile() -> String { - return VectorL10n.messageReplyToSenderSentAFile + VectorL10n.messageReplyToSenderSentAFile } func senderSentTheirLocation() -> String { - return VectorL10n.messageReplyToSenderSentTheirLocation + VectorL10n.messageReplyToSenderSentTheirLocation } func senderSentTheirLiveLocation() -> String { - return VectorL10n.messageReplyToSenderSentTheirLiveLocation + VectorL10n.messageReplyToSenderSentTheirLiveLocation } func messageToReplyToPrefix() -> String { - return VectorL10n.messageReplyToMessageToReplyToPrefix + VectorL10n.messageReplyToMessageToReplyToPrefix + } + + func replyToEndedPoll() -> String { + VectorL10n.pollTimelineReplyEndedPoll } } From ba7c0f04844d9a9a0c6ecdf0ef45e64d4b1f7070 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 12:43:11 +0100 Subject: [PATCH 077/468] =?UTF-8?q?Add=20replacement=20logic=20for=20?= =?UTF-8?q?=E2=80=9CEnded=20poll=E2=80=9D=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/EventFormatter/MXKEventFormatter.m | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index cee8dccec..4a7f6dc0e 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -31,6 +31,7 @@ #import "GeneratedInterface-Swift.h" static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; +static NSString *const kEndedPollPattern = @".*
.*
(.*)
"; @interface MXKEventFormatter () { @@ -1808,6 +1809,7 @@ static NSString *const kHTMLATagRegexPattern = @"( } html = [self renderReplyTo:html withRoomState:roomState]; + html = [self renderPollEndedReplyTo:html repliedEvent:repliedEvent]; } // Apply the css style that corresponds to the event state @@ -2014,6 +2016,39 @@ static NSString *const kHTMLATagRegexPattern = @"( return html; } +- (NSString*)renderPollEndedReplyTo:(NSString*)htmlString repliedEvent:(MXEvent*)repliedEvent { + static NSRegularExpression *endedPollRegex; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kEndedPollPattern options:NSRegularExpressionCaseInsensitive error:nil]; + }); + + NSTextCheckingResult* match = [endedPollRegex firstMatchInString:htmlString options:0 range:NSMakeRange(0, htmlString.length)]; + NSString* finalString = htmlString; + + if (!(match && match.numberOfRanges > 1)) { + // no useful match found + return finalString; + } + + NSRange groupRange = [match rangeAtIndex:1]; + NSString* replacementText; + + if (repliedEvent) { + MXEvent* pollStartedEvent = [mxSession.store eventWithEventId:repliedEvent.relatesTo.eventId inRoom:repliedEvent.roomId]; + replacementText = [MXEventContentPollStart modelFromJSON:pollStartedEvent.content].question; + } + + if (replacementText == nil) { + replacementText = VectorL10n.pollTimelineReplyEndedPoll; + } + + finalString = [htmlString stringByReplacingCharactersInRange:groupRange withString:replacementText]; + + return finalString; +} + - (void)postFormatMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEvent:(MXEvent*)event andRepliedEvent:(MXEvent*)repliedEvent From b576b90448a66f4791a7ac6d02bea7de2ac1ce33 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 15:05:35 +0100 Subject: [PATCH 078/468] Improve formatter --- .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 4a7f6dc0e..1e7d53fe9 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -2024,9 +2024,14 @@ static NSString *const kEndedPollPattern = @".*
.*
(.*)< endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kEndedPollPattern options:NSRegularExpressionCaseInsensitive error:nil]; }); - NSTextCheckingResult* match = [endedPollRegex firstMatchInString:htmlString options:0 range:NSMakeRange(0, htmlString.length)]; NSString* finalString = htmlString; + if (repliedEvent.eventType != MXEventTypePollEnd) { + return finalString; + } + + NSTextCheckingResult* match = [endedPollRegex firstMatchInString:htmlString options:0 range:NSMakeRange(0, htmlString.length)]; + if (!(match && match.numberOfRanges > 1)) { // no useful match found return finalString; From a65bb5a826e4297c04bc7893bc2736206337be64 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 16:10:15 +0100 Subject: [PATCH 079/468] =?UTF-8?q?Handle=20edge=20cases=20for=20plain=20?= =?UTF-8?q?=E2=80=9Cbody=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 1e7d53fe9..c8dce874a 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1882,6 +1882,12 @@ static NSString *const kEndedPollPattern = @".*
.*
(.*)< { MXJSONModelSetString(repliedEventContent, repliedEvent.content[kMXMessageBodyKey]); } + if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollStart) { + repliedEventContent = [MXEventContentPollStart modelFromJSON:repliedEvent.content].question; + } + if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollEnd) { + repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.replyToEndedPoll; + } } // No message content in a non-redacted event. Formatter should use fallback. From a9791f507a14735929c60583de8fc367bdf83265 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 17:22:38 +0100 Subject: [PATCH 080/468] Improve code --- .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index c8dce874a..422e08990 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -31,7 +31,7 @@ #import "GeneratedInterface-Swift.h" static NSString *const kHTMLATagRegexPattern = @"
([^<]*)"; -static NSString *const kEndedPollPattern = @".*
.*
(.*)
"; +static NSString *const kRepliedTextPattern = @".*
.*
(.*)
"; @interface MXKEventFormatter () { @@ -2027,7 +2027,7 @@ static NSString *const kEndedPollPattern = @".*
.*
(.*)< static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kEndedPollPattern options:NSRegularExpressionCaseInsensitive error:nil]; + endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kRepliedTextPattern options:NSRegularExpressionCaseInsensitive error:nil]; }); NSString* finalString = htmlString; From c29af7920247f7d31b4c4dd7f289d92381fdf2d3 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 17:23:12 +0100 Subject: [PATCH 081/468] Add UTs --- ...wift => MXKEventFormatterSwiftTests.swift} | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) rename RiotTests/MatrixKitTests/{MXKEventFormatterTests.swift => MXKEventFormatterSwiftTests.swift} (72%) diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift similarity index 72% rename from RiotTests/MatrixKitTests/MXKEventFormatterTests.swift rename to RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift index 31db30954..03731dd50 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift +++ b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift @@ -29,9 +29,10 @@ private enum Constants { static let expectedEditedHTML = "
In reply to alice
Edited message
Reply" static let expectedEditedHTMLWithNewContent = "
In reply to alice
New content
Reply" static let expectedEditedHTMLWithParsedItalic = "
In reply to alice
New content
Reply" + static let expectedReplyToPollEndedEvent = "
In reply to alice
Ended poll
Reply" } -class MXKEventFormatterTests: XCTestCase { +class MXKEventFormatterSwiftTests: XCTestCase { func testBuildHTMLString() { let formatter = MXKEventFormatter() let repliedEvent = MXEvent() @@ -73,4 +74,39 @@ class MXKEventFormatterTests: XCTestCase { repliedEvent.wireContent[kMXMessageContentKeyNewContent] = nil XCTAssertNil(buildHTML()) } + + func testBuildHTMLStringWithPollEndedReply() { + let formatter = MXKEventFormatter() + let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringPollEnd, body: nil) + + let event = MXEvent() + event.sender = "bob" + event.wireType = kMXEventTypeStringRoomMessage + event.wireContent = [ + kMXMessageTypeKey: kMXMessageTypeText, + kMXMessageBodyKey: Constants.replyBody, + kMXEventRelationRelatesToKey: [kMXEventContentRelatesToKeyInReplyTo: ["event_id": Constants.repliedEventId]] + ] + + let formattedText = formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) + + XCTAssertEqual(formattedText, Constants.expectedReplyToPollEndedEvent) + } +} + +private extension MXEvent { + static func mockEvent(eventType: String, body: String? = Constants.repliedEventBody) -> MXEvent { + let repliedEvent = MXEvent() + repliedEvent.sender = "alice" + repliedEvent.roomId = Constants.roomId + repliedEvent.eventId = Constants.repliedEventId + repliedEvent.wireType = eventType + repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText] + + if let body = body { + repliedEvent.wireContent[kMXMessageBodyKey] = body + } + + return repliedEvent + } } From 694c12abb862039cbfa416e703bb3c0851031ca9 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 17:25:14 +0100 Subject: [PATCH 082/468] Cleanup --- .../MatrixKitTests/MXKEventFormatterSwiftTests.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift index 03731dd50..457f10853 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift +++ b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift @@ -35,17 +35,10 @@ private enum Constants { class MXKEventFormatterSwiftTests: XCTestCase { func testBuildHTMLString() { let formatter = MXKEventFormatter() - let repliedEvent = MXEvent() + let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringRoomMessage) let event = MXEvent() func buildHTML() -> String? { return formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) } - // Initial setup. - repliedEvent.sender = "alice" - repliedEvent.roomId = Constants.roomId - repliedEvent.eventId = Constants.repliedEventId - repliedEvent.wireType = kMXEventTypeStringRoomMessage - repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText, - kMXMessageBodyKey: Constants.repliedEventBody] event.sender = "bob" event.wireType = kMXEventTypeStringRoomMessage event.wireContent = [ From a42f231a6da515d7390c77156e6f0711add81281 Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Wed, 18 Jan 2023 16:27:13 +0000 Subject: [PATCH 083/468] Voice broadcast connection error handling while recording (#7282) --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 + Riot/Modules/Application/AppCoordinator.swift | 6 ++ .../VoiceBroadcastPlaybackErrorView.swift | 8 +- .../VoiceBroadcastRecorderCoordinator.swift | 4 + .../VoiceBroadcastRecorderProvider.swift | 19 +++-- .../VoiceBroadcastRecorderService.swift | 18 +++++ ...oiceBroadcastRecorderServiceProtocol.swift | 3 + ...BroadcastRecorderConnectionErrorView.swift | 49 ++++++++++++ .../View/VoiceBroadcastRecorderView.swift | 80 ++++++++++--------- .../VoiceBroadcastRecorderModels.swift | 2 + .../VoiceBroadcastRecorderViewModel.swift | 6 ++ changelog.d/7229.change | 1 + 13 files changed, 152 insertions(+), 49 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift create mode 100644 changelog.d/7229.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 95531c6da..959d539fc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2220,6 +2220,7 @@ Tap the + to start adding people."; "voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call."; "voice_broadcast_connection_error_title" = "Connection error"; "voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later."; +"voice_broadcast_recorder_connection_error" = "Connection error - Recording paused"; // MARK: - Version check diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 105a5d70e..bc1dd8471 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9231,6 +9231,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLockScreenPlaceholder: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder") } + /// Connection error - Recording paused + public static var voiceBroadcastRecorderConnectionError: String { + return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error") + } /// Yes, stop public static var voiceBroadcastStopAlertAgreeButton: String { return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_agree_button") diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 202e698a0..8c84e8bb0 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -100,8 +100,14 @@ final class AppCoordinator: NSObject, AppCoordinatorType { if AppDelegate.theDelegate().isOffline { self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash")) + + // Pause voice broadcast recording without sending pending events. + VoiceBroadcastRecorderProvider.shared.pauseRecordingOnError() } else { self.splitViewCoordinator?.hideAppStateIndicator() + + // Send pause voice broadcast event. + VoiceBroadcastRecorderProvider.shared.pauseRecording() } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift index 0ac7822c6..0836bc661 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift @@ -28,19 +28,17 @@ struct VoiceBroadcastPlaybackErrorView: View { var action: (() -> Void)? var body: some View { - VStack { - VStack { + ZStack { + HStack { Image(uiImage: Asset.Images.errorIcon.image) .frame(width: 40, height: 40) Text(VectorL10n.voiceBroadcastPlaybackLoadingError) .multilineTextAlignment(.center) .font(theme.fonts.caption1) - .foregroundColor(theme.colors.primaryContent) + .foregroundColor(theme.colors.alert) } - .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(theme.colors.system.ignoresSafeArea()) } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index 409266d15..2b33f4121 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -70,6 +70,10 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { voiceBroadcastRecorderViewModel.context.send(viewAction: .pause) } + func pauseRecordingOnError() { + voiceBroadcastRecorderViewModel.context.send(viewAction: .pauseOnError) + } + func isVoiceBroadcastRecording() -> Bool { return voiceBroadcastRecorderService.isRecording } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index 7b82429cb..9ef2d5c98 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ import Foundation if !self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener == nil { redactionsListener = session?.listenToEvents([MXEventType(identifier: kMXEventTypeStringRoomRedaction)], self.handleRedactedEvent) } - + if self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener != nil { session?.removeListener(self.redactionsListener) self.redactionsListener = nil @@ -49,7 +49,7 @@ import Foundation // MARK: Private private var currentEventIdentifier: String? - + // MARK: - Setup private override init() { } @@ -85,6 +85,11 @@ import Foundation voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecording() } + /// Pause current voice broadcast recording without sending pending events. + @objc public func pauseRecordingOnError() { + voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecordingOnError() + } + @objc public func isVoiceBroadcastRecording() -> Bool { guard let coordinator = voiceBroadcastRecorderCoordinatorForCurrentEvent() else { return false @@ -100,7 +105,7 @@ import Foundation guard let currentEventIdentifier = currentEventIdentifier else { return nil } - + return coordinatorsForEventIdentifiers[currentEventIdentifier] } @@ -109,11 +114,11 @@ import Foundation // ignore backwards events return } - + var coordinator = coordinatorsForEventIdentifiers.removeValue(forKey: event.redacts) - + coordinator?.toPresentable().dismiss(animated: false) { - coordinator = nil + coordinator = nil } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index fd8fc664d..c4851e779 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -116,6 +116,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // Discard the service on VoiceBroadcastService error. We keep the service in case of other error type if error as? VoiceBroadcastServiceError != nil { self.tearDownVoiceBroadcastService() + } else { + AppDelegate.theDelegate().showError(asAlert: error) } }) } @@ -136,6 +138,10 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error) + // Pause voice broadcast recording without sending pending events. + if error is VoiceBroadcastServiceError == false { + AppDelegate.theDelegate().showError(asAlert: error) + } }) } @@ -151,6 +157,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { UIApplication.shared.isIdleTimerDisabled = true }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error) + if error is VoiceBroadcastServiceError == false { + AppDelegate.theDelegate().showError(asAlert: error) + } }) } @@ -169,6 +178,15 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { self.tearDownVoiceBroadcastService() } + func pauseOnErrorRecordingVoiceBroadcast() { + audioEngine.pause() + UIApplication.shared.isIdleTimerDisabled = false + invalidateTimer() + + // Update state + self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .error) + } + // MARK: - Private /// Reset chunk values. private func resetValues() { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift index 1b3e77878..78492fe15 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift @@ -42,4 +42,7 @@ protocol VoiceBroadcastRecorderServiceProtocol { /// Cancel voice broadcast recording after redacted it. func cancelRecordingVoiceBroadcast() + + /// Pause voice broadcast recording without sending pending events. + func pauseOnErrorRecordingVoiceBroadcast() } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift new file mode 100644 index 000000000..051a6477b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift @@ -0,0 +1,49 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VoiceBroadcastRecorderConnectionErrorView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var action: (() -> Void)? + + var body: some View { + ZStack { + HStack(spacing: 0) { + Image(uiImage: Asset.Images.errorIcon.image) + .frame(width: 40, height: 40) + Text(VectorL10n.voiceBroadcastRecorderConnectionError) + .multilineTextAlignment(.center) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct VoiceBroadcastRecorderConnectionErrorView_Previews: PreviewProvider { + static var previews: some View { + VoiceBroadcastRecorderConnectionErrorView() + } +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index c0cafed9b..c8e6532f6 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -26,7 +26,7 @@ struct VoiceBroadcastRecorderView: View { @State private var showingStopAlert = false private var backgroundColor: Color { - if viewModel.viewState.recordingState != .paused { + if viewModel.viewState.recordingState != .paused, viewModel.viewState.recordingState != .error { return theme.colors.alert } return theme.colors.quarterlyContent @@ -78,47 +78,53 @@ struct VoiceBroadcastRecorderView: View { .accessibilityIdentifier("liveButton") } - HStack(alignment: .top, spacing: 34.0) { - Button { - switch viewModel.viewState.recordingState { - case .started, .resumed: - viewModel.send(viewAction: .pause) - case .stopped: - viewModel.send(viewAction: .start) - case .paused: - viewModel.send(viewAction: .resume) + if viewModel.viewState.recordingState == .error { + VoiceBroadcastRecorderConnectionErrorView() + } else { + HStack(alignment: .top, spacing: 34.0) { + Button { + switch viewModel.viewState.recordingState { + case .started, .resumed: + viewModel.send(viewAction: .pause) + case .stopped: + viewModel.send(viewAction: .start) + case .paused: + viewModel.send(viewAction: .resume) + case .error: + break + } + } label: { + if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed { + Image("voice_broadcast_record_pause") + .renderingMode(.original) + } else { + Image("voice_broadcast_record") + .renderingMode(.original) + } } - } label: { - if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed { - Image("voice_broadcast_record_pause") - .renderingMode(.original) - } else { - Image("voice_broadcast_record") + .accessibilityIdentifier("recordButton") + + Button { + showingStopAlert = true + } label: { + Image("voice_broadcast_stop") .renderingMode(.original) } + .alert(isPresented:$showingStopAlert) { + Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle), + message: Text(VectorL10n.voiceBroadcastStopAlertDescription), + primaryButton: .cancel(), + secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton), + action: { + viewModel.send(viewAction: .stop) + })) + } + .accessibilityIdentifier("stopButton") + .disabled(viewModel.viewState.recordingState == .stopped) + .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0)) } - .accessibilityIdentifier("recordButton") - - Button { - showingStopAlert = true - } label: { - Image("voice_broadcast_stop") - .renderingMode(.original) - } - .alert(isPresented:$showingStopAlert) { - Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle), - message: Text(VectorL10n.voiceBroadcastStopAlertDescription), - primaryButton: .cancel(), - secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton), - action: { - viewModel.send(viewAction: .stop) - })) - } - .accessibilityIdentifier("stopButton") - .disabled(viewModel.viewState.recordingState == .stopped) - .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0)) + .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0)) } - .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0)) } .padding(EdgeInsets(top: 12.0, leading: 4.0, bottom: 12.0, trailing: 4.0)) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift index cb807a430..f992cd2f4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift @@ -21,6 +21,7 @@ enum VoiceBroadcastRecorderViewAction { case stop case pause case resume + case pauseOnError } enum VoiceBroadcastRecorderState { @@ -28,6 +29,7 @@ enum VoiceBroadcastRecorderState { case stopped case paused case resumed + case error } struct VoiceBroadcastRecorderDetails { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift index ba9690bfb..ff486c5df 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift @@ -56,6 +56,8 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic pause() case .resume: resume() + case .pauseOnError: + pauseOnError() } } @@ -80,6 +82,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast() } + private func pauseOnError() { + voiceBroadcastRecorderService.pauseOnErrorRecordingVoiceBroadcast() + } + private func updateRemainingTime(_ remainingTime: UInt) { state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime) } diff --git a/changelog.d/7229.change b/changelog.d/7229.change new file mode 100644 index 000000000..7099b8500 --- /dev/null +++ b/changelog.d/7229.change @@ -0,0 +1 @@ +Voice broadcast connection error handling while recording. From c2a86201415a9998cf1b5d8f3047a2185fbfcd5c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 18 Jan 2023 17:27:30 +0100 Subject: [PATCH 084/468] Add changelog.d file --- changelog.d/pr-7284.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7284.change diff --git a/changelog.d/pr-7284.change b/changelog.d/pr-7284.change new file mode 100644 index 000000000..edab71856 --- /dev/null +++ b/changelog.d/pr-7284.change @@ -0,0 +1 @@ +Polls: render replies to poll events better. From f7cd7c445952002350eee3938da70bd29ed8db61 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 16 Jan 2023 17:18:53 +0000 Subject: [PATCH 085/468] Display migration progress during startup --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ Riot/Modules/Application/LegacyAppDelegate.m | 6 ++-- .../AuthenticationCoordinator.swift | 4 +-- .../LegacyAuthenticationCoordinator.swift | 4 +-- .../LaunchLoading/LaunchLoadingView.swift | 32 +++++++++++++------ .../LaunchLoadingViewController.swift | 4 +-- changelog.d/pr-7286.change | 1 + 8 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 changelog.d/pr-7286.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bf1b69746..62f0797dc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1977,6 +1977,7 @@ Tap the + to start adding people."; // MARK: - Launch loading +"launch_loading_migrating_data" = "Migrating data\n%@ %%"; "launch_loading_server_syncing" = "Syncing with the server"; "launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)"; "launch_loading_processing_response" = "Processing data\n%@ %%"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3de9542c2..bb8e85211 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3179,6 +3179,10 @@ public class VectorL10n: NSObject { public static var later: String { return VectorL10n.tr("Vector", "later") } + /// Migrating data\n%@ %% + public static func launchLoadingMigratingData(_ p1: String) -> String { + return VectorL10n.tr("Vector", "launch_loading_migrating_data", p1) + } /// Processing data\n%@ %% public static func launchLoadingProcessingResponse(_ p1: String) -> String { return VectorL10n.tr("Vector", "launch_loading_processing_response", p1) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 15cdb8be1..6ede5be42 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2395,14 +2395,14 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] showLaunchAnimation"); LaunchLoadingView *launchLoadingView; - if (MXSDKOptions.sharedInstance.enableSyncProgress) + if (MXSDKOptions.sharedInstance.enableStartupProgress) { MXSession *mainSession = self.mxSessions.firstObject; - launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:mainSession.syncProgress]; + launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; } else { - launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:nil]; + launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil]; } launchLoadingView.frame = window.bounds; diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 8b9898992..9f2e7083b 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -613,8 +613,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc /// Replace the contents of the navigation router with a loading animation. private func showLoadingAnimation() { - let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil - let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress) + let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil + let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index 583419075..d6270edae 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -106,8 +106,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator // MARK: - Private private func showLoadingAnimation() { - let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil - let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress) + let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil + let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift index 55f3aff05..18d6add9d 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift @@ -41,9 +41,9 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { // MARK: - Setup - static func instantiate(syncProgress: MXSessionSyncProgress?) -> LaunchLoadingView { + static func instantiate(startupProgress: MXSessionStartupProgress?) -> LaunchLoadingView { let view = LaunchLoadingView.loadFromNib() - syncProgress?.delegate = view + startupProgress?.delegate = view return view } @@ -54,7 +54,7 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { animationTimeline.play() self.animationTimeline = animationTimeline - self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableSyncProgress + self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableStartupProgress } // MARK: - Public @@ -65,21 +65,35 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { } } -extension LaunchLoadingView: MXSessionSyncProgressDelegate { - func sessionDidUpdateSyncState(_ state: MXSessionSyncState) { - guard MXSDKOptions.sharedInstance().enableSyncProgress else { +extension LaunchLoadingView: MXSessionStartupProgressDelegate { + func sessionDidUpdateStartupStage(_ stage: MXSessionStartupStage) { + guard MXSDKOptions.sharedInstance().enableStartupProgress else { + return + } + updateStatusText(for: stage) + + } + + private func updateStatusText(for stage: MXSessionStartupStage) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.updateStatusText(for: stage) + } return } // Sync may be doing a lot of heavy work on the main thread and the status text // does not update reliably enough without explicitly refreshing CATransaction.begin() - statusLabel.text = statusText(for: state) + statusLabel.text = statusText(for: stage) CATransaction.commit() } - private func statusText(for state: MXSessionSyncState) -> String { - switch state { + private func statusText(for stage: MXSessionStartupStage) -> String { + switch stage { + case .migratingData(let progress): + let percent = Int(floor(progress * 100)) + return VectorL10n.launchLoadingMigratingData("\(percent)") case .serverSyncing(let attempts): if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) { return VectorL10n.launchLoadingServerSyncingNthAttempt(nth) diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift index 1da229b79..ef9630dda 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift @@ -21,10 +21,10 @@ class LaunchLoadingViewController: UIViewController, Reusable { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - init(syncProgress: MXSessionSyncProgress?) { + init(startupProgress: MXSessionStartupProgress?) { super.init(nibName: "LaunchLoadingViewController", bundle: nil) - let launchLoadingView = LaunchLoadingView.instantiate(syncProgress: syncProgress) + let launchLoadingView = LaunchLoadingView.instantiate(startupProgress: startupProgress) launchLoadingView.update(theme: ThemeService.shared().theme) view.vc_addSubViewMatchingParent(launchLoadingView) diff --git a/changelog.d/pr-7286.change b/changelog.d/pr-7286.change new file mode 100644 index 000000000..ff8cbb820 --- /dev/null +++ b/changelog.d/pr-7286.change @@ -0,0 +1 @@ +CryptoV2: Display migration progress during startup From 6933cd08c0f6917ae86d1d07987082fce0e2f690 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 19 Jan 2023 12:13:44 +0100 Subject: [PATCH 086/468] Delete a voice broadcast with all related events if MSC3912 is supported. --- Riot/Modules/Room/RoomViewController.m | 42 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a1a0b6b6c..e701aacf6 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4302,18 +4302,36 @@ static CGSize kThreadListBarButtonItemImageSize; [self startActivityIndicator]; - MXWeakify(self); - [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - } failure:^(NSError *error) { - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); - //Alert user - [self showError:error]; - }]; + // If it's a voice broadcast, delete the selected event and all related events (only if this feature is supported). + BOOL supportsRedactionWithRelations = self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelations || self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelationsUnstable; + if (supportsRedactionWithRelations && selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { + MXWeakify(self); + [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:@[MXEventRelationTypeReference] reason:nil success:^{ + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); + //Alert user + [self showError:error]; + }]; + + } else { + MXWeakify(self); + [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); + //Alert user + [self showError:error]; + }]; + } }]]; } From 48cb1e5a8b6a81713d6139faba9158a7bb9bc2e0 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 20 Jan 2023 09:27:20 +0100 Subject: [PATCH 087/468] Add Towncrier file --- changelog.d/7283.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7283.feature diff --git a/changelog.d/7283.feature b/changelog.d/7283.feature new file mode 100644 index 000000000..d139728d5 --- /dev/null +++ b/changelog.d/7283.feature @@ -0,0 +1 @@ +Voice Broadcast: When deleting a voice broadcast, all data is now deleted on server side (MSC3912 implementation). From 87fb137e605e487386a9fd31e095bdde8de7584b Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 20 Jan 2023 16:09:18 +0100 Subject: [PATCH 088/468] Pause the voicebroadcast recording if the homeserver is not reachable --- Riot/Modules/Application/AppCoordinator.swift | 8 +--- .../VoiceBroadcastRecorderProvider.swift | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 8c84e8bb0..c356d0b86 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -100,14 +100,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType { if AppDelegate.theDelegate().isOffline { self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash")) - - // Pause voice broadcast recording without sending pending events. - VoiceBroadcastRecorderProvider.shared.pauseRecordingOnError() } else { - self.splitViewCoordinator?.hideAppStateIndicator() - - // Send pause voice broadcast event. - VoiceBroadcastRecorderProvider.shared.pauseRecording() + self.splitViewCoordinator?.hideAppStateIndicator() } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index 9ef2d5c98..b69476593 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -32,6 +32,9 @@ import Foundation coordinatorsForEventIdentifiers.removeAll() } } + didSet { + sessionState = session?.state + } } private var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]() { didSet { @@ -49,9 +52,19 @@ import Foundation // MARK: Private private var currentEventIdentifier: String? + private var sessionState: MXSessionState? + + private var sessionStateDidChangeObserver: Any? // MARK: - Setup - private override init() { } + private override init() { + super.init() + self.registerNotificationObservers() + } + + deinit { + unregisterNotificationObservers() + } // MARK: - Public @@ -121,4 +134,36 @@ import Foundation coordinator = nil } } + + // MARK: - Notification handling + + private func registerNotificationObservers() { + self.sessionStateDidChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: session, queue: nil) { [weak self] notification in + guard let self else { return } + guard let concernedSession = notification.object as? MXSession, self.session === concernedSession else { return } + + self.update(sessionState: concernedSession.state) + } + } + + private func unregisterNotificationObservers() { + if let observer = self.sessionStateDidChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + // MARK: - Session state + private func update(sessionState: MXSessionState) { + let oldState = self.sessionState + self.sessionState = sessionState + + switch (oldState, sessionState) { + case (_, .homeserverNotReachable): + pauseRecordingOnError() + case (_, .running): + pauseRecording() + default: + break + } + } } From 668ecc1bf447d71629d9149cf09964165c5360f1 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 23 Jan 2023 09:22:00 +0100 Subject: [PATCH 089/468] Add Towncrier file. --- changelog.d/7285.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7285.change diff --git a/changelog.d/7285.change b/changelog.d/7285.change new file mode 100644 index 000000000..9d3f369ae --- /dev/null +++ b/changelog.d/7285.change @@ -0,0 +1 @@ +Voice Broadcast: handle the lost of connectivity with the homeserver while recording. From 3f343a028b9701f8d39766c5d3a4139b2b420e37 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 19 Jan 2023 16:22:22 +0100 Subject: [PATCH 090/468] Begin PollHistoryService --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 2 +- .../Coordinator/PollHistoryCoordinator.swift | 5 +- .../MatrixSDK/PollHistoryService.swift | 92 ++++++++++++++++++- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 046c20c79..558951391 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -176,7 +176,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { coordinator.start() push(coordinator: coordinator) case .pollHistory: - let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active)) + let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: self.room)) coordinator.start() push(coordinator: coordinator) default: diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index b9129a6e9..4b9f6c63f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -19,6 +19,7 @@ import SwiftUI struct PollHistoryCoordinatorParameters { let mode: PollHistoryMode + let room: MXRoom } final class PollHistoryCoordinator: Coordinator, Presentable { @@ -32,9 +33,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable { init(parameters: PollHistoryCoordinatorParameters) { self.parameters = parameters - - #warning("replace with the real service after that it's done") - let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: MockPollHistoryService()) + let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room) ) let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index a2dcf256a..dc5303998 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -16,9 +16,99 @@ import MatrixSDK import Foundation +import Combine final class PollHistoryService: PollHistoryServiceProtocol { + private let room: MXRoom + private let livePolls: PassthroughSubject = .init() + + private var listner: Any? + private var timeline: MXEventTimeline? + private var pollAggregators: [String: PollAggregator] = [:] + + init(room: MXRoom) { + self.room = room + } + func fetchHistory() async throws -> [PollListData] { - [] + guard timeline == nil else { + paginate() + return [] + } + + room.liveTimeline { [weak self] timeline in + guard + let self = self, + let timeline = timeline + else { + #warning("Handle error") + return + } + + self.setup(timeline: timeline) + self.paginate() + } + + return [] + } +} + +private extension PollHistoryService { + enum Constants { + static let pageSize: UInt = 250 + } + + func setup(timeline: MXEventTimeline) { + self.timeline = timeline + listner = timeline.listenToEvents([MXEventType.pollStart]) { [weak self] event, direction, roomState in + self?.aggregatePoll(pollStartEvent: event) + } + } + + func paginate() { + guard let timeline = timeline else { + return + } + + timeline.paginate(Constants.pageSize, + direction: .backwards, + onlyFromStore: false) { response in + switch response { + case .success: + break + case .failure(let error): + #warning("Handle error") + break + } + } + } + + func aggregatePoll(pollStartEvent: MXEvent) { + guard pollAggregators[pollStartEvent.eventId] == nil else { + return + } + + guard let aggregator = try? PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) else { + return + } + + pollAggregators[pollStartEvent.eventId] = aggregator + } +} + +// MARK: - PollAggregatorDelegate + +extension PollHistoryService: PollAggregatorDelegate { + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { + } + + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + } + + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { + } + + func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { + } } From eb64ff11fc5d3be87414b22912cc7802a04f549b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 19 Jan 2023 16:29:47 +0100 Subject: [PATCH 091/468] Expose TimelinePollDetails init init TimelinePollCoordinator --- .../Coordinator/TimelinePollCoordinator.swift | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index c3c1cf327..457b51674 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -114,10 +114,17 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } // MARK: - Private - - // PollProtocol is intentionally not available in the SwiftUI target as we don't want - // to add the SDK as a dependency to it. We need to translate from one to the other on this level. + func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails { + let representedType: TimelinePollEventType = parameters.pollEvent.eventType == .pollStart ? .started : .ended + return .init(poll: poll, represent: representedType) + } +} + +// PollProtocol is intentionally not available in the SwiftUI target as we don't want +// to add the SDK as a dependency to it. We need to translate from one to the other on this level. +extension TimelinePollDetails { + init(poll: PollProtocol, represent eventType: TimelinePollEventType) { let answerOptions = poll.answerOptions.map { pollAnswerOption in TimelinePollAnswerOption(id: pollAnswerOption.id, text: pollAnswerOption.text, @@ -126,18 +133,18 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel selected: pollAnswerOption.isCurrentUserSelection) } - return TimelinePollDetails(question: poll.text, - answerOptions: answerOptions, - closed: poll.isClosed, - totalAnswerCount: poll.totalAnswerCount, - type: pollKindToTimelinePollType(poll.kind), - eventType: parameters.pollEvent.eventType == .pollStart ? .started : .ended, - maxAllowedSelections: poll.maxAllowedSelections, - hasBeenEdited: poll.hasBeenEdited, - hasDecryptionError: poll.hasDecryptionError) + self.init(question: poll.text, + answerOptions: answerOptions, + closed: poll.isClosed, + totalAnswerCount: poll.totalAnswerCount, + type: Self.pollKindToTimelinePollType(poll.kind), + eventType: eventType, + maxAllowedSelections: poll.maxAllowedSelections, + hasBeenEdited: poll.hasBeenEdited, + hasDecryptionError: poll.hasDecryptionError) } - private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { + private static func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { let mapping = [PollKind.disclosed: TimelinePollType.disclosed, PollKind.undisclosed: TimelinePollType.undisclosed] From 460c5a96823c2c3478490a6dca0b249574c71b26 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 19 Jan 2023 17:10:14 +0100 Subject: [PATCH 092/468] Inject TimelinePollDetails in PollListItem --- .../Room/PollHistory/PollHistoryModels.swift | 2 +- .../PollHistory/PollHistoryViewModel.swift | 44 +++++----- .../MatrixSDK/PollHistoryService.swift | 19 +++-- .../Service/Mock/MockPollHistoryService.swift | 59 ++++++++----- .../Service/PollHistoryServiceProtocol.swift | 7 +- .../Room/PollHistory/View/PollListItem.swift | 83 +++++++++++-------- 6 files changed, 131 insertions(+), 83 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 93ef30819..1cfb22fbe 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -37,7 +37,7 @@ struct PollHistoryViewState: BindableState { } var bindings: PollHistoryViewBindings - var polls: [PollListData] = [] + var polls: [TimelinePollDetails] = [] } enum PollHistoryViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 4199251da..063f962f3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -14,18 +14,15 @@ // limitations under the License. // +import Combine import SwiftUI typealias PollHistoryViewModelType = StateStoreViewModel final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol { private let pollService: PollHistoryServiceProtocol - private var polls: [PollListData] = [] - private var fetchingTask: Task? { - didSet { - oldValue?.cancel() - } - } + private var polls: [TimelinePollDetails] = [] + private var subcriptions: Set = .init() var completion: ((PollHistoryViewModelResult) -> Void)? @@ -39,7 +36,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel override func process(viewAction: PollHistoryViewAction) { switch viewAction { case .viewAppeared: - fetchingTask = fetchPolls() + setupSubscriptions() + pollService.startFetching() case .segmentDidChange: updatePolls() } @@ -47,29 +45,33 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel } private extension PollHistoryViewModel { - func fetchPolls() -> Task { - Task { - let polls = try await pollService.fetchHistory() - - guard Task.isCancelled == false else { - return + func setupSubscriptions() { + subcriptions.removeAll() + + pollService + .pollHistory + .sink { [weak self] detail in + self?.polls.append(detail) + self?.updatePolls() } - - await MainActor.run { - self.polls = polls - updatePolls() + .store(in: &subcriptions) + + pollService + .error + .sink { detail in + #warning("Handle errors") } - } + .store(in: &subcriptions) } func updatePolls() { - let renderedPolls: [PollListData] + let renderedPolls: [TimelinePollDetails] switch context.mode { case .active: - renderedPolls = polls.filter { $0.winningOption == nil } + renderedPolls = polls.filter { $0.closed == false } case .past: - renderedPolls = polls.filter { $0.winningOption != nil } + renderedPolls = polls.filter { $0.closed == true } } state.polls = renderedPolls diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index dc5303998..a8e27b7e1 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -20,20 +20,29 @@ import Combine final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom - private let livePolls: PassthroughSubject = .init() + private let pollsSubject: PassthroughSubject = .init() + private let errorSubject: PassthroughSubject = .init() private var listner: Any? private var timeline: MXEventTimeline? private var pollAggregators: [String: PollAggregator] = [:] + var pollHistory: AnyPublisher { + pollsSubject.eraseToAnyPublisher() + } + + var error: AnyPublisher { + errorSubject.eraseToAnyPublisher() + } + init(room: MXRoom) { self.room = room } - func fetchHistory() async throws -> [PollListData] { + func startFetching() { guard timeline == nil else { paginate() - return [] + return } room.liveTimeline { [weak self] timeline in @@ -48,8 +57,6 @@ final class PollHistoryService: PollHistoryServiceProtocol { self.setup(timeline: timeline) self.paginate() } - - return [] } } @@ -75,6 +82,7 @@ private extension PollHistoryService { onlyFromStore: false) { response in switch response { case .success: + #warning("Go on with pagination...") break case .failure(let error): #warning("Handle error") @@ -103,6 +111,7 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + pollsSubject.send(.init(poll: aggregator.poll, represent: .started)) } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 62796963d..03e0f733e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -14,31 +14,48 @@ // limitations under the License. // +import Combine + final class MockPollHistoryService: PollHistoryServiceProtocol { - var activePollsData: [PollListData] = (1..<10) + private let polls: PassthroughSubject = .init() + + var pollHistory: AnyPublisher { + polls.eraseToAnyPublisher() + } + + var error: AnyPublisher { + Empty().eraseToAnyPublisher() + } + + func startFetching() { + for poll in activePollsData + pastPollsData { + polls.send(poll) + } + } + + var activePollsData: [TimelinePollDetails] = (1..<10) .map { index in - PollListData( - startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), - question: "Do you like the active poll number \(index)?", - numberOfVotes: 30, - winningOption: nil - ) + TimelinePollDetails(question: "Do you like the active poll number \(index)?", + answerOptions: [], + closed: false, + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) } - var pastPollsData: [PollListData] = (1..<10) + var pastPollsData: [TimelinePollDetails] = (1..<10) .map { index in - PollListData( - startDate: .init().addingTimeInterval(-CGFloat(index) * 3600), - question: "Do you like the past poll number \(index)?", - numberOfVotes: 30, - winningOption: .init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true) - ) + TimelinePollDetails(question: "Do you like the active poll number \(index)?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], + closed: true, + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) } - - func fetchHistory() async throws -> [PollListData] { - (activePollsData + pastPollsData) - .sorted { poll1, poll2 in - poll1.startDate > poll2.startDate - } - } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 4bb9b43b5..831b6428c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -14,6 +14,11 @@ // limitations under the License. // +import Combine + protocol PollHistoryServiceProtocol { - func fetchHistory() async throws -> [PollListData] + var pollHistory: AnyPublisher { get } + var error: AnyPublisher { get } + + func startFetching() } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 335d62c39..58ff02c89 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -16,26 +16,20 @@ import SwiftUI -struct PollListData { - let startDate: Date - let question: String - let numberOfVotes: UInt - let winningOption: TimelinePollAnswerOption? -} - struct PollListItem: View { @Environment(\.theme) private var theme - private let pollData: PollListData + private let pollData: TimelinePollDetails @ScaledMetric private var imageSize = 16 - init(pollData: PollListData) { + init(pollData: TimelinePollDetails) { self.pollData = pollData } var body: some View { VStack(alignment: .leading, spacing: 12) { - Text(pollData.formattedDate) + #warning("fix me") + Text(DateFormatter.shortDateFormatter.string(from: .init())) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) @@ -50,10 +44,14 @@ struct PollListItem: View { .lineLimit(2) .accessibilityLabel("PollListItem.title") } + .frame(maxWidth: .infinity, alignment: .leading) - if pollData.winningOption != nil { + if pollData.closed { VStack(alignment: .leading, spacing: 12) { - optionView(winningOption: pollData.winningOption!) + let winningOptions = pollData.answerOptions.filter(\.winner) + ForEach(winningOptions) { + optionView(winningOption: $0) + } resultView } } @@ -67,7 +65,7 @@ struct PollListItem: View { private func optionView(winningOption: TimelinePollAnswerOption) -> some View { VStack(alignment: .leading, spacing: 12.0) { HStack(alignment: .top, spacing: 8.0) { - Text(pollData.winningOption!.text) + Text(winningOption.text) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) .accessibilityIdentifier("PollListData.winningOption") @@ -78,7 +76,7 @@ struct PollListItem: View { } ProgressView(value: Double(winningOption.count), - total: Double(pollData.numberOfVotes)) + total: Double(pollData.totalAnswerCount)) .progressViewStyle(LinearProgressViewStyle()) .scaleEffect(x: 1.0, y: 1.2, anchor: .center) } @@ -102,7 +100,7 @@ struct PollListItem: View { } private var resultView: some View { - let text = pollData.numberOfVotes == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.numberOfVotes)) + let text = pollData.totalAnswerCount == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.totalAnswerCount)) return Text(text) .font(theme.fonts.footnote) @@ -110,12 +108,6 @@ struct PollListItem: View { } } -private extension PollListData { - var formattedDate: String { - DateFormatter.shortDateFormatter.string(from: startDate) - } -} - private extension DateFormatter { static let shortDateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -131,22 +123,45 @@ private extension DateFormatter { struct PollListItem_Previews: PreviewProvider { static var previews: some View { Group { - let pollData1 = PollListData( - startDate: .init(), - question: "Do you like polls?", - numberOfVotes: 30, - winningOption: .init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true) - ) + let pollData1 = TimelinePollDetails(question: "Do you like polls?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + closed: true, + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) - PollListItem(pollData: pollData1) - let pollData2 = PollListData( - startDate: .init(), - question: "Do you like polls?", - numberOfVotes: 30, - winningOption: nil) + let pollData2 = TimelinePollDetails(question: "Do you like polls?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + closed: false, + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) - PollListItem(pollData: pollData2) + let pollData3 = TimelinePollDetails(question: "Do you like polls?", + answerOptions: [ + .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true), + .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) + ], + closed: true, + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + + let allPollData = Array([pollData1, pollData2, pollData3].enumerated()) + ForEach(allPollData, id: \.offset) { _, poll in + PollListItem(pollData: poll) + } } .padding() } From 49973316e0f8ef4ab04fe413bc365cd49769e4d4 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 19 Jan 2023 17:27:24 +0100 Subject: [PATCH 093/468] Reset pagination on landing --- .../PollHistory/Service/MatrixSDK/PollHistoryService.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index a8e27b7e1..70c0b17ed 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -77,6 +77,9 @@ private extension PollHistoryService { return } + #warning("to be removed probably?") + timeline.resetPagination() + timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { response in From 8981abba7343b82aeb5ed66ecc46e1c9346f0d73 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 19 Jan 2023 18:15:39 +0100 Subject: [PATCH 094/468] Add support to start date --- .../PollHistory/Service/Mock/MockPollHistoryService.swift | 2 ++ .../Modules/Room/PollHistory/View/PollListItem.swift | 6 ++++-- .../TimelinePoll/Coordinator/TimelinePollCoordinator.swift | 1 + .../Modules/Room/TimelinePoll/TimelinePollModels.swift | 6 +++++- .../Modules/Room/TimelinePoll/TimelinePollScreenState.swift | 1 + .../TimelinePoll/View/TimelinePollAnswerOptionButton.swift | 1 + 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 03e0f733e..b4299cd96 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -38,6 +38,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { TimelinePollDetails(question: "Do you like the active poll number \(index)?", answerOptions: [], closed: false, + startDate: .init(), totalAnswerCount: 30, type: .disclosed, eventType: .started, @@ -51,6 +52,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { TimelinePollDetails(question: "Do you like the active poll number \(index)?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], closed: true, + startDate: .init(), totalAnswerCount: 30, type: .disclosed, eventType: .started, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 58ff02c89..7739baa2b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -28,8 +28,7 @@ struct PollListItem: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - #warning("fix me") - Text(DateFormatter.shortDateFormatter.string(from: .init())) + Text(DateFormatter.shortDateFormatter.string(from: pollData.startDate)) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) @@ -126,6 +125,7 @@ struct PollListItem_Previews: PreviewProvider { let pollData1 = TimelinePollDetails(question: "Do you like polls?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], closed: true, + startDate: .init(), totalAnswerCount: 30, type: .disclosed, eventType: .started, @@ -137,6 +137,7 @@ struct PollListItem_Previews: PreviewProvider { let pollData2 = TimelinePollDetails(question: "Do you like polls?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], closed: false, + startDate: .init(), totalAnswerCount: 30, type: .disclosed, eventType: .started, @@ -150,6 +151,7 @@ struct PollListItem_Previews: PreviewProvider { .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) ], closed: true, + startDate: .init(), totalAnswerCount: 30, type: .disclosed, eventType: .started, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 457b51674..3d01f0e90 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -136,6 +136,7 @@ extension TimelinePollDetails { self.init(question: poll.text, answerOptions: answerOptions, closed: poll.isClosed, + startDate: poll.startDate, totalAnswerCount: poll.totalAnswerCount, type: Self.pollKindToTimelinePollType(poll.kind), eventType: eventType, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 3629aae3e..f598de67a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -65,6 +65,7 @@ struct TimelinePollDetails { var question: String var answerOptions: [TimelinePollAnswerOption] var closed: Bool + var startDate: Date var totalAnswerCount: UInt var type: TimelinePollType var eventType: TimelinePollEventType @@ -72,8 +73,10 @@ struct TimelinePollDetails { var hasBeenEdited = true var hasDecryptionError: Bool - init(question: String, answerOptions: [TimelinePollAnswerOption], + init(question: String, + answerOptions: [TimelinePollAnswerOption], closed: Bool, + startDate: Date, totalAnswerCount: UInt, type: TimelinePollType, eventType: TimelinePollEventType, @@ -83,6 +86,7 @@ struct TimelinePollDetails { self.question = question self.answerOptions = answerOptions self.closed = closed + self.startDate = startDate self.totalAnswerCount = totalAnswerCount self.type = type self.eventType = eventType diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index a53a745b8..c4bad93cf 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -36,6 +36,7 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { let poll = TimelinePollDetails(question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, + startDate: .init(), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, eventType: self == .closedPollEnded ? .ended : .started, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 85309c31c..277c27eba 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -149,6 +149,7 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { TimelinePollDetails(question: "", answerOptions: [], closed: closed, + startDate: .init(), totalAnswerCount: 100, type: type, eventType: .started, From 34c218baa9be9a8ad12e3832a629ab1fbed8f9c1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 11:25:27 +0100 Subject: [PATCH 095/468] Add id in TimelinePollDetails --- .../Service/Mock/MockPollHistoryService.swift | 6 ++++-- .../Room/PollHistory/View/PollHistory.swift | 4 +--- .../Room/PollHistory/View/PollListItem.swift | 15 ++++++++------- .../Coordinator/TimelinePollCoordinator.swift | 3 ++- .../Room/TimelinePoll/TimelinePollModels.swift | 9 +++++++-- .../TimelinePoll/TimelinePollScreenState.swift | 3 ++- .../View/TimelinePollAnswerOptionButton.swift | 3 ++- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index b4299cd96..d2d6f487c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -35,7 +35,8 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { var activePollsData: [TimelinePollDetails] = (1..<10) .map { index in - TimelinePollDetails(question: "Do you like the active poll number \(index)?", + TimelinePollDetails(id: "a\(index)", + question: "Do you like the active poll number \(index)?", answerOptions: [], closed: false, startDate: .init(), @@ -49,7 +50,8 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { var pastPollsData: [TimelinePollDetails] = (1..<10) .map { index in - TimelinePollDetails(question: "Do you like the active poll number \(index)?", + TimelinePollDetails(id: "p\(index)", + question: "Do you like the active poll number \(index)?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], closed: true, startDate: .init(), diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 7cf9bb45a..d852b8794 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -53,9 +53,7 @@ struct PollHistory: View { private var pollListView: some View { ScrollView { LazyVStack(spacing: 32) { - let enumeratedPolls = Array(viewModel.viewState.polls.enumerated()) - - ForEach(enumeratedPolls, id: \.offset) { _, pollData in + ForEach(viewModel.viewState.polls) { pollData in PollListItem(pollData: pollData) } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 7739baa2b..04acaab6e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -122,7 +122,8 @@ private extension DateFormatter { struct PollListItem_Previews: PreviewProvider { static var previews: some View { Group { - let pollData1 = TimelinePollDetails(question: "Do you like polls?", + let pollData1 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], closed: true, startDate: .init(), @@ -134,7 +135,8 @@ struct PollListItem_Previews: PreviewProvider { hasDecryptionError: false) - let pollData2 = TimelinePollDetails(question: "Do you like polls?", + let pollData2 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], closed: false, startDate: .init(), @@ -145,7 +147,8 @@ struct PollListItem_Previews: PreviewProvider { hasBeenEdited: false, hasDecryptionError: false) - let pollData3 = TimelinePollDetails(question: "Do you like polls?", + let pollData3 = TimelinePollDetails(id: UUID().uuidString, + question: "Do you like polls?", answerOptions: [ .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true), .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) @@ -158,10 +161,8 @@ struct PollListItem_Previews: PreviewProvider { maxAllowedSelections: 1, hasBeenEdited: false, hasDecryptionError: false) - - - let allPollData = Array([pollData1, pollData2, pollData3].enumerated()) - ForEach(allPollData, id: \.offset) { _, poll in + + ForEach([pollData1, pollData2, pollData3]) { poll in PollListItem(pollData: poll) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3d01f0e90..d8c9f59f8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -133,7 +133,8 @@ extension TimelinePollDetails { selected: pollAnswerOption.isCurrentUserSelection) } - self.init(question: poll.text, + self.init(id: poll.id, + question: poll.text, answerOptions: answerOptions, closed: poll.isClosed, startDate: poll.startDate, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index f598de67a..3ad624f57 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -62,6 +62,7 @@ extension MutableCollection where Element == TimelinePollAnswerOption { } struct TimelinePollDetails { + var id: String var question: String var answerOptions: [TimelinePollAnswerOption] var closed: Bool @@ -73,7 +74,8 @@ struct TimelinePollDetails { var hasBeenEdited = true var hasDecryptionError: Bool - init(question: String, + init(id: String, + question: String, answerOptions: [TimelinePollAnswerOption], closed: Bool, startDate: Date, @@ -83,6 +85,7 @@ struct TimelinePollDetails { maxAllowedSelections: UInt, hasBeenEdited: Bool, hasDecryptionError: Bool) { + self.id = id self.question = question self.answerOptions = answerOptions self.closed = closed @@ -96,7 +99,7 @@ struct TimelinePollDetails { } var hasCurrentUserVoted: Bool { - answerOptions.filter { $0.selected == true }.count > 0 + answerOptions.contains(where: \.selected) } var shouldDiscloseResults: Bool { @@ -112,6 +115,8 @@ struct TimelinePollDetails { } } +extension TimelinePollDetails: Identifiable { } + struct TimelinePollViewState: BindableState { var poll: TimelinePollDetails var bindings: TimelinePollViewStateBindings diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index c4bad93cf..8c70b21e3 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -33,7 +33,8 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - let poll = TimelinePollDetails(question: "Question", + let poll = TimelinePollDetails(id: "id", + question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, startDate: .init(), diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 277c27eba..498fe29f5 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -146,7 +146,8 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { } static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { - TimelinePollDetails(question: "", + TimelinePollDetails(id: UUID().uuidString, + question: "", answerOptions: [], closed: closed, startDate: .init(), From 512f9ae7edca2d4330b62d1b845f88899f690dc5 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 11:33:54 +0100 Subject: [PATCH 096/468] Add target timestamp --- .../PollHistory/Service/MatrixSDK/PollHistoryService.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 70c0b17ed..a61919d2c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -26,6 +26,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private var listner: Any? private var timeline: MXEventTimeline? private var pollAggregators: [String: PollAggregator] = [:] + private var targetTimestamp: Date var pollHistory: AnyPublisher { pollsSubject.eraseToAnyPublisher() @@ -37,6 +38,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { init(room: MXRoom) { self.room = room + self.targetTimestamp = Date().addingTimeInterval(-TimeInterval(Constants.daysToSync) * Constants.oneDayInSeconds) } func startFetching() { @@ -63,6 +65,8 @@ final class PollHistoryService: PollHistoryServiceProtocol { private extension PollHistoryService { enum Constants { static let pageSize: UInt = 250 + static let daysToSync: UInt = 30 + static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } func setup(timeline: MXEventTimeline) { From e067e044ef227f59e16fef002078bf5f70d30bb2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 12:11:12 +0100 Subject: [PATCH 097/468] Add loading view --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ .../Room/PollHistory/PollHistoryModels.swift | 29 ++++++++++++++++++- .../PollHistory/PollHistoryViewModel.swift | 20 +++++++++++-- .../MatrixSDK/PollHistoryService.swift | 13 +++++++-- .../Service/Mock/MockPollHistoryService.swift | 6 +++- .../Service/PollHistoryServiceProtocol.swift | 11 ++++++- .../Room/PollHistory/View/PollHistory.swift | 20 +++++++++++-- 8 files changed, 93 insertions(+), 11 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 5d7ac9e4a..bba4c82dc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2304,6 +2304,7 @@ Tap the + to start adding people."; // MARK: - Polls history "poll_history_title" = "Poll history"; +"poll_history_loading_text" = "Displaying polls"; "poll_history_active_segment_title" = "Active polls"; "poll_history_past_segment_title" = "Past polls"; "poll_history_no_active_poll_text" = "There are no active polls in this room"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d42977875..1428f0606 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4851,6 +4851,10 @@ public class VectorL10n: NSObject { public static var pollHistoryActiveSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_active_segment_title") } + /// Displaying polls + public static var pollHistoryLoadingText: String { + return VectorL10n.tr("Vector", "poll_history_loading_text") + } /// There are no active polls in this room public static var pollHistoryNoActivePollText: String { return VectorL10n.tr("Vector", "poll_history_no_active_poll_text") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 1cfb22fbe..99fa76b03 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -27,16 +27,43 @@ enum PollHistoryMode: CaseIterable { case past } +enum PollHistoryLoadingState { + case idle + case loading(firstLoad: Bool) +} + +extension PollHistoryLoadingState { + var isLoadingOnLanding: Bool { + switch self { + case .idle: + return false + case .loading(let firstLoad): + return firstLoad + } + } + + var isLoading: Bool { + switch self { + case .idle: + return false + case .loading: + return true + } + } +} + struct PollHistoryViewBindings { var mode: PollHistoryMode } struct PollHistoryViewState: BindableState { - init(mode: PollHistoryMode) { + init(mode: PollHistoryMode, loadingState: PollHistoryLoadingState) { bindings = .init(mode: mode) + self.loadingState = loadingState } var bindings: PollHistoryViewBindings + var loadingState: PollHistoryLoadingState var polls: [TimelinePollDetails] = [] } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 063f962f3..30133dc13 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -28,7 +28,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) { self.pollService = pollService - super.init(initialViewState: PollHistoryViewState(mode: mode)) + super.init(initialViewState: PollHistoryViewState(mode: mode, loadingState: .loading(firstLoad: true))) } // MARK: - Public @@ -37,7 +37,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel switch viewAction { case .viewAppeared: setupSubscriptions() - pollService.startFetching() + pollService.next() case .segmentDidChange: updatePolls() } @@ -62,6 +62,22 @@ private extension PollHistoryViewModel { #warning("Handle errors") } .store(in: &subcriptions) + + pollService + .isFetching + .filter { $0 } + .first() + .sink { isFetching in + self.state.loadingState = .loading(firstLoad: true) + } + .store(in: &subcriptions) + + pollService + .isFetching + .sink { isFetching in + self.state.loadingState = isFetching ? .loading(firstLoad: false) : .idle + } + .store(in: &subcriptions) } func updatePolls() { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index a61919d2c..8fd001731 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -22,6 +22,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom private let pollsSubject: PassthroughSubject = .init() private let errorSubject: PassthroughSubject = .init() + private let isFetchingSubject: PassthroughSubject = .init() private var listner: Any? private var timeline: MXEventTimeline? @@ -36,12 +37,16 @@ final class PollHistoryService: PollHistoryServiceProtocol { errorSubject.eraseToAnyPublisher() } + var isFetching: AnyPublisher { + isFetchingSubject.eraseToAnyPublisher() + } + init(room: MXRoom) { self.room = room self.targetTimestamp = Date().addingTimeInterval(-TimeInterval(Constants.daysToSync) * Constants.oneDayInSeconds) } - func startFetching() { + func next() { guard timeline == nil else { paginate() return @@ -81,12 +86,14 @@ private extension PollHistoryService { return } - #warning("to be removed probably?") timeline.resetPagination() + isFetchingSubject.send(true) timeline.paginate(Constants.pageSize, direction: .backwards, - onlyFromStore: false) { response in + onlyFromStore: false) { [weak self] response in + self?.isFetchingSubject.send(false) + switch response { case .success: #warning("Go on with pagination...") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index d2d6f487c..80c2ac2dc 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -27,11 +27,15 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { Empty().eraseToAnyPublisher() } - func startFetching() { + func next() { for poll in activePollsData + pastPollsData { polls.send(poll) } } + + var isFetching: AnyPublisher { + Just(false).eraseToAnyPublisher() + } var activePollsData: [TimelinePollDetails] = (1..<10) .map { index in diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 831b6428c..3488e8bac 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -17,8 +17,17 @@ import Combine protocol PollHistoryServiceProtocol { + /// Publishes poll data as soon they are found in the timeline. + /// Updates are also published here, so clients needs to address duplicates. var pollHistory: AnyPublisher { get } + + /// Publishes whatever errors produced during the sync. var error: AnyPublisher { get } - func startFetching() + /// Ask to fetch the next batch of polls. + /// Concrete implementations can decide what a batch is. + func next() + + /// Inform the whenever a new batch of polls starts or ends. + var isFetching: AnyPublisher { get } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index d852b8794..916445cff 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -31,8 +31,8 @@ struct PollHistory: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) - if viewModel.viewState.polls.isEmpty { - noPollsView + if viewModel.viewState.loadingState.isLoadingOnLanding { + loadingView } else { pollListView } @@ -76,7 +76,21 @@ struct PollHistory: View { .foregroundColor(theme.colors.secondaryContent) .frame(maxHeight: .infinity) .padding(.horizontal, 16) - .accessibilityLabel("PollHistory.emptyText") + .accessibilityIdentifier("PollHistory.emptyText") + } + + private var loadingView: some View { + HStack(spacing: 8) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + + Text(VectorL10n.pollHistoryLoadingText) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxHeight: .infinity) + .accessibilityIdentifier("PollHistory.loadingText") + } + .padding(.horizontal, 16) } } From c1001647f8500097f962b64a0af12bbe0c8ad1f3 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 12:25:52 +0100 Subject: [PATCH 098/468] Refine loading logic --- .../MockPollHistoryScreenState.swift | 4 +++ .../PollHistory/PollHistoryViewModel.swift | 4 +-- .../Service/Mock/MockPollHistoryService.swift | 5 ++-- .../Room/PollHistory/View/PollHistory.swift | 27 ++++++++++++++----- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 65d393957..00b7880f8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -27,6 +27,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { case past case activeEmpty case pastEmpty + case loading /// The associated screen var screenType: Any.Type { @@ -49,6 +50,9 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { case .pastEmpty: pollHistoryMode = .past pollService.pastPollsData = [] + case .loading: + pollHistoryMode = .active + pollService.fetchState = true } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 30133dc13..de987925a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -65,15 +65,15 @@ private extension PollHistoryViewModel { pollService .isFetching - .filter { $0 } .first() .sink { isFetching in - self.state.loadingState = .loading(firstLoad: true) + self.state.loadingState = isFetching ? .loading(firstLoad: true) : .idle } .store(in: &subcriptions) pollService .isFetching + .dropFirst() .sink { isFetching in self.state.loadingState = isFetching ? .loading(firstLoad: false) : .idle } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 80c2ac2dc..b8646c771 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -32,9 +32,10 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { polls.send(poll) } } - + + var fetchState: Bool = false var isFetching: AnyPublisher { - Just(false).eraseToAnyPublisher() + Just(fetchState).eraseToAnyPublisher() } var activePollsData: [TimelinePollDetails] = (1..<10) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 916445cff..fc8d14fad 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -33,6 +33,8 @@ struct PollHistory: View { if viewModel.viewState.loadingState.isLoadingOnLanding { loadingView + } else if viewModel.viewState.loadingState.isLoading == false, viewModel.viewState.polls.isEmpty { + noPollsView } else { pollListView } @@ -57,19 +59,30 @@ struct PollHistory: View { PollListItem(pollData: pollData) } .frame(maxWidth: .infinity, alignment: .leading) - - Button { - #warning("handle action") - } label: { - Text("Load more polls") - } - .frame(maxWidth: .infinity, alignment: .leading) + + loadMoreButton } .padding(.top, 32) .padding(.horizontal, 16) } } + private var loadMoreButton: some View { + HStack(spacing: 8) { + if viewModel.viewState.loadingState.isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + + Button { + #warning("handle action") + } label: { + Text("Load more polls") + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + private var noPollsView: some View { Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText) .font(theme.fonts.body) From a3ad7e4beb8cae9a9952d21838860be5fe8425d2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 13:02:49 +0100 Subject: [PATCH 099/468] Add pagination loop --- .../MatrixSDK/PollHistoryService.swift | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 8fd001731..8d96b3e36 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -28,6 +28,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private var timeline: MXEventTimeline? private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date + private var oldestEventDate: Date = .distantFuture var pollHistory: AnyPublisher { pollsSubject.eraseToAnyPublisher() @@ -48,7 +49,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { func next() { guard timeline == nil else { - paginate() + startPagination() return } @@ -62,7 +63,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { } self.setup(timeline: timeline) - self.paginate() + self.startPagination() } } } @@ -76,30 +77,52 @@ private extension PollHistoryService { func setup(timeline: MXEventTimeline) { self.timeline = timeline - listner = timeline.listenToEvents([MXEventType.pollStart]) { [weak self] event, direction, roomState in - self?.aggregatePoll(pollStartEvent: event) + + listner = timeline.listenToEvents([MXEventType.pollStart, MXEventType.roomMessage, MXEventType.roomEncrypted]) { [weak self] event, direction, roomState in + if event.eventType == .pollStart { + self?.aggregatePoll(pollStartEvent: event) + } + + self?.updateTimestamp(event: event) } } - func paginate() { + func updateTimestamp(event: MXEvent) { + let eventDate = Date(timeIntervalSince1970: Double(event.originServerTs) / 1000) + oldestEventDate = min(eventDate, oldestEventDate) + } + + func startPagination() { + isFetchingSubject.send(true) + guard let timeline = timeline else { + isFetchingSubject.send(false) return } timeline.resetPagination() - - isFetchingSubject.send(true) + paginate(timeline: timeline) + } + + func paginate(timeline: MXEventTimeline) { timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in - self?.isFetchingSubject.send(false) + + guard let self = self else { + return + } switch response { case .success: - #warning("Go on with pagination...") - break + if timeline.canPaginate(.backwards), self.timestampTargetReached == false { + self.paginate(timeline: timeline) + } else { + self.isFetchingSubject.send(false) + } case .failure(let error): #warning("Handle error") + self.isFetchingSubject.send(false) break } } @@ -116,6 +139,10 @@ private extension PollHistoryService { pollAggregators[pollStartEvent.eventId] = aggregator } + + var timestampTargetReached: Bool { + oldestEventDate <= targetTimestamp + } } // MARK: - PollAggregatorDelegate From 775dc8771fe66610c36ea9d0d3a13a2c6a5b67c2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 13:39:52 +0100 Subject: [PATCH 100/468] Handle poll updates --- .../Room/PollHistory/PollHistoryViewModel.swift | 16 ++++++++++++---- .../Service/MatrixSDK/PollHistoryService.swift | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index de987925a..358404514 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -39,7 +39,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel setupSubscriptions() pollService.next() case .segmentDidChange: - updatePolls() + updateState() } } } @@ -51,8 +51,8 @@ private extension PollHistoryViewModel { pollService .pollHistory .sink { [weak self] detail in - self?.polls.append(detail) - self?.updatePolls() + self?.updatePolls(with: detail) + self?.updateState() } .store(in: &subcriptions) @@ -80,7 +80,15 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) } - func updatePolls() { + func updatePolls(with poll: TimelinePollDetails) { + if let matchIndex = polls.firstIndex(where: { $0.id == poll.id }) { + polls[matchIndex] = poll + } else { + polls.append(poll) + } + } + + func updateState() { let renderedPolls: [TimelinePollDetails] switch context.mode { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 8d96b3e36..1bdd34df0 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -148,17 +148,17 @@ private extension PollHistoryService { // MARK: - PollAggregatorDelegate extension PollHistoryService: PollAggregatorDelegate { - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { - } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {} func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { pollsSubject.send(.init(poll: aggregator.poll, represent: .started)) } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { + #warning("Handle error") } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - + pollsSubject.send(.init(poll: aggregator.poll, represent: .started)) } } From 4b4c9f11a9dc108be6e56c56cc27e62715f9dab5 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 15:20:34 +0100 Subject: [PATCH 101/468] Refactor PollHistoryViewState --- .../Room/PollHistory/PollHistoryModels.swift | 32 ++----------------- .../PollHistory/PollHistoryViewModel.swift | 26 +++++++++------ .../Room/PollHistory/View/PollHistory.swift | 8 ++--- 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 99fa76b03..ec2e8a4f6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -27,44 +27,18 @@ enum PollHistoryMode: CaseIterable { case past } -enum PollHistoryLoadingState { - case idle - case loading(firstLoad: Bool) -} - -extension PollHistoryLoadingState { - var isLoadingOnLanding: Bool { - switch self { - case .idle: - return false - case .loading(let firstLoad): - return firstLoad - } - } - - var isLoading: Bool { - switch self { - case .idle: - return false - case .loading: - return true - } - } -} - struct PollHistoryViewBindings { var mode: PollHistoryMode } struct PollHistoryViewState: BindableState { - init(mode: PollHistoryMode, loadingState: PollHistoryLoadingState) { + init(mode: PollHistoryMode) { bindings = .init(mode: mode) - self.loadingState = loadingState } var bindings: PollHistoryViewBindings - var loadingState: PollHistoryLoadingState - var polls: [TimelinePollDetails] = [] + var isLoading = false + var polls: [TimelinePollDetails]? } enum PollHistoryViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 358404514..175e41286 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -23,12 +23,13 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel private let pollService: PollHistoryServiceProtocol private var polls: [TimelinePollDetails] = [] private var subcriptions: Set = .init() + private var hasLoadedFirstGroup: Bool = false var completion: ((PollHistoryViewModelResult) -> Void)? init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) { self.pollService = pollService - super.init(initialViewState: PollHistoryViewState(mode: mode, loadingState: .loading(firstLoad: true))) + super.init(initialViewState: PollHistoryViewState(mode: mode)) } // MARK: - Public @@ -39,7 +40,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel setupSubscriptions() pollService.next() case .segmentDidChange: - updateState() + updateViewState() } } } @@ -52,7 +53,7 @@ private extension PollHistoryViewModel { .pollHistory .sink { [weak self] detail in self?.updatePolls(with: detail) - self?.updateState() + self?.updateViewState() } .store(in: &subcriptions) @@ -63,20 +64,21 @@ private extension PollHistoryViewModel { } .store(in: &subcriptions) - pollService + let didCompleteFirstFetch = pollService .isFetching + .filter { $0 == false } .first() + + didCompleteFirstFetch .sink { isFetching in - self.state.loadingState = isFetching ? .loading(firstLoad: true) : .idle + self.hasLoadedFirstGroup = true + self.updateViewState() } .store(in: &subcriptions) pollService .isFetching - .dropFirst() - .sink { isFetching in - self.state.loadingState = isFetching ? .loading(firstLoad: false) : .idle - } + .weakAssign(to: \.state.isLoading, on: self) .store(in: &subcriptions) } @@ -88,7 +90,11 @@ private extension PollHistoryViewModel { } } - func updateState() { + func updateViewState() { + guard hasLoadedFirstGroup else { + return + } + let renderedPolls: [TimelinePollDetails] switch context.mode { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index fc8d14fad..29cea979f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -31,9 +31,9 @@ struct PollHistory: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) - if viewModel.viewState.loadingState.isLoadingOnLanding { + if viewModel.viewState.polls == nil { loadingView - } else if viewModel.viewState.loadingState.isLoading == false, viewModel.viewState.polls.isEmpty { + } else if viewModel.viewState.polls?.isEmpty == true { noPollsView } else { pollListView @@ -55,7 +55,7 @@ struct PollHistory: View { private var pollListView: some View { ScrollView { LazyVStack(spacing: 32) { - ForEach(viewModel.viewState.polls) { pollData in + ForEach(viewModel.viewState.polls ?? []) { pollData in PollListItem(pollData: pollData) } .frame(maxWidth: .infinity, alignment: .leading) @@ -69,7 +69,7 @@ struct PollHistory: View { private var loadMoreButton: some View { HStack(spacing: 8) { - if viewModel.viewState.loadingState.isLoading { + if viewModel.viewState.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle()) } From a7dea54852128190a597f7f59fe7a6677eb8b90b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 16:42:37 +0100 Subject: [PATCH 102/468] Add empty screen with number of days --- Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 8 +++ .../Coordinator/PollHistoryCoordinator.swift | 2 +- .../Room/PollHistory/PollHistoryModels.swift | 5 ++ .../PollHistory/PollHistoryViewModel.swift | 3 +- .../MatrixSDK/PollHistoryService.swift | 16 +++--- .../Room/PollHistory/View/PollHistory.swift | 50 +++++++++++++------ 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bba4c82dc..e181a16c5 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2309,6 +2309,8 @@ Tap the + to start adding people."; "poll_history_past_segment_title" = "Past polls"; "poll_history_no_active_poll_text" = "There are no active polls in this room"; "poll_history_no_past_poll_text" = "There are no past polls in this room"; +"poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months"; +"poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months"; // MARK: - Polls diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1428f0606..4e7089027 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4855,10 +4855,18 @@ public class VectorL10n: NSObject { public static var pollHistoryLoadingText: String { return VectorL10n.tr("Vector", "poll_history_loading_text") } + /// There are no active polls for the past %@ days. Load more polls to view polls for previous months + public static func pollHistoryNoActivePollPeriodText(_ p1: String) -> String { + return VectorL10n.tr("Vector", "poll_history_no_active_poll_period_text", p1) + } /// There are no active polls in this room public static var pollHistoryNoActivePollText: String { return VectorL10n.tr("Vector", "poll_history_no_active_poll_text") } + /// There are no past polls for the past %@ days. Load more polls to view polls for previous months + public static func pollHistoryNoPastPollPeriodText(_ p1: String) -> String { + return VectorL10n.tr("Vector", "poll_history_no_past_poll_period_text", p1) + } /// There are no past polls in this room public static var pollHistoryNoPastPollText: String { return VectorL10n.tr("Vector", "poll_history_no_past_poll_text") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 4b9f6c63f..0311cda4e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -33,7 +33,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable { init(parameters: PollHistoryCoordinatorParameters) { self.parameters = parameters - let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room) ) + let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room, chunkSizeInDays: PollHistoryConstants.chunkSizeInDays)) let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index ec2e8a4f6..7331dc3ed 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -16,6 +16,10 @@ // MARK: View model +enum PollHistoryConstants { + static let chunkSizeInDays: UInt = 30 +} + enum PollHistoryViewModelResult: Equatable { #warning("e.g. show poll detail") } @@ -38,6 +42,7 @@ struct PollHistoryViewState: BindableState { var bindings: PollHistoryViewBindings var isLoading = false + var canLoadMoreContent = true var polls: [TimelinePollDetails]? } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 175e41286..531cdd5b9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -23,7 +23,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel private let pollService: PollHistoryServiceProtocol private var polls: [TimelinePollDetails] = [] private var subcriptions: Set = .init() - private var hasLoadedFirstGroup: Bool = false + private var hasLoadedFirstGroup = false var completion: ((PollHistoryViewModelResult) -> Void)? @@ -87,6 +87,7 @@ private extension PollHistoryViewModel { polls[matchIndex] = poll } else { polls.append(poll) + polls.sort(by: { $0.startDate > $1.startDate }) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 1bdd34df0..23b9b8763 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -14,12 +14,13 @@ // limitations under the License. // -import MatrixSDK -import Foundation import Combine +import Foundation +import MatrixSDK final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom + private let chunkSizeInDays: UInt private let pollsSubject: PassthroughSubject = .init() private let errorSubject: PassthroughSubject = .init() private let isFetchingSubject: PassthroughSubject = .init() @@ -42,9 +43,10 @@ final class PollHistoryService: PollHistoryServiceProtocol { isFetchingSubject.eraseToAnyPublisher() } - init(room: MXRoom) { + init(room: MXRoom, chunkSizeInDays: UInt) { self.room = room - self.targetTimestamp = Date().addingTimeInterval(-TimeInterval(Constants.daysToSync) * Constants.oneDayInSeconds) + self.chunkSizeInDays = chunkSizeInDays + targetTimestamp = Date().addingTimeInterval(-TimeInterval(chunkSizeInDays) * Constants.oneDayInSeconds) } func next() { @@ -71,14 +73,13 @@ final class PollHistoryService: PollHistoryServiceProtocol { private extension PollHistoryService { enum Constants { static let pageSize: UInt = 250 - static let daysToSync: UInt = 30 static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } func setup(timeline: MXEventTimeline) { self.timeline = timeline - listner = timeline.listenToEvents([MXEventType.pollStart, MXEventType.roomMessage, MXEventType.roomEncrypted]) { [weak self] event, direction, roomState in + listner = timeline.listenToEvents([MXEventType.pollStart, MXEventType.roomMessage, MXEventType.roomEncrypted]) { [weak self] event, _, _ in if event.eventType == .pollStart { self?.aggregatePoll(pollStartEvent: event) } @@ -95,7 +96,7 @@ private extension PollHistoryService { func startPagination() { isFetchingSubject.send(true) - guard let timeline = timeline else { + guard let timeline = timeline else { isFetchingSubject.send(false) return } @@ -123,7 +124,6 @@ private extension PollHistoryService { case .failure(let error): #warning("Handle error") self.isFetchingSubject.send(false) - break } } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 29cea979f..a3f0dc268 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -31,13 +31,7 @@ struct PollHistory: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) - if viewModel.viewState.polls == nil { - loadingView - } else if viewModel.viewState.polls?.isEmpty == true { - noPollsView - } else { - pollListView - } + content } .padding(.top, 32) .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -52,6 +46,17 @@ struct PollHistory: View { } } + @ViewBuilder + private var content: some View { + if viewModel.viewState.polls == nil { + loadingView + } else if viewModel.viewState.polls?.isEmpty == true { + noPollsView + } else { + pollListView + } + } + private var pollListView: some View { ScrollView { LazyVStack(spacing: 32) { @@ -59,8 +64,9 @@ struct PollHistory: View { PollListItem(pollData: pollData) } .frame(maxWidth: .infinity, alignment: .leading) - + loadMoreButton + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.top, 32) .padding(.horizontal, 16) @@ -80,16 +86,32 @@ struct PollHistory: View { Text("Load more polls") } } - .frame(maxWidth: .infinity, alignment: .leading) } + @ViewBuilder private var noPollsView: some View { - Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText) - .font(theme.fonts.body) - .foregroundColor(theme.colors.secondaryContent) + if viewModel.viewState.canLoadMoreContent { + let days = PollHistoryConstants.chunkSizeInDays + + VStack(spacing: 32) { + Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollPeriodText("\(days)") : VectorL10n.pollHistoryNoPastPollPeriodText("\(days)")) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + .padding(.horizontal, 16) + .accessibilityIdentifier("PollHistory.emptyLoadMoreText") + + loadMoreButton + } .frame(maxHeight: .infinity) - .padding(.horizontal, 16) - .accessibilityIdentifier("PollHistory.emptyText") + } else { + Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText) + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + .accessibilityIdentifier("PollHistory.emptyText") + } } private var loadingView: some View { From 9187b026627c130ce9334438667da252fedafbc1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 16:55:19 +0100 Subject: [PATCH 103/468] Improve tests --- .../PollHistory/MockPollHistoryScreenState.swift | 7 ++++++- .../Service/Mock/MockPollHistoryService.swift | 4 ++-- .../Modules/Room/PollHistory/View/PollHistory.swift | 12 ++++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 00b7880f8..fc283be67 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import Foundation import SwiftUI @@ -28,6 +29,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { case activeEmpty case pastEmpty case loading + case loadingWithContent /// The associated screen var screenType: Any.Type { @@ -52,7 +54,10 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollService.pastPollsData = [] case .loading: pollHistoryMode = .active - pollService.fetchState = true + pollService.isLoadingPublisher = Just(true).eraseToAnyPublisher() + case .loadingWithContent: + pollHistoryMode = .active + pollService.isLoadingPublisher = [false, true].publisher.eraseToAnyPublisher() } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index b8646c771..fe8564450 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -33,9 +33,9 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { } } - var fetchState: Bool = false + var isLoadingPublisher: AnyPublisher = Just(false).eraseToAnyPublisher() var isFetching: AnyPublisher { - Just(fetchState).eraseToAnyPublisher() + isLoadingPublisher } var activePollsData: [TimelinePollDetails] = (1..<10) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index a3f0dc268..ac8f3a20e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -76,8 +76,7 @@ struct PollHistory: View { private var loadMoreButton: some View { HStack(spacing: 8) { if viewModel.viewState.isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) + spinner } Button { @@ -88,6 +87,12 @@ struct PollHistory: View { } } + @ViewBuilder + private var spinner: some View { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + @ViewBuilder private var noPollsView: some View { if viewModel.viewState.canLoadMoreContent { @@ -116,8 +121,7 @@ struct PollHistory: View { private var loadingView: some View { HStack(spacing: 8) { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) + spinner Text(VectorL10n.pollHistoryLoadingText) .font(theme.fonts.body) From 7486e1d1893a28b3f4ef47512d9c49bdc58c92c3 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 17:04:00 +0100 Subject: [PATCH 104/468] Improve UX --- .../Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift | 2 +- RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 5867b88e8..b6d062370 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -53,7 +53,7 @@ class PollHistoryUITests: MockScreenTestCase { func testPastPollHistoryIsEmpty() { app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title) let title = app.navigationBars.firstMatch.identifier - let emptyText = app.staticTexts["PollHistory.emptyText"] + let emptyText = app.staticTexts["PollHistory.emptyLoadMoreText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] let winningOption = app.staticTexts["PollListData.winningOption"] diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index ac8f3a20e..fcd133ee4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -84,6 +84,7 @@ struct PollHistory: View { } label: { Text("Load more polls") } + .disabled(viewModel.viewState.isLoading) } } From 8af080c04972618a47fe4b60a005c662a3ef3316 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 17:59:09 +0100 Subject: [PATCH 105/468] Improve error handling --- .../Service/MatrixSDK/PollHistoryService.swift | 10 +++++----- .../Service/Mock/MockPollHistoryService.swift | 2 +- .../Service/PollHistoryServiceProtocol.swift | 10 ++++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 23b9b8763..7bfc2b41e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -22,7 +22,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom private let chunkSizeInDays: UInt private let pollsSubject: PassthroughSubject = .init() - private let errorSubject: PassthroughSubject = .init() + private let errorSubject: PassthroughSubject = .init() private let isFetchingSubject: PassthroughSubject = .init() private var listner: Any? @@ -35,7 +35,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { pollsSubject.eraseToAnyPublisher() } - var error: AnyPublisher { + var error: AnyPublisher { errorSubject.eraseToAnyPublisher() } @@ -60,7 +60,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { let self = self, let timeline = timeline else { - #warning("Handle error") + self?.errorSubject.send(.timelineUnavailable) return } @@ -122,7 +122,7 @@ private extension PollHistoryService { self.isFetchingSubject.send(false) } case .failure(let error): - #warning("Handle error") + self.errorSubject.send(.paginationFailed(error)) self.isFetchingSubject.send(false) } } @@ -155,7 +155,7 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - #warning("Handle error") + errorSubject.send(.pollAggregationFailed(didFailWithError)) } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index fe8564450..aae34f6ec 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -23,7 +23,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { polls.eraseToAnyPublisher() } - var error: AnyPublisher { + var error: AnyPublisher { Empty().eraseToAnyPublisher() } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 3488e8bac..fc8130efc 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -22,12 +22,18 @@ protocol PollHistoryServiceProtocol { var pollHistory: AnyPublisher { get } /// Publishes whatever errors produced during the sync. - var error: AnyPublisher { get } + var error: AnyPublisher { get } /// Ask to fetch the next batch of polls. /// Concrete implementations can decide what a batch is. func next() - /// Inform the whenever a new batch of polls starts or ends. + /// Inform whenever the fetch of a new batch of polls starts or ends. var isFetching: AnyPublisher { get } } + +enum PollHistoryError: Error { + case paginationFailed(Error) + case timelineUnavailable + case pollAggregationFailed(Error) +} From dbe034760d6d9a335f25d3b471ca50cd74807d43 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 20:03:34 +0100 Subject: [PATCH 106/468] Cleanup code --- .../MatrixSDK/PollHistoryService.swift | 30 +++---------------- .../Service/PollHistoryServiceProtocol.swift | 1 - 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 7bfc2b41e..3062d6bfc 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -20,13 +20,13 @@ import MatrixSDK final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom + private let timeline: MXEventTimeline private let chunkSizeInDays: UInt private let pollsSubject: PassthroughSubject = .init() private let errorSubject: PassthroughSubject = .init() private let isFetchingSubject: PassthroughSubject = .init() private var listner: Any? - private var timeline: MXEventTimeline? private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date private var oldestEventDate: Date = .distantFuture @@ -46,27 +46,13 @@ final class PollHistoryService: PollHistoryServiceProtocol { init(room: MXRoom, chunkSizeInDays: UInt) { self.room = room self.chunkSizeInDays = chunkSizeInDays + self.timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) targetTimestamp = Date().addingTimeInterval(-TimeInterval(chunkSizeInDays) * Constants.oneDayInSeconds) + setup(timeline: timeline) } func next() { - guard timeline == nil else { - startPagination() - return - } - - room.liveTimeline { [weak self] timeline in - guard - let self = self, - let timeline = timeline - else { - self?.errorSubject.send(.timelineUnavailable) - return - } - - self.setup(timeline: timeline) - self.startPagination() - } + startPagination() } } @@ -77,8 +63,6 @@ private extension PollHistoryService { } func setup(timeline: MXEventTimeline) { - self.timeline = timeline - listner = timeline.listenToEvents([MXEventType.pollStart, MXEventType.roomMessage, MXEventType.roomEncrypted]) { [weak self] event, _, _ in if event.eventType == .pollStart { self?.aggregatePoll(pollStartEvent: event) @@ -95,12 +79,6 @@ private extension PollHistoryService { func startPagination() { isFetchingSubject.send(true) - - guard let timeline = timeline else { - isFetchingSubject.send(false) - return - } - timeline.resetPagination() paginate(timeline: timeline) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index fc8130efc..b7cac3874 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -34,6 +34,5 @@ protocol PollHistoryServiceProtocol { enum PollHistoryError: Error { case paginationFailed(Error) - case timelineUnavailable case pollAggregationFailed(Error) } From 9bfe61deb6ff6277124342c544b2f6c2d718134b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 20:06:33 +0100 Subject: [PATCH 107/468] Optimize page size --- .../Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 3062d6bfc..6a69fa18a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -58,7 +58,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private extension PollHistoryService { enum Constants { - static let pageSize: UInt = 250 + static let pageSize: UInt = 500 static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } From f6a9f5472ad2c8f8df9f618a20879103b40a53cf Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 20 Jan 2023 20:25:46 +0100 Subject: [PATCH 108/468] Cleanup code --- .../MatrixSDK/PollHistoryService.swift | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 6a69fa18a..0c03677f8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -26,7 +26,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let errorSubject: PassthroughSubject = .init() private let isFetchingSubject: PassthroughSubject = .init() - private var listner: Any? + private var timelineListener: Any? private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date private var oldestEventDate: Date = .distantFuture @@ -63,20 +63,19 @@ private extension PollHistoryService { } func setup(timeline: MXEventTimeline) { - listner = timeline.listenToEvents([MXEventType.pollStart, MXEventType.roomMessage, MXEventType.roomEncrypted]) { [weak self] event, _, _ in + timelineListener = timeline.listenToEvents { [weak self] event, _, _ in + guard let self = self else { + return + } + if event.eventType == .pollStart { - self?.aggregatePoll(pollStartEvent: event) + self.aggregatePoll(pollStartEvent: event) } - self?.updateTimestamp(event: event) + self.oldestEventDate = min(event.originServerDate, self.oldestEventDate) } } - func updateTimestamp(event: MXEvent) { - let eventDate = Date(timeIntervalSince1970: Double(event.originServerTs) / 1000) - oldestEventDate = min(eventDate, oldestEventDate) - } - func startPagination() { isFetchingSubject.send(true) timeline.resetPagination() @@ -84,10 +83,7 @@ private extension PollHistoryService { } func paginate(timeline: MXEventTimeline) { - timeline.paginate(Constants.pageSize, - direction: .backwards, - onlyFromStore: false) { [weak self] response in - + timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in guard let self = self else { return } @@ -123,6 +119,12 @@ private extension PollHistoryService { } } +private extension MXEvent { + var originServerDate: Date { + .init(timeIntervalSince1970: Double(originServerTs) / 1000) + } +} + // MARK: - PollAggregatorDelegate extension PollHistoryService: PollAggregatorDelegate { From 8f3a959ab67dc0b187e221379474735166c55d66 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 12:04:24 +0100 Subject: [PATCH 109/468] Refactor PollHistoryService --- .../MockPollHistoryScreenState.swift | 16 ++-- .../PollHistory/PollHistoryViewModel.swift | 65 ++++++------- .../MatrixSDK/PollHistoryService.swift | 70 ++++++++------ .../Service/Mock/MockPollHistoryService.swift | 96 ++++++++++--------- .../Service/PollHistoryServiceProtocol.swift | 20 +--- 5 files changed, 130 insertions(+), 137 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index fc283be67..4fc9f6cef 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -29,7 +29,6 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { case activeEmpty case pastEmpty case loading - case loadingWithContent /// The associated screen var screenType: Any.Type { @@ -48,16 +47,19 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollHistoryMode = .past case .activeEmpty: pollHistoryMode = .active - pollService.activePollsData = [] + pollService.nextPublisher = Empty(completeImmediately: true, + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() case .pastEmpty: pollHistoryMode = .past - pollService.pastPollsData = [] + pollService.nextPublisher = Empty(completeImmediately: true, + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() case .loading: pollHistoryMode = .active - pollService.isLoadingPublisher = Just(true).eraseToAnyPublisher() - case .loadingWithContent: - pollHistoryMode = .active - pollService.isLoadingPublisher = [false, true].publisher.eraseToAnyPublisher() + pollService.nextPublisher = Empty(completeImmediately: false, + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 531cdd5b9..02d1d853a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -21,9 +21,8 @@ typealias PollHistoryViewModelType = StateStoreViewModel = .init() - private var hasLoadedFirstGroup = false var completion: ((PollHistoryViewModelResult) -> Void)? @@ -37,8 +36,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel override func process(viewAction: PollHistoryViewAction) { switch viewAction { case .viewAppeared: - setupSubscriptions() - pollService.next() + setupUpdateSubscriptions() + fetchFirstBatch() case .segmentDidChange: updateViewState() } @@ -46,11 +45,27 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel } private extension PollHistoryViewModel { - func setupSubscriptions() { + private func fetchFirstBatch() { + state.isLoading = true + + pollService + .next() + .collect() + .sink { [weak self] _ in + #warning("Handle errors") + self?.state.isLoading = false + } receiveValue: { [weak self] polls in + self?.polls = polls + self?.updateViewState() + } + .store(in: &subcriptions) + } + + func setupUpdateSubscriptions() { subcriptions.removeAll() pollService - .pollHistory + .updates .sink { [weak self] detail in self?.updatePolls(with: detail) self?.updateViewState() @@ -58,53 +73,31 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) pollService - .error + .updatesErrors .sink { detail in #warning("Handle errors") } .store(in: &subcriptions) - - let didCompleteFirstFetch = pollService - .isFetching - .filter { $0 == false } - .first() - - didCompleteFirstFetch - .sink { isFetching in - self.hasLoadedFirstGroup = true - self.updateViewState() - } - .store(in: &subcriptions) - - pollService - .isFetching - .weakAssign(to: \.state.isLoading, on: self) - .store(in: &subcriptions) } func updatePolls(with poll: TimelinePollDetails) { - if let matchIndex = polls.firstIndex(where: { $0.id == poll.id }) { - polls[matchIndex] = poll + if let matchIndex = polls?.firstIndex(where: { $0.id == poll.id }) { + polls?[matchIndex] = poll } else { - polls.append(poll) - polls.sort(by: { $0.startDate > $1.startDate }) + polls?.append(poll) } } func updateViewState() { - guard hasLoadedFirstGroup else { - return - } - - let renderedPolls: [TimelinePollDetails] + let renderedPolls: [TimelinePollDetails]? switch context.mode { case .active: - renderedPolls = polls.filter { $0.closed == false } + renderedPolls = polls?.filter { $0.closed == false } case .past: - renderedPolls = polls.filter { $0.closed == true } + renderedPolls = polls?.filter { $0.closed == true } } - state.polls = renderedPolls + state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate }) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 0c03677f8..575832053 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -22,25 +22,22 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom private let timeline: MXEventTimeline private let chunkSizeInDays: UInt - private let pollsSubject: PassthroughSubject = .init() - private let errorSubject: PassthroughSubject = .init() - private let isFetchingSubject: PassthroughSubject = .init() - private var timelineListener: Any? + + private let updatesSubject: PassthroughSubject = .init() + private let updatesErrorsSubject: PassthroughSubject = .init() + private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date private var oldestEventDate: Date = .distantFuture + private var currentBatchSubject: PassthroughSubject? - var pollHistory: AnyPublisher { - pollsSubject.eraseToAnyPublisher() + var updates: AnyPublisher { + updatesSubject.eraseToAnyPublisher() } - var error: AnyPublisher { - errorSubject.eraseToAnyPublisher() - } - - var isFetching: AnyPublisher { - isFetchingSubject.eraseToAnyPublisher() + var updatesErrors: AnyPublisher { + updatesErrorsSubject.eraseToAnyPublisher() } init(room: MXRoom, chunkSizeInDays: UInt) { @@ -51,8 +48,8 @@ final class PollHistoryService: PollHistoryServiceProtocol { setup(timeline: timeline) } - func next() { - startPagination() + func next() -> AnyPublisher { + currentBatchSubject?.eraseToAnyPublisher() ?? startPagination() } } @@ -64,22 +61,31 @@ private extension PollHistoryService { func setup(timeline: MXEventTimeline) { timelineListener = timeline.listenToEvents { [weak self] event, _, _ in - guard let self = self else { - return - } - if event.eventType == .pollStart { - self.aggregatePoll(pollStartEvent: event) + self?.aggregatePoll(pollStartEvent: event) } - self.oldestEventDate = min(event.originServerDate, self.oldestEventDate) + self?.updateTimestamp(event: event) } } - func startPagination() { - isFetchingSubject.send(true) - timeline.resetPagination() - paginate(timeline: timeline) + func updateTimestamp(event: MXEvent) { + oldestEventDate = min(event.originServerDate, oldestEventDate) + } + + func startPagination() -> AnyPublisher { + let batchSubject = PassthroughSubject() + currentBatchSubject = batchSubject + + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + self.timeline.resetPagination() + self.paginate(timeline: self.timeline) + } + + return batchSubject.eraseToAnyPublisher() } func paginate(timeline: MXEventTimeline) { @@ -93,15 +99,19 @@ private extension PollHistoryService { if timeline.canPaginate(.backwards), self.timestampTargetReached == false { self.paginate(timeline: timeline) } else { - self.isFetchingSubject.send(false) + self.completeBatch(completion: .finished) } case .failure(let error): - self.errorSubject.send(.paginationFailed(error)) - self.isFetchingSubject.send(false) + self.completeBatch(completion: .failure(error)) } } } + func completeBatch(completion: Subscribers.Completion) { + currentBatchSubject?.send(completion: completion) + currentBatchSubject = nil + } + func aggregatePoll(pollStartEvent: MXEvent) { guard pollAggregators[pollStartEvent.eventId] == nil else { return @@ -131,14 +141,14 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {} func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - pollsSubject.send(.init(poll: aggregator.poll, represent: .started)) + currentBatchSubject?.send(.init(poll: aggregator.poll, represent: .started)) } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - errorSubject.send(.pollAggregationFailed(didFailWithError)) + updatesErrorsSubject.send(didFailWithError) } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - pollsSubject.send(.init(poll: aggregator.poll, represent: .started)) + updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index aae34f6ec..be6474cb3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -17,54 +17,56 @@ import Combine final class MockPollHistoryService: PollHistoryServiceProtocol { - private let polls: PassthroughSubject = .init() - - var pollHistory: AnyPublisher { - polls.eraseToAnyPublisher() - } - - var error: AnyPublisher { + var updates: AnyPublisher { Empty().eraseToAnyPublisher() } - - func next() { - for poll in activePollsData + pastPollsData { - polls.send(poll) - } - } - - var isLoadingPublisher: AnyPublisher = Just(false).eraseToAnyPublisher() - var isFetching: AnyPublisher { - isLoadingPublisher - } - - var activePollsData: [TimelinePollDetails] = (1..<10) - .map { index in - TimelinePollDetails(id: "a\(index)", - question: "Do you like the active poll number \(index)?", - answerOptions: [], - closed: false, - startDate: .init(), - totalAnswerCount: 30, - type: .disclosed, - eventType: .started, - maxAllowedSelections: 1, - hasBeenEdited: false, - hasDecryptionError: false) - } - var pastPollsData: [TimelinePollDetails] = (1..<10) - .map { index in - TimelinePollDetails(id: "p\(index)", - question: "Do you like the active poll number \(index)?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], - closed: true, - startDate: .init(), - totalAnswerCount: 30, - type: .disclosed, - eventType: .started, - maxAllowedSelections: 1, - hasBeenEdited: false, - hasDecryptionError: false) - } + var updatesErrors: AnyPublisher { + Empty().eraseToAnyPublisher() + } + + lazy var nextPublisher: AnyPublisher = (activePollsData + pastPollsData) + .publisher + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + func next() -> AnyPublisher { + nextPublisher + } +} + +private extension MockPollHistoryService { + var activePollsData: [TimelinePollDetails] { + (1..<10) + .map { index in + TimelinePollDetails(id: "a\(index)", + question: "Do you like the active poll number \(index)?", + answerOptions: [], + closed: false, + startDate: .init(), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } + } + + var pastPollsData: [TimelinePollDetails] { + (1..<10) + .map { index in + TimelinePollDetails(id: "p\(index)", + question: "Do you like the active poll number \(index)?", + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], + closed: true, + startDate: .init(), + totalAnswerCount: 30, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index b7cac3874..bec8dbe53 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -17,22 +17,8 @@ import Combine protocol PollHistoryServiceProtocol { - /// Publishes poll data as soon they are found in the timeline. - /// Updates are also published here, so clients needs to address duplicates. - var pollHistory: AnyPublisher { get } + var updates: AnyPublisher { get } + var updatesErrors: AnyPublisher { get } - /// Publishes whatever errors produced during the sync. - var error: AnyPublisher { get } - - /// Ask to fetch the next batch of polls. - /// Concrete implementations can decide what a batch is. - func next() - - /// Inform whenever the fetch of a new batch of polls starts or ends. - var isFetching: AnyPublisher { get } -} - -enum PollHistoryError: Error { - case paginationFailed(Error) - case pollAggregationFailed(Error) + func next() -> AnyPublisher } From 5cea4e441ddabfd6a520e49f392c6fe7fa8315f2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 14:57:34 +0100 Subject: [PATCH 110/468] Add PollHistory view model UTs --- .../MatrixSDK/PollHistoryService.swift | 2 +- .../Service/Mock/MockPollHistoryService.swift | 16 ++-- .../Test/UI/PollHistoryUITests.swift | 13 ++- .../Test/Unit/PollHistoryViewModelTests.swift | 90 +++++++++++++++++++ .../Room/PollHistory/View/PollHistory.swift | 2 +- .../Unit/TimelinePollViewModelTests.swift | 4 +- 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 575832053..6a7499248 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -43,7 +43,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { init(room: MXRoom, chunkSizeInDays: UInt) { self.room = room self.chunkSizeInDays = chunkSizeInDays - self.timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) + timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) targetTimestamp = Date().addingTimeInterval(-TimeInterval(chunkSizeInDays) * Constants.oneDayInSeconds) setup(timeline: timeline) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index be6474cb3..7f412b94e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,12 +17,14 @@ import Combine final class MockPollHistoryService: PollHistoryServiceProtocol { + var updatesPublisher: AnyPublisher = Empty().eraseToAnyPublisher() var updates: AnyPublisher { - Empty().eraseToAnyPublisher() + updatesPublisher } + var updatesErrorsPublisher: AnyPublisher = Empty().eraseToAnyPublisher() var updatesErrors: AnyPublisher { - Empty().eraseToAnyPublisher() + updatesErrorsPublisher } lazy var nextPublisher: AnyPublisher = (activePollsData + pastPollsData) @@ -37,13 +39,13 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { private extension MockPollHistoryService { var activePollsData: [TimelinePollDetails] { - (1..<10) + (1...10) .map { index in TimelinePollDetails(id: "a\(index)", question: "Do you like the active poll number \(index)?", answerOptions: [], closed: false, - startDate: .init(), + startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, type: .disclosed, eventType: .started, @@ -54,13 +56,13 @@ private extension MockPollHistoryService { } var pastPollsData: [TimelinePollDetails] { - (1..<10) + (1...10) .map { index in TimelinePollDetails(id: "p\(index)", question: "Do you like the active poll number \(index)?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], closed: true, - startDate: .init(), + startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, type: .disclosed, eventType: .started, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index b6d062370..bd08d10c2 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -17,7 +17,7 @@ import RiotSwiftUI import XCTest -class PollHistoryUITests: MockScreenTestCase { +final class PollHistoryUITests: MockScreenTestCase { func testActivePollHistoryHasContent() { app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title) let title = app.navigationBars.firstMatch.identifier @@ -65,4 +65,15 @@ class PollHistoryUITests: MockScreenTestCase { XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) XCTAssertFalse(winningOption.exists) } + + func testLoaderIsShowing() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.loading.title) + let title = app.navigationBars.firstMatch.identifier + let loadingText = app.staticTexts["PollHistory.loadingText"] + let items = app.staticTexts["PollListItem.title"] + + XCTAssertEqual(title, VectorL10n.pollHistoryTitle) + XCTAssertFalse(items.exists) + XCTAssertTrue(loadingText.exists) + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift new file mode 100644 index 000000000..f5694a7bb --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -0,0 +1,90 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +@testable import RiotSwiftUI +import XCTest + +final class PollHistoryViewModelTests: XCTestCase { + private var viewModel: PollHistoryViewModel! + private var pollHistoryService: MockPollHistoryService = .init() + + override func setUpWithError() throws { + pollHistoryService = .init() + viewModel = .init(mode: .active, pollService: pollHistoryService) + } + + func testEmitsContentOnLanding() throws { + XCTAssert(viewModel.state.polls == nil) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(try polls.isEmpty) + } + + func testLoadingState() throws { + XCTAssertFalse(viewModel.state.isLoading) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) + XCTAssertFalse(try polls.isEmpty) + } + + func testLoadingStateIsTrueWhileLoading() { + XCTAssertFalse(viewModel.state.isLoading) + pollHistoryService.nextPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + XCTAssertTrue(viewModel.state.isLoading) + } + + func testUpdatesAreHandled() throws { + let mockUpdates: PassthroughSubject = .init() + pollHistoryService.updatesPublisher = mockUpdates.eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + + var firstPoll = try XCTUnwrap(try polls.first) + XCTAssertEqual(firstPoll.question, "Do you like the active poll number 9?") + firstPoll.question = "foo" + + mockUpdates.send(firstPoll) + + let updatedPoll = try XCTUnwrap(viewModel.state.polls?.first) + XCTAssertEqual(updatedPoll.question, "foo") + } + + func testSegmentsAreUpdated() throws { + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(try polls.isEmpty) + XCTAssertTrue(try polls.allSatisfy { !$0.closed }) + + viewModel.state.bindings.mode = .past + viewModel.process(viewAction: .segmentDidChange) + + XCTAssertTrue(try polls.allSatisfy(\.closed)) + } + + func testPollsAreReverseOrdered() throws { + viewModel.process(viewAction: .viewAppeared) + + let pollDates = try polls.map(\.startDate) + XCTAssertEqual(pollDates, pollDates.sorted(by: { $0 > $1 })) + } +} + +private extension PollHistoryViewModelTests { + var polls: [TimelinePollDetails] { + get throws { + try XCTUnwrap(viewModel.state.polls) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index fcd133ee4..cf6038bcd 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -80,7 +80,7 @@ struct PollHistory: View { } Button { - #warning("handle action") + #warning("handle action in next ticket") } label: { Text("Load more polls") } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index cd806da54..a36a7d092 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -29,9 +29,11 @@ class TimelinePollViewModelTests: XCTestCase { TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] - let timelinePoll = TimelinePollDetails(question: "Question", + let timelinePoll = TimelinePollDetails(id: "poll-id", + question: "Question", answerOptions: answerOptions, closed: false, + startDate: .init(), totalAnswerCount: 3, type: .disclosed, eventType: .started, From bbdda2a8b032438fefd899c4e311454ba08f0f2c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 15:18:58 +0100 Subject: [PATCH 111/468] Add docs --- .../Room/PollHistory/PollHistoryViewModel.swift | 2 +- .../Service/MatrixSDK/PollHistoryService.swift | 2 +- .../Service/Mock/MockPollHistoryService.swift | 6 +++--- .../Service/PollHistoryServiceProtocol.swift | 12 +++++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 02d1d853a..773a012fb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -73,7 +73,7 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) pollService - .updatesErrors + .pollErrors .sink { detail in #warning("Handle errors") } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 6a7499248..0d4a5db09 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -36,7 +36,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { updatesSubject.eraseToAnyPublisher() } - var updatesErrors: AnyPublisher { + var pollErrors: AnyPublisher { updatesErrorsSubject.eraseToAnyPublisher() } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 7f412b94e..9cfa8da3b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -22,9 +22,9 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { updatesPublisher } - var updatesErrorsPublisher: AnyPublisher = Empty().eraseToAnyPublisher() - var updatesErrors: AnyPublisher { - updatesErrorsPublisher + var pollErrorPublisher: AnyPublisher = Empty().eraseToAnyPublisher() + var pollErrors: AnyPublisher { + pollErrorPublisher } lazy var nextPublisher: AnyPublisher = (activePollsData + pastPollsData) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index bec8dbe53..3458c3d64 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -17,8 +17,14 @@ import Combine protocol PollHistoryServiceProtocol { - var updates: AnyPublisher { get } - var updatesErrors: AnyPublisher { get } - + /// Returns a Publisher publishing the polls in the next batch. + /// Implementations should return the same publisher if `next()` is called again before the previous publisher completes. func next() -> AnyPublisher + + /// Publishes updates for the polls previously pusblished by the `next()` publishers. + var updates: AnyPublisher { get } + + /// Publishes errors regarding poll aggregations. + /// Note: `next()` will continue to publish new polls even if some poll isn't being aggregated correctly. + var pollErrors: AnyPublisher { get } } From 3b5cbe1c6162b42d478210b2f7dc0a996d5c8e73 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 15:31:44 +0100 Subject: [PATCH 112/468] Cleanup --- .../Modules/Room/PollHistory/PollHistoryViewModel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 773a012fb..5f95067a5 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -81,11 +81,11 @@ private extension PollHistoryViewModel { } func updatePolls(with poll: TimelinePollDetails) { - if let matchIndex = polls?.firstIndex(where: { $0.id == poll.id }) { - polls?[matchIndex] = poll - } else { - polls?.append(poll) + guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else { + return } + + polls?[pollIndex] = poll } func updateViewState() { From f06bee79d4eaaebb2c565bc89d7528f1655b42e9 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 16:02:26 +0100 Subject: [PATCH 113/468] Cleanup --- RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 5f95067a5..efa218b52 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -45,7 +45,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel } private extension PollHistoryViewModel { - private func fetchFirstBatch() { + func fetchFirstBatch() { state.isLoading = true pollService From a4e2db4d551a1c07bb0966ef8ff01293f6413bc1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 16:05:01 +0100 Subject: [PATCH 114/468] Rename private var --- .../PollHistory/Service/MatrixSDK/PollHistoryService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 0d4a5db09..3afd48887 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -25,7 +25,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private var timelineListener: Any? private let updatesSubject: PassthroughSubject = .init() - private let updatesErrorsSubject: PassthroughSubject = .init() + private let pollErrorsSubject: PassthroughSubject = .init() private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date @@ -37,7 +37,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { } var pollErrors: AnyPublisher { - updatesErrorsSubject.eraseToAnyPublisher() + pollErrorsSubject.eraseToAnyPublisher() } init(room: MXRoom, chunkSizeInDays: UInt) { @@ -145,7 +145,7 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - updatesErrorsSubject.send(didFailWithError) + pollErrorsSubject.send(didFailWithError) } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { From b63e3b219e0b697ee32f776f4facf40ac3bc7bd2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 16:09:11 +0100 Subject: [PATCH 115/468] Add changelog.d file --- changelog.d/pr-7293.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7293.change diff --git a/changelog.d/pr-7293.change b/changelog.d/pr-7293.change new file mode 100644 index 000000000..4fe2717d2 --- /dev/null +++ b/changelog.d/pr-7293.change @@ -0,0 +1 @@ +Polls: add logic for fetching poll histories in rooms. From 333cba06062a8ef26193c5821541cd5e36ffc323 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Thu, 19 Jan 2023 15:42:45 +0100 Subject: [PATCH 116/468] added poll detail scene with mock data --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 2 +- .../Coordinator/PollHistoryCoordinator.swift | 42 ++++- .../PollHistoryDetailCoordinator.swift | 105 +++++++++++ .../MockPollHistoryDetailScreenState.swift | 55 ++++++ .../PollHistoryDetailModels.swift | 88 ++++++++++ .../PollHistoryDetailViewModel.swift | 115 ++++++++++++ .../PollHistoryDetailViewModelProtocol.swift | 21 +++ .../Test/UI/PollHistoryDetailUITests.swift | 38 ++++ .../PollHistoryDetailViewModelTests.swift | 48 ++++++ .../View/PollHistoryDetail.swift | 122 +++++++++++++ .../PollHistoryDetailAnswerOptionButton.swift | 163 ++++++++++++++++++ .../Room/PollHistory/PollHistoryModels.swift | 5 +- .../PollHistory/PollHistoryViewModel.swift | 2 + .../Room/PollHistory/View/PollHistory.swift | 3 + 14 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift create mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 558951391..bb15c964e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -176,7 +176,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { coordinator.start() push(coordinator: coordinator) case .pollHistory: - let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: self.room)) + let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, session: session, room: room, navigationRouter: navigationRouter)) coordinator.start() push(coordinator: coordinator) default: diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 0311cda4e..74966fb21 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -15,17 +15,21 @@ // import CommonKit +import MatrixSDK import SwiftUI struct PollHistoryCoordinatorParameters { let mode: PollHistoryMode + let session: MXSession let room: MXRoom + let navigationRouter: NavigationRouterType } -final class PollHistoryCoordinator: Coordinator, Presentable { +final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { private let parameters: PollHistoryCoordinatorParameters private let pollHistoryHostingController: UIViewController private var pollHistoryViewModel: PollHistoryViewModelProtocol + private let navigationRouter: NavigationRouterType // Must be used only internally var childCoordinators: [Coordinator] = [] @@ -37,6 +41,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable { let view = PollHistory(viewModel: viewModel.context) pollHistoryViewModel = viewModel pollHistoryHostingController = VectorHostingController(rootView: view) + navigationRouter = parameters.navigationRouter } // MARK: - Public @@ -44,11 +49,44 @@ final class PollHistoryCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[PollHistoryCoordinator] did start.") pollHistoryViewModel.completion = { [weak self] result in - self?.completion?() + switch result { + case .showPollDetail(let poll): + self?.showPollDetail(poll) + } } } + func showPollDetail(_ poll: PollListData) { + let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: .dummy, session: parameters.session, room: parameters.room)) + detailCoordinator.toPresentable().presentationController?.delegate = self + detailCoordinator.completion = { [weak self, weak detailCoordinator] result in + guard let self = self, let coordinator = detailCoordinator else { return } + switch result { + case .dismiss: + self.toPresentable().dismiss(animated: true) + self.remove(childCoordinator: coordinator) + default: + break + } + } + + add(childCoordinator: detailCoordinator) + detailCoordinator.start() + toPresentable().present(detailCoordinator.toPresentable(), animated: true) + } + func toPresentable() -> UIViewController { pollHistoryHostingController } } + +// MARK: UIAdaptivePresentationControllerDelegate + +extension PollHistoryCoordinator: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + guard let coordinator = childCoordinators.last else { + return + } + remove(childCoordinator: coordinator) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift new file mode 100644 index 000000000..f08228dfc --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -0,0 +1,105 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit +import SwiftUI +import Combine + +struct PollHistoryDetailCoordinatorParameters { + let pollHistoryDetails: PollHistoryDetails + let session: MXSession + let room: MXRoom +} + +final class PollHistoryDetailCoordinator: Coordinator, Presentable { + private let parameters: PollHistoryDetailCoordinatorParameters + private let pollHistoryDetailHostingController: UIViewController + private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol + private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() + private var cancellables = Set() + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((PollHistoryDetailViewModelResult) -> Void)? + + init(parameters: PollHistoryDetailCoordinatorParameters) { + self.parameters = parameters + + let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails) + let view = PollHistoryDetail(viewModel: viewModel.context) + pollHistoryDetailViewModel = viewModel + + pollHistoryDetailHostingController = VectorHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryDetailHostingController) + + viewModel.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case .selectedAnswerOptionsWithIdentifiers(let identifiers): + self.selectedAnswerIdentifiersSubject.send(identifiers) + case .dismiss: + self.completion?(.dismiss) + } + } + selectedAnswerIdentifiersSubject + .debounce(for: 2.0, scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] identifiers in + guard let self = self else { return } + +// self.parameters.room.sendPollResponse(for: parameters.pollEvent, +// withAnswerIdentifiers: identifiers, +// threadId: nil, +// localEcho: nil, success: nil) { [weak self] error in +// guard let self = self else { return } +// +// MXLog.error("[TimelinePollCoordinator]] Failed submitting response", context: error) +// +// self.viewModel.showAnsweringFailure() +// } + } + .store(in: &cancellables) + } + + // MARK: - Public + + func start() { + MXLog.debug("[PollHistoryDetailCoordinator] did start.") + + } + + func toPresentable() -> UIViewController { + pollHistoryDetailHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift new file mode 100644 index 000000000..4e85cc552 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -0,0 +1,55 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { + case openDisclosed + case closedDisclosed + case openUndisclosed + case closedUndisclosed + case closedPollEnded + + var screenType: Any.Type { + PollHistoryDetails.self + } + + var poll: PollHistoryDetails { + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + + let poll = PollHistoryDetails(question: "Question", + answerOptions: answerOptions, + closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, + totalAnswerCount: 20, + type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, + eventType: self == .closedPollEnded ? .ended : .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + return poll + } + + var screenView: ([Any], AnyView) { + + + let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll) + + return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift new file mode 100644 index 000000000..b2d3bb7fc --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -0,0 +1,88 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult) -> Void + +enum PollHistoryDetailViewModelResult { + case selectedAnswerOptionsWithIdentifiers([String]) + case dismiss +} + +// MARK: View model + +struct PollHistoryDetails { + + public static let dummy: PollHistoryDetails = MockPollHistoryDetailScreenState.openUndisclosed.poll + + var question: String + var answerOptions: [TimelinePollAnswerOption] + var closed: Bool + var totalAnswerCount: UInt + var type: TimelinePollType + var eventType: TimelinePollEventType + var maxAllowedSelections: UInt + var hasBeenEdited = true + var hasDecryptionError: Bool + + init(question: String, answerOptions: [TimelinePollAnswerOption], + closed: Bool, + totalAnswerCount: UInt, + type: TimelinePollType, + eventType: TimelinePollEventType, + maxAllowedSelections: UInt, + hasBeenEdited: Bool, + hasDecryptionError: Bool) { + self.question = question + self.answerOptions = answerOptions + self.closed = closed + self.totalAnswerCount = totalAnswerCount + self.type = type + self.eventType = eventType + self.maxAllowedSelections = maxAllowedSelections + self.hasBeenEdited = hasBeenEdited + self.hasDecryptionError = hasDecryptionError + } + + var hasCurrentUserVoted: Bool { + answerOptions.filter { $0.selected == true }.count > 0 + } + + var shouldDiscloseResults: Bool { + if closed { + return totalAnswerCount > 0 + } else { + return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted + } + } + + var representsPollEndedEvent: Bool { + eventType == .ended + } +} + +// MARK: View + +struct PollHistoryDetailViewState: BindableState { + var poll: PollHistoryDetails +} + +enum PollHistoryDetailViewAction { + case selectAnswerOptionWithIdentifier(String) +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift new file mode 100644 index 000000000..d22811100 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -0,0 +1,115 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias PollHistoryDetailViewModelType = StateStoreViewModel + +class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDetailViewModelProtocol { + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: PollHistoryDetailViewModelCallback? + + // MARK: - Setup + + init(pollHistoryDetails: PollHistoryDetails) { + super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails)) + + } + + // MARK: - Public + + override func process(viewAction: PollHistoryDetailViewAction) { + switch viewAction { + case .selectAnswerOptionWithIdentifier(let identifier): + guard !state.poll.closed else { + return + } + + if state.poll.maxAllowedSelections == 1 { + updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) + } else { + updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) + } + } + } + + + // MARK: - TimelinePollViewModelProtocol + + func updateWithPollDetails(_ pollDetails: PollHistoryDetails) { + state.poll = pollDetails + } + + func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) { + state.poll.answerOptions.updateEach { answerOption in + if answerOption.selected { + answerOption.selected = false + answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) + state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + } + + if answerOption.id == selectedAnswerIdentifier { + answerOption.selected = true + answerOption.count += 1 + state.poll.totalAnswerCount += 1 + } + } + + informCoordinatorOfSelectionUpdate(state: state, callback: callback) + } + + func updateMultiSelectPollLocalState(_ state: inout PollHistoryDetailViewState, selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) { + let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } + + let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 + + if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { + return + } + + state.poll.answerOptions.updateEach { answerOption in + if answerOption.id != selectedAnswerIdentifier { + return + } + + if answerOption.selected { + answerOption.selected = false + answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) + state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + } else { + answerOption.selected = true + answerOption.count += 1 + state.poll.totalAnswerCount += 1 + } + } + + informCoordinatorOfSelectionUpdate(state: state, callback: callback) + } + + func informCoordinatorOfSelectionUpdate(state: PollHistoryDetailViewState, callback: PollHistoryDetailViewModelCallback?) { + let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in + answerOption.selected ? answerOption.id : nil + } + + callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers)) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift new file mode 100644 index 000000000..4feadbfb0 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift @@ -0,0 +1,21 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol PollHistoryDetailViewModelProtocol { + var context: PollHistoryDetailViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift new file mode 100644 index 000000000..27d68342b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift @@ -0,0 +1,38 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +class PollHistoryDetailUITests: MockScreenTestCase { + func testPollHistoryDetailPromptRegular() { + let promptType = PollHistoryDetailPromptType.regular + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title) + + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + + func testPollHistoryDetailPromptUpgrade() { + let promptType = PollHistoryDetailPromptType.upgrade + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title) + + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift new file mode 100644 index 000000000..0de398a49 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -0,0 +1,48 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import RiotSwiftUI + +class PollHistoryDetailViewModelTests: XCTestCase { + private enum Constants { + static let counterInitialValue = 0 + } + + var viewModel: PollHistoryDetailViewModelProtocol! + var context: PollHistoryDetailViewModelType.Context! + + override func setUpWithError() throws { + viewModel = PollHistoryDetailViewModel(promptType: .regular, initialCount: Constants.counterInitialValue) + context = viewModel.context + } + + func testInitialState() { + XCTAssertEqual(context.viewState.count, Constants.counterInitialValue) + } + + func testCounter() throws { + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 1) + + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 2) + + context.send(viewAction: .decrementCount) + XCTAssertEqual(context.viewState.count, 1) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift new file mode 100644 index 000000000..a6359dece --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -0,0 +1,122 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollHistoryDetail: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollHistoryDetailViewModel.Context + + var body: some View { + let poll = viewModel.viewState.poll + + VStack(alignment: .leading, spacing: 16.0) { + if poll.representsPollEndedEvent { + Text(VectorL10n.pollTimelineEndedText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + } + + Text(poll.question) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + + Text(editedText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + + VStack(spacing: 24.0) { + ForEach(poll.answerOptions) { answerOption in + PollHistoryDetailAnswerOptionButton(poll: poll, answerOption: answerOption) { + viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id)) + } + } + } + .disabled(poll.closed) + .fixedSize(horizontal: false, vertical: true) + + Text(totalVotesString) + .lineLimit(2) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + } + .padding([.horizontal], 16) + .padding([.top, .bottom]) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(theme.colors.background.ignoresSafeArea()) + .navigationTitle(navigationTitle) +// .alert(item: $viewModel.alertInfo) { info in +// info.alert +// } + } + + private var navigationTitle: String { + let poll = viewModel.viewState.poll + if poll.closed { + return VectorL10n.pollHistoryPastSegmentTitle + } else { + return VectorL10n.pollHistoryActiveSegmentTitle + } + } + + private var totalVotesString: String { + let poll = viewModel.viewState.poll + + if poll.hasDecryptionError, poll.totalAnswerCount > 0 { + return VectorL10n.pollTimelineDecryptionError + } + + if poll.closed { + if poll.totalAnswerCount == 1 { + return VectorL10n.pollTimelineTotalFinalResultsOneVote + } else { + return VectorL10n.pollTimelineTotalFinalResults(Int(poll.totalAnswerCount)) + } + } + + switch poll.totalAnswerCount { + case 0: + return VectorL10n.pollTimelineTotalNoVotes + case 1: + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? + VectorL10n.pollTimelineTotalOneVote : + VectorL10n.pollTimelineTotalOneVoteNotVoted) + default: + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? + VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : + VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) + } + } + + private var editedText: String { + viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" + } +} + +// MARK: - Previews + +struct PollHistoryDetail_Previews: PreviewProvider { + static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift new file mode 100644 index 000000000..e7ed7d75f --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift @@ -0,0 +1,163 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct PollHistoryDetailAnswerOptionButton: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + let poll: PollHistoryDetails + let answerOption: TimelinePollAnswerOption + let action: () -> Void + + // MARK: Public + + var body: some View { + Button(action: action) { + let rect = RoundedRectangle(cornerRadius: 4.0) + answerOptionLabel + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8.0) + .padding(.top, 12.0) + .padding(.bottom, 12.0) + .clipShape(rect) + .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) + .accentColor(progressViewAccentColor) + } + .accessibilityIdentifier("PollAnswerOption\(optionIndex)") + } + + var answerOptionLabel: some View { + VStack(alignment: .leading, spacing: 12.0) { + HStack(alignment: .top, spacing: 8.0) { + if !poll.closed { + Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) + } + + Text(answerOption.text) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label") + + if poll.closed, answerOption.winner { + Spacer() + Image(uiImage: Asset.Images.pollWinnerIcon.image) + } + } + + if poll.type == .disclosed || poll.closed { + HStack { + ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), + total: Double(poll.totalAnswerCount)) + .progressViewStyle(LinearProgressViewStyle()) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") + + 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") + } + } + } + } + } + + var borderAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent + } + + var progressViewAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent + } + + var optionIndex: Int { + poll.answerOptions.firstIndex { $0.id == answerOption.id } ?? Int.max + } +} + +struct PollHistoryDetailAnswerOptionButton_Previews: PreviewProvider { + static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer + + static var previews: some View { + Group { + let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed] + + ForEach(pollTypes, id: \.self) { type in + VStack { + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: false), + action: { }) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: true), + action: { }) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: false), + action: { }) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: true), + action: { }) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: false), + action: { }) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: true), + action: { }) + + let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(text: longText, selected: true, winner: true), + action: { }) + } + } + } + } + + static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { + TimelinePollDetails(question: "", + answerOptions: [], + closed: closed, + totalAnswerCount: 100, + type: type, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } + + static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { + TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 7331dc3ed..54e1adbbe 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -20,8 +20,8 @@ enum PollHistoryConstants { static let chunkSizeInDays: UInt = 30 } -enum PollHistoryViewModelResult: Equatable { - #warning("e.g. show poll detail") +enum PollHistoryViewModelResult { + case showPollDetail(poll: PollListData) } // MARK: View @@ -49,4 +49,5 @@ struct PollHistoryViewState: BindableState { enum PollHistoryViewAction { case viewAppeared case segmentDidChange + case showPollDetail(poll: PollListData) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index efa218b52..230bef3b6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -40,6 +40,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel fetchFirstBatch() case .segmentDidChange: updateViewState() + case .showPollDetail(let poll): + completion?(.showPollDetail(poll: poll)) } } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index cf6038bcd..abbfceea6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -62,6 +62,9 @@ struct PollHistory: View { LazyVStack(spacing: 32) { ForEach(viewModel.viewState.polls ?? []) { pollData in PollListItem(pollData: pollData) + .onTapGesture { + viewModel.send(viewAction: .showPollDetail(poll: pollData)) + } } .frame(maxWidth: .infinity, alignment: .leading) From a5487f91fa8714cafac7c48bc829aa903b1fe63d Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Mon, 23 Jan 2023 16:31:55 +0100 Subject: [PATCH 117/468] embedding swiftUI view --- .../Coordinator/PollHistoryCoordinator.swift | 2 +- .../PollHistoryDetailCoordinator.swift | 30 +--- .../MockPollHistoryDetailScreenState.swift | 7 +- .../PollHistoryDetailModels.swift | 56 +----- .../PollHistoryDetailViewModel.swift | 77 +-------- .../View/PollHistoryDetail.swift | 101 ++++------- .../PollHistoryDetailAnswerOptionButton.swift | 163 ------------------ 7 files changed, 53 insertions(+), 383 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 74966fb21..86b56fcde 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -57,7 +57,7 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { } func showPollDetail(_ poll: PollListData) { - let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: .dummy, session: parameters.session, room: parameters.room)) + let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: MockPollHistoryDetailScreenState.openUndisclosed.poll, session: parameters.session, room: parameters.room)) detailCoordinator.toPresentable().presentationController?.delegate = self detailCoordinator.completion = { [weak self, weak detailCoordinator] result in guard let self = self, let coordinator = detailCoordinator else { return } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index f08228dfc..744891fa3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -17,9 +17,10 @@ import CommonKit import SwiftUI import Combine +import MatrixSDK struct PollHistoryDetailCoordinatorParameters { - let pollHistoryDetails: PollHistoryDetails + let pollHistoryDetails: TimelinePollDetails let session: MXSession let room: MXRoom } @@ -28,7 +29,6 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { private let parameters: PollHistoryDetailCoordinatorParameters private let pollHistoryDetailHostingController: UIViewController private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol - private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var cancellables = Set() private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? @@ -40,6 +40,12 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { init(parameters: PollHistoryDetailCoordinatorParameters) { self.parameters = parameters +// let event: MXEvent = .init() +// do { +// let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.session, room: parameters.room, pollEvent: event)) +// } catch { +// MXLog.debug("[PollHistoryDetailCoordinator] initKeys: Failed to init TimelinePollCoordinator with event: \(error.localizedDescription)") +// } let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel @@ -51,30 +57,10 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { viewModel.completion = { [weak self] result in guard let self = self else { return } switch result { - case .selectedAnswerOptionsWithIdentifiers(let identifiers): - self.selectedAnswerIdentifiersSubject.send(identifiers) case .dismiss: self.completion?(.dismiss) } } - selectedAnswerIdentifiersSubject - .debounce(for: 2.0, scheduler: RunLoop.main) - .removeDuplicates() - .sink { [weak self] identifiers in - guard let self = self else { return } - -// self.parameters.room.sendPollResponse(for: parameters.pollEvent, -// withAnswerIdentifiers: identifiers, -// threadId: nil, -// localEcho: nil, success: nil) { [weak self] error in -// guard let self = self else { return } -// -// MXLog.error("[TimelinePollCoordinator]] Failed submitting response", context: error) -// -// self.viewModel.showAnsweringFailure() -// } - } - .store(in: &cancellables) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 4e85cc552..611b94438 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -25,15 +25,15 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { case closedPollEnded var screenType: Any.Type { - PollHistoryDetails.self + TimelinePollDetails.self } - var poll: PollHistoryDetails { + var poll: TimelinePollDetails { let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - let poll = PollHistoryDetails(question: "Question", + let poll = TimelinePollDetails(question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, totalAnswerCount: 20, @@ -47,7 +47,6 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { - let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index b2d3bb7fc..4e5bda481 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -21,68 +21,20 @@ import Foundation typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult) -> Void enum PollHistoryDetailViewModelResult { - case selectedAnswerOptionsWithIdentifiers([String]) case dismiss } // MARK: View model -struct PollHistoryDetails { - - public static let dummy: PollHistoryDetails = MockPollHistoryDetailScreenState.openUndisclosed.poll - - var question: String - var answerOptions: [TimelinePollAnswerOption] - var closed: Bool - var totalAnswerCount: UInt - var type: TimelinePollType - var eventType: TimelinePollEventType - var maxAllowedSelections: UInt - var hasBeenEdited = true - var hasDecryptionError: Bool - - init(question: String, answerOptions: [TimelinePollAnswerOption], - closed: Bool, - totalAnswerCount: UInt, - type: TimelinePollType, - eventType: TimelinePollEventType, - maxAllowedSelections: UInt, - hasBeenEdited: Bool, - hasDecryptionError: Bool) { - self.question = question - self.answerOptions = answerOptions - self.closed = closed - self.totalAnswerCount = totalAnswerCount - self.type = type - self.eventType = eventType - self.maxAllowedSelections = maxAllowedSelections - self.hasBeenEdited = hasBeenEdited - self.hasDecryptionError = hasDecryptionError - } - - var hasCurrentUserVoted: Bool { - answerOptions.filter { $0.selected == true }.count > 0 - } - - var shouldDiscloseResults: Bool { - if closed { - return totalAnswerCount > 0 - } else { - return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted - } - } - - var representsPollEndedEvent: Bool { - eventType == .ended - } -} + // MARK: View struct PollHistoryDetailViewState: BindableState { - var poll: PollHistoryDetails + var poll: TimelinePollDetails + var timelineViewModel: TimelinePollViewModel } enum PollHistoryDetailViewAction { - case selectAnswerOptionWithIdentifier(String) + case dismiss } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index d22811100..41f4e1ccf 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -25,91 +25,24 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet // MARK: Private // MARK: Public - var completion: PollHistoryDetailViewModelCallback? // MARK: - Setup - init(pollHistoryDetails: PollHistoryDetails) { - super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails)) - + init(pollHistoryDetails: TimelinePollDetails) { + super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails, timelineViewModel: TimelinePollViewModel(timelinePollDetails: pollHistoryDetails))) } // MARK: - Public override func process(viewAction: PollHistoryDetailViewAction) { switch viewAction { - case .selectAnswerOptionWithIdentifier(let identifier): - guard !state.poll.closed else { - return - } - - if state.poll.maxAllowedSelections == 1 { - updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) - } else { - updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) - } + case .dismiss: + completion?(.dismiss) } } // MARK: - TimelinePollViewModelProtocol - - func updateWithPollDetails(_ pollDetails: PollHistoryDetails) { - state.poll = pollDetails - } - - func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) { - state.poll.answerOptions.updateEach { answerOption in - if answerOption.selected { - answerOption.selected = false - answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) - } - - if answerOption.id == selectedAnswerIdentifier { - answerOption.selected = true - answerOption.count += 1 - state.poll.totalAnswerCount += 1 - } - } - - informCoordinatorOfSelectionUpdate(state: state, callback: callback) - } - - func updateMultiSelectPollLocalState(_ state: inout PollHistoryDetailViewState, selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) { - let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } - - let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 - - if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { - return - } - - state.poll.answerOptions.updateEach { answerOption in - if answerOption.id != selectedAnswerIdentifier { - return - } - - if answerOption.selected { - answerOption.selected = false - answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) - } else { - answerOption.selected = true - answerOption.count += 1 - state.poll.totalAnswerCount += 1 - } - } - - informCoordinatorOfSelectionUpdate(state: state, callback: callback) - } - - func informCoordinatorOfSelectionUpdate(state: PollHistoryDetailViewState, callback: PollHistoryDetailViewModelCallback?) { - let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in - answerOption.selected ? answerOption.id : nil - } - - callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers)) - } + } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index a6359dece..c50641c6c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -28,45 +28,41 @@ struct PollHistoryDetail: View { @ObservedObject var viewModel: PollHistoryDetailViewModel.Context var body: some View { - let poll = viewModel.viewState.poll - - VStack(alignment: .leading, spacing: 16.0) { - if poll.representsPollEndedEvent { - Text(VectorL10n.pollTimelineEndedText) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.tertiaryContent) + navigation + .padding([.horizontal], 16) + .padding([.top, .bottom]) + .background(theme.colors.background.ignoresSafeArea()) + } + + private var navigation: some View { + if #available(iOS 16.0, *) { + return NavigationStack { + content } - - Text(poll.question) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + - Text(editedText) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.secondaryContent) - - VStack(spacing: 24.0) { - ForEach(poll.answerOptions) { answerOption in - PollHistoryDetailAnswerOptionButton(poll: poll, answerOption: answerOption) { - viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id)) - } - } + } else { + return NavigationView { + content + } + } + } + private var content: some View { + let timelineViewModel = viewModel.viewState.timelineViewModel + return TimelinePollView(viewModel: timelineViewModel.context) + .navigationTitle(navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: btnBack) + } + + private var btnBack : some View { Button(action: { + viewModel.send(viewAction: .dismiss) + }) { + HStack { + Image(systemName: "xmark") //"chevron.left" + .aspectRatio(contentMode: .fit) + .foregroundColor(theme.colors.accent) } - .disabled(poll.closed) - .fixedSize(horizontal: false, vertical: true) - - Text(totalVotesString) - .lineLimit(2) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.tertiaryContent) } - .padding([.horizontal], 16) - .padding([.top, .bottom]) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(theme.colors.background.ignoresSafeArea()) - .navigationTitle(navigationTitle) -// .alert(item: $viewModel.alertInfo) { info in -// info.alert -// } } private var navigationTitle: String { @@ -77,39 +73,6 @@ struct PollHistoryDetail: View { return VectorL10n.pollHistoryActiveSegmentTitle } } - - private var totalVotesString: String { - let poll = viewModel.viewState.poll - - if poll.hasDecryptionError, poll.totalAnswerCount > 0 { - return VectorL10n.pollTimelineDecryptionError - } - - if poll.closed { - if poll.totalAnswerCount == 1 { - return VectorL10n.pollTimelineTotalFinalResultsOneVote - } else { - return VectorL10n.pollTimelineTotalFinalResults(Int(poll.totalAnswerCount)) - } - } - - switch poll.totalAnswerCount { - case 0: - return VectorL10n.pollTimelineTotalNoVotes - case 1: - return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? - VectorL10n.pollTimelineTotalOneVote : - VectorL10n.pollTimelineTotalOneVoteNotVoted) - default: - return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? - VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : - VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) - } - } - - private var editedText: String { - viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" - } } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift deleted file mode 100644 index e7ed7d75f..000000000 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetailAnswerOptionButton.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct PollHistoryDetailAnswerOptionButton: View { - // MARK: - Properties - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - - let poll: PollHistoryDetails - let answerOption: TimelinePollAnswerOption - let action: () -> Void - - // MARK: Public - - var body: some View { - Button(action: action) { - let rect = RoundedRectangle(cornerRadius: 4.0) - answerOptionLabel - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8.0) - .padding(.top, 12.0) - .padding(.bottom, 12.0) - .clipShape(rect) - .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) - .accentColor(progressViewAccentColor) - } - .accessibilityIdentifier("PollAnswerOption\(optionIndex)") - } - - var answerOptionLabel: some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(alignment: .top, spacing: 8.0) { - if !poll.closed { - Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) - } - - Text(answerOption.text) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label") - - if poll.closed, answerOption.winner { - Spacer() - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - } - - if poll.type == .disclosed || poll.closed { - HStack { - ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), - total: Double(poll.totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") - - 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") - } - } - } - } - } - - var borderAccentColor: Color { - guard !poll.closed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent - } - - var progressViewAccentColor: Color { - guard !poll.closed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent - } - - var optionIndex: Int { - poll.answerOptions.firstIndex { $0.id == answerOption.id } ?? Int.max - } -} - -struct PollHistoryDetailAnswerOptionButton_Previews: PreviewProvider { - static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer - - static var previews: some View { - Group { - let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed] - - ForEach(pollTypes, id: \.self) { type in - VStack { - TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), - answerOption: buildAnswerOption(selected: false), - action: { }) - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), - answerOption: buildAnswerOption(selected: true), - action: { }) - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), - answerOption: buildAnswerOption(selected: false, winner: false), - action: { }) - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), - answerOption: buildAnswerOption(selected: false, winner: true), - action: { }) - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), - answerOption: buildAnswerOption(selected: true, winner: false), - action: { }) - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), - answerOption: buildAnswerOption(selected: true, winner: true), - action: { }) - - let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." - - TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), - answerOption: buildAnswerOption(text: longText, selected: true, winner: true), - action: { }) - } - } - } - } - - static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { - TimelinePollDetails(question: "", - answerOptions: [], - closed: closed, - totalAnswerCount: 100, - type: type, - eventType: .started, - maxAllowedSelections: 1, - hasBeenEdited: false, - hasDecryptionError: false) - } - - static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { - TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) - } -} From 5415b94aa6e4fdbe6d16dbe25e189fce226c083e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 23 Jan 2023 16:46:52 +0100 Subject: [PATCH 118/468] Fix UT --- .../Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift index f5694a7bb..2814223df 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -53,7 +53,7 @@ final class PollHistoryViewModelTests: XCTestCase { viewModel.process(viewAction: .viewAppeared) var firstPoll = try XCTUnwrap(try polls.first) - XCTAssertEqual(firstPoll.question, "Do you like the active poll number 9?") + XCTAssertEqual(firstPoll.question, "Do you like the active poll number 1?") firstPoll.question = "foo" mockUpdates.send(firstPoll) From 56d215fb603dca8212923f42137542ba36969ada Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 23 Jan 2023 16:47:17 +0100 Subject: [PATCH 119/468] Use the new endpoint for redaction --- Riot/Modules/Room/RoomViewController.m | 47 +++++++++++--------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e701aacf6..c1a760c48 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4302,36 +4302,27 @@ static CGSize kThreadListBarButtonItemImageSize; [self startActivityIndicator]; + NSArray* relationTypes = nil; // If it's a voice broadcast, delete the selected event and all related events (only if this feature is supported). - BOOL supportsRedactionWithRelations = self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelations || self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelationsUnstable; - if (supportsRedactionWithRelations && selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { - MXWeakify(self); - [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:@[MXEventRelationTypeReference] reason:nil success:^{ - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - } failure:^(NSError *error) { - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); - //Alert user - [self showError:error]; - }]; - - } else { - MXWeakify(self); - [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - } failure:^(NSError *error) { - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); - //Alert user - [self showError:error]; - }]; + if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { + // Check if the homeserver supports redaction with relations + if (self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelations || self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelationsUnstable) { + relationTypes = @[MXEventRelationTypeReference]; + } } + + MXWeakify(self); + [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:relationTypes reason:nil success:^{ + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); + //Alert user + [self showError:error]; + }]; }]]; } From 5192730821afe83fb3ef347665a03528afd18417 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 23 Jan 2023 17:06:47 +0100 Subject: [PATCH 120/468] Always try to delete a voicebroadcast with relations. The SDK will ensure the feature is supported. --- Riot/Modules/Room/RoomViewController.m | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index c1a760c48..3792fee64 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4303,12 +4303,9 @@ static CGSize kThreadListBarButtonItemImageSize; [self startActivityIndicator]; NSArray* relationTypes = nil; - // If it's a voice broadcast, delete the selected event and all related events (only if this feature is supported). + // If it's a voice broadcast, delete the selected event and all related events. if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { - // Check if the homeserver supports redaction with relations - if (self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelations || self.mainSession.store.supportedMatrixVersions.supportsRedactionWithRelationsUnstable) { - relationTypes = @[MXEventRelationTypeReference]; - } + relationTypes = @[MXEventRelationTypeReference]; } MXWeakify(self); From 2708a932ee9b4637a76b7924175f548e5b5a08e0 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Mon, 23 Jan 2023 17:13:33 +0100 Subject: [PATCH 121/468] some fix for models --- .../Room/PollHistory/Coordinator/PollHistoryCoordinator.swift | 4 +--- .../PollHistoryDetail/MockPollHistoryDetailScreenState.swift | 4 +++- RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 86b56fcde..b939aec38 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -56,7 +56,7 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { } } - func showPollDetail(_ poll: PollListData) { + func showPollDetail(_ poll: TimelinePollDetails) { let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: MockPollHistoryDetailScreenState.openUndisclosed.poll, session: parameters.session, room: parameters.room)) detailCoordinator.toPresentable().presentationController?.delegate = self detailCoordinator.completion = { [weak self, weak detailCoordinator] result in @@ -65,8 +65,6 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { case .dismiss: self.toPresentable().dismiss(animated: true) self.remove(childCoordinator: coordinator) - default: - break } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 611b94438..8394359be 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -33,9 +33,11 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - let poll = TimelinePollDetails(question: "Question", + let poll = TimelinePollDetails(id: "id", + question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, + startDate: .init(), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, eventType: self == .closedPollEnded ? .ended : .started, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 54e1adbbe..68c30e064 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -21,7 +21,7 @@ enum PollHistoryConstants { } enum PollHistoryViewModelResult { - case showPollDetail(poll: PollListData) + case showPollDetail(poll: TimelinePollDetails) } // MARK: View @@ -49,5 +49,5 @@ struct PollHistoryViewState: BindableState { enum PollHistoryViewAction { case viewAppeared case segmentDidChange - case showPollDetail(poll: PollListData) + case showPollDetail(poll: TimelinePollDetails) } From 6cbc6df94f96ed54449265d652904a351e1b5c4b Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Mon, 23 Jan 2023 17:36:11 +0100 Subject: [PATCH 122/468] added real event --- .../Coordinator/PollHistoryCoordinator.swift | 32 ++++++++++++------- .../PollHistoryDetailCoordinator.swift | 15 +++------ .../MockPollHistoryDetailScreenState.swift | 2 +- .../PollHistoryDetailModels.swift | 2 +- .../PollHistoryDetailViewModel.swift | 4 +-- .../Coordinator/TimelinePollCoordinator.swift | 2 +- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index b939aec38..2c3f2e13a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -57,20 +57,30 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { } func showPollDetail(_ poll: TimelinePollDetails) { - let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: MockPollHistoryDetailScreenState.openUndisclosed.poll, session: parameters.session, room: parameters.room)) - detailCoordinator.toPresentable().presentationController?.delegate = self - detailCoordinator.completion = { [weak self, weak detailCoordinator] result in - guard let self = self, let coordinator = detailCoordinator else { return } - switch result { - case .dismiss: - self.toPresentable().dismiss(animated: true) - self.remove(childCoordinator: coordinator) + + parameters.session.event(withEventId: poll.id, inRoom: parameters.room.roomId) { [weak self] response in + guard let self else { return } + if let event = response.value, + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(pollHistoryDetails: MockPollHistoryDetailScreenState.openUndisclosed.poll, event: event, session: self.parameters.session, room: self.parameters.room)) { + detailCoordinator.toPresentable().presentationController?.delegate = self + detailCoordinator.completion = { [weak self, weak detailCoordinator] result in + guard let self = self, let coordinator = detailCoordinator else { return } + switch result { + case .dismiss: + self.toPresentable().dismiss(animated: true) + self.remove(childCoordinator: coordinator) + } + } + + self.add(childCoordinator: detailCoordinator) + detailCoordinator.start() + self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) + } else { + // TODO: manage error } } - add(childCoordinator: detailCoordinator) - detailCoordinator.start() - toPresentable().present(detailCoordinator.toPresentable(), animated: true) + } func toPresentable() -> UIViewController { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index 744891fa3..2b68d31ae 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -21,6 +21,7 @@ import MatrixSDK struct PollHistoryDetailCoordinatorParameters { let pollHistoryDetails: TimelinePollDetails + let event: MXEvent let session: MXSession let room: MXRoom } @@ -37,19 +38,13 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { var childCoordinators: [Coordinator] = [] var completion: ((PollHistoryDetailViewModelResult) -> Void)? - init(parameters: PollHistoryDetailCoordinatorParameters) { + init(parameters: PollHistoryDetailCoordinatorParameters) throws { self.parameters = parameters - -// let event: MXEvent = .init() -// do { -// let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.session, room: parameters.room, pollEvent: event)) -// } catch { -// MXLog.debug("[PollHistoryDetailCoordinator] initKeys: Failed to init TimelinePollCoordinator with event: \(error.localizedDescription)") -// } - let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails) + let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.session, room: parameters.room, pollEvent: parameters.event)) + let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails, timelineViewModel: timelinePollCoordinator.viewModel) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel - + pollHistoryDetailHostingController = VectorHostingController(rootView: view) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryDetailHostingController) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 8394359be..c4e46f0e1 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -49,7 +49,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { - let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll) + let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll, timelineViewModel: TimelinePollViewModel(timelinePollDetails: poll)) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 4e5bda481..7be630114 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -32,7 +32,7 @@ enum PollHistoryDetailViewModelResult { struct PollHistoryDetailViewState: BindableState { var poll: TimelinePollDetails - var timelineViewModel: TimelinePollViewModel + var timelineViewModel: TimelinePollViewModelProtocol } enum PollHistoryDetailViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index 41f4e1ccf..3566cdf87 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -29,8 +29,8 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet // MARK: - Setup - init(pollHistoryDetails: TimelinePollDetails) { - super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails, timelineViewModel: TimelinePollViewModel(timelinePollDetails: pollHistoryDetails))) + init(pollHistoryDetails: TimelinePollDetails, timelineViewModel: TimelinePollViewModelProtocol) { + super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails, timelineViewModel: timelineViewModel)) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index d8c9f59f8..25f96f7b6 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -33,7 +33,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var pollAggregator: PollAggregator - private var viewModel: TimelinePollViewModelProtocol! + private(set) var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() // MARK: Public From aaa9d5b91e0eb9e1f3ea3db95ee7c008f0e43a07 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 23 Jan 2023 17:14:11 +0000 Subject: [PATCH 123/468] Fix compile error --- .../VoiceBroadcastSDK/VoiceBroadcastService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index c41d5d37d..66bd6dd57 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -298,7 +298,7 @@ extension MXRoom { threadId: String? = nil, sequence: UInt, success: @escaping ((String?) -> Void), - failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? { + failure: @escaping ((Swift.Error?) -> Void)) -> MXHTTPOperation? { guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference, eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else { failure(VoiceBroadcastServiceError.unknown) From 6fdee31c031aba787762751cfe3c06edb5f95200 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 24 Jan 2023 09:15:55 +0100 Subject: [PATCH 124/468] Fix a crash for some voice broadcast in case of redaction --- .../Coordinator/VoiceBroadcastPlaybackCoordinator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 963dba558..7df3eaa04 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -57,7 +57,8 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } deinit { - viewModel.context.send(viewAction: .redact) + // If init has failed, our viewmodel will be nil. + viewModel?.context.send(viewAction: .redact) } // MARK: - Public From 77d0bc65e5b36bfa96f67a789b6e8f8f4936d742 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:22:57 +0100 Subject: [PATCH 125/468] Rename update poll method --- .../Modules/Room/PollHistory/PollHistoryViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index efa218b52..620903867 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -67,7 +67,7 @@ private extension PollHistoryViewModel { pollService .updates .sink { [weak self] detail in - self?.updatePolls(with: detail) + self?.update(poll: detail) self?.updateViewState() } .store(in: &subcriptions) @@ -80,7 +80,7 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) } - func updatePolls(with poll: TimelinePollDetails) { + func update(poll: TimelinePollDetails) { guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else { return } From ee43787857d055d302156ad5265d9e2d4b3b8a41 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:27:20 +0100 Subject: [PATCH 126/468] Localize load more button --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e181a16c5..8ddf6ad5a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2311,6 +2311,7 @@ Tap the + to start adding people."; "poll_history_no_past_poll_text" = "There are no past polls in this room"; "poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months"; "poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months"; +"poll_history_load_more" = "Load more polls"; // MARK: - Polls diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4e7089027..1b475b9c0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4851,6 +4851,10 @@ public class VectorL10n: NSObject { public static var pollHistoryActiveSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_active_segment_title") } + /// Load more polls + public static var pollHistoryLoadMore: String { + return VectorL10n.tr("Vector", "poll_history_load_more") + } /// Displaying polls public static var pollHistoryLoadingText: String { return VectorL10n.tr("Vector", "poll_history_loading_text") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index cf6038bcd..111387165 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -82,7 +82,8 @@ struct PollHistory: View { Button { #warning("handle action in next ticket") } label: { - Text("Load more polls") + Text(VectorL10n.pollHistoryLoadMore) + .font(theme.fonts.body) } .disabled(viewModel.viewState.isLoading) } From 78c0744f0ae53555ce239464abb80cdf26b9b057 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:28:38 +0100 Subject: [PATCH 127/468] Update RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 111387165..21eec4068 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -89,7 +89,6 @@ struct PollHistory: View { } } - @ViewBuilder private var spinner: some View { ProgressView() .progressViewStyle(CircularProgressViewStyle()) From 9f28fdc382f7f1ba53aecb3e6f2515f17ac761ec Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:31:12 +0100 Subject: [PATCH 128/468] Removing redundant init --- .../TimelinePoll/TimelinePollModels.swift | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 3ad624f57..0ee87c55f 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -71,33 +71,9 @@ struct TimelinePollDetails { var type: TimelinePollType var eventType: TimelinePollEventType var maxAllowedSelections: UInt - var hasBeenEdited = true + var hasBeenEdited: Bool var hasDecryptionError: Bool - init(id: String, - question: String, - answerOptions: [TimelinePollAnswerOption], - closed: Bool, - startDate: Date, - totalAnswerCount: UInt, - type: TimelinePollType, - eventType: TimelinePollEventType, - maxAllowedSelections: UInt, - hasBeenEdited: Bool, - hasDecryptionError: Bool) { - self.id = id - self.question = question - self.answerOptions = answerOptions - self.closed = closed - self.startDate = startDate - self.totalAnswerCount = totalAnswerCount - self.type = type - self.eventType = eventType - self.maxAllowedSelections = maxAllowedSelections - self.hasBeenEdited = hasBeenEdited - self.hasDecryptionError = hasDecryptionError - } - var hasCurrentUserVoted: Bool { answerOptions.contains(where: \.selected) } From 8bb7cd412f8e2da4f050b58234227245f0c7ec11 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:33:51 +0100 Subject: [PATCH 129/468] Refactor PollKind conversion --- .../Coordinator/TimelinePollCoordinator.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index d8c9f59f8..63ccae763 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -139,17 +139,21 @@ extension TimelinePollDetails { closed: poll.isClosed, startDate: poll.startDate, totalAnswerCount: poll.totalAnswerCount, - type: Self.pollKindToTimelinePollType(poll.kind), + type: poll.kind.timelinePollType, eventType: eventType, maxAllowedSelections: poll.maxAllowedSelections, hasBeenEdited: poll.hasBeenEdited, hasDecryptionError: poll.hasDecryptionError) } - - private static func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { - let mapping = [PollKind.disclosed: TimelinePollType.disclosed, - PollKind.undisclosed: TimelinePollType.undisclosed] - - return mapping[kind] ?? .disclosed +} + +private extension PollKind { + var timelinePollType: TimelinePollType { + switch self { + case .disclosed: + return .disclosed + case .undisclosed: + return .undisclosed + } } } From 85c2cf3323fbb1c98efcb572a9fdbe5300bb0837 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 09:47:50 +0100 Subject: [PATCH 130/468] Add emptyPollsText in the view model --- .../PollHistory/PollHistoryViewModel.swift | 17 ++++++++++++ .../Test/UI/PollHistoryUITests.swift | 2 +- .../Room/PollHistory/View/PollHistory.swift | 26 ++++++------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 620903867..dae1ee197 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -101,3 +101,20 @@ private extension PollHistoryViewModel { state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate }) } } + +extension PollHistoryViewModel.Context { + var emptyPollsText: String { + let days = PollHistoryConstants.chunkSizeInDays + + switch (viewState.bindings.mode, viewState.canLoadMoreContent) { + case (.active, true): + return VectorL10n.pollHistoryNoActivePollPeriodText("\(days)") + case (.active, false): + return VectorL10n.pollHistoryNoActivePollText + case (.past, true): + return VectorL10n.pollHistoryNoPastPollPeriodText("\(days)") + case (.past, false): + return VectorL10n.pollHistoryNoPastPollText + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index bd08d10c2..7b9e8fc3f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -53,7 +53,7 @@ final class PollHistoryUITests: MockScreenTestCase { func testPastPollHistoryIsEmpty() { app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title) let title = app.navigationBars.firstMatch.identifier - let emptyText = app.staticTexts["PollHistory.emptyLoadMoreText"] + let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] let winningOption = app.staticTexts["PollListData.winningOption"] diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 21eec4068..49ce11a5a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -94,30 +94,20 @@ struct PollHistory: View { .progressViewStyle(CircularProgressViewStyle()) } - @ViewBuilder private var noPollsView: some View { - if viewModel.viewState.canLoadMoreContent { - let days = PollHistoryConstants.chunkSizeInDays - - VStack(spacing: 32) { - Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollPeriodText("\(days)") : VectorL10n.pollHistoryNoPastPollPeriodText("\(days)")) - .font(theme.fonts.body) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.center) - .padding(.horizontal, 16) - .accessibilityIdentifier("PollHistory.emptyLoadMoreText") - - loadMoreButton - } - .frame(maxHeight: .infinity) - } else { - Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText) + VStack(spacing: 32) { + Text(viewModel.emptyPollsText) .font(theme.fonts.body) + .multilineTextAlignment(.center) .foregroundColor(theme.colors.secondaryContent) - .frame(maxHeight: .infinity) .padding(.horizontal, 16) .accessibilityIdentifier("PollHistory.emptyText") + + if viewModel.viewState.canLoadMoreContent { + loadMoreButton + } } + .frame(maxHeight: .infinity) } private var loadingView: some View { From c974add57d2cc69dc9b06101812c3306d6bb5173 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 10:42:45 +0100 Subject: [PATCH 131/468] Refactor TimelinePollAnswerOptionButton --- .../Room/PollHistory/View/PollListItem.swift | 48 ++----------------- .../View/TimelinePollAnswerOptionButton.swift | 35 +++++++------- 2 files changed, 22 insertions(+), 61 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 04acaab6e..df7cb3774 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -48,56 +48,17 @@ struct PollListItem: View { if pollData.closed { VStack(alignment: .leading, spacing: 12) { let winningOptions = pollData.answerOptions.filter(\.winner) + ForEach(winningOptions) { - optionView(winningOption: $0) + TimelinePollAnswerOptionButton(poll: pollData, answerOption: $0, action: nil) } + resultView } } } } - private var clipShape: some Shape { - RoundedRectangle(cornerRadius: 4.0) - } - - private func optionView(winningOption: TimelinePollAnswerOption) -> some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(alignment: .top, spacing: 8.0) { - Text(winningOption.text) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - .accessibilityIdentifier("PollListData.winningOption") - - Spacer() - - votesText(winningOption: winningOption) - } - - ProgressView(value: Double(winningOption.count), - total: Double(pollData.totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8.0) - .padding(.top, 12.0) - .padding(.bottom, 12.0) - .clipShape(clipShape) - .overlay(clipShape.stroke(theme.colors.accent, lineWidth: 1.0)) - .accentColor(theme.colors.accent) - } - - private func votesText(winningOption: TimelinePollAnswerOption) -> some View { - Label { - Text(winningOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(winningOption.count))) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.accent) - } icon: { - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - } - private var resultView: some View { let text = pollData.totalAnswerCount == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.totalAnswerCount)) @@ -133,8 +94,7 @@ struct PollListItem_Previews: PreviewProvider { maxAllowedSelections: 1, hasBeenEdited: false, hasDecryptionError: false) - - + let pollData2 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 498fe29f5..6b0765a49 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -25,23 +25,26 @@ struct TimelinePollAnswerOptionButton: View { let poll: TimelinePollDetails let answerOption: TimelinePollAnswerOption - let action: () -> Void + let action: (() -> Void)? // MARK: Public var body: some View { - Button(action: action) { + Button { + action?() + } label: { let rect = RoundedRectangle(cornerRadius: 4.0) answerOptionLabel .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 8.0) .padding(.top, 12.0) - .padding(.bottom, 12.0) + .padding(.bottom, 8.0) .clipShape(rect) .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) .accentColor(progressViewAccentColor) } .accessibilityIdentifier("PollAnswerOption\(optionIndex)") + .disabled(action == nil) } var answerOptionLabel: some View { @@ -60,23 +63,20 @@ struct TimelinePollAnswerOptionButton: View { Spacer() Image(uiImage: Asset.Images.pollWinnerIcon.image) } + + 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") + } } if poll.type == .disclosed || poll.closed { - HStack { - ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), - total: Double(poll.totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") - - 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") - } - } + ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), total: Double(poll.totalAnswerCount)) + .progressViewStyle(LinearProgressViewStyle.linear) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress") } } } @@ -143,6 +143,7 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { } } } + .padding() } static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { From c5f649a530b0df8e7e4efdbfea8068fd513c39b9 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 10:46:38 +0100 Subject: [PATCH 132/468] Refactor next() -> nextBatch() --- .../Room/PollHistory/MockPollHistoryScreenState.swift | 6 +++--- .../Room/PollHistory/PollHistoryViewModel.swift | 2 +- .../Service/MatrixSDK/PollHistoryService.swift | 2 +- .../Service/Mock/MockPollHistoryService.swift | 6 +++--- .../Service/PollHistoryServiceProtocol.swift | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 4fc9f6cef..d939dab2f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -47,17 +47,17 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollHistoryMode = .past case .activeEmpty: pollHistoryMode = .active - pollService.nextPublisher = Empty(completeImmediately: true, + pollService.nextBatchPublisher = Empty(completeImmediately: true, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() case .pastEmpty: pollHistoryMode = .past - pollService.nextPublisher = Empty(completeImmediately: true, + pollService.nextBatchPublisher = Empty(completeImmediately: true, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() case .loading: pollHistoryMode = .active - pollService.nextPublisher = Empty(completeImmediately: false, + pollService.nextBatchPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index dae1ee197..eed1e6c6c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -49,7 +49,7 @@ private extension PollHistoryViewModel { state.isLoading = true pollService - .next() + .nextBatch() .collect() .sink { [weak self] _ in #warning("Handle errors") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 3afd48887..a57731cdd 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -48,7 +48,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { setup(timeline: timeline) } - func next() -> AnyPublisher { + func nextBatch() -> AnyPublisher { currentBatchSubject?.eraseToAnyPublisher() ?? startPagination() } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 9cfa8da3b..acd9543e3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -27,13 +27,13 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { pollErrorPublisher } - lazy var nextPublisher: AnyPublisher = (activePollsData + pastPollsData) + lazy var nextBatchPublisher: AnyPublisher = (activePollsData + pastPollsData) .publisher .setFailureType(to: Error.self) .eraseToAnyPublisher() - func next() -> AnyPublisher { - nextPublisher + func nextBatch() -> AnyPublisher { + nextBatchPublisher } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 3458c3d64..637ba393f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,13 +18,13 @@ import Combine protocol PollHistoryServiceProtocol { /// Returns a Publisher publishing the polls in the next batch. - /// Implementations should return the same publisher if `next()` is called again before the previous publisher completes. - func next() -> AnyPublisher + /// Implementations should return the same publisher if `nextBatch()` is called again before the previous publisher completes. + func nextBatch() -> AnyPublisher - /// Publishes updates for the polls previously pusblished by the `next()` publishers. + /// Publishes updates for the polls previously pusblished by the `nextBatch()` publishers. var updates: AnyPublisher { get } /// Publishes errors regarding poll aggregations. - /// Note: `next()` will continue to publish new polls even if some poll isn't being aggregated correctly. + /// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly. var pollErrors: AnyPublisher { get } } From ed7c85e3dc1c13e9d4ef63f208af8deacb3908ac Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 10 Jan 2023 14:21:37 +0000 Subject: [PATCH 133/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/hu/ --- Riot/Assets/hu.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/hu.lproj/Localizable.strings b/Riot/Assets/hu.lproj/Localizable.strings index 82161237e..fee155510 100644 --- a/Riot/Assets/hu.lproj/Localizable.strings +++ b/Riot/Assets/hu.lproj/Localizable.strings @@ -120,3 +120,6 @@ /* New video message from a specific person, not referencing a room. */ "VIDEO_FROM_USER" = "%@ videót küldött"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ hang közvetítést indított"; From c44ed6d698207a6ddadd874b30db692852ce79e6 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 10 Jan 2023 18:00:06 +0000 Subject: [PATCH 134/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/uk/ --- Riot/Assets/uk.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/uk.lproj/Localizable.strings b/Riot/Assets/uk.lproj/Localizable.strings index 276df4c3e..90e0de28e 100644 --- a/Riot/Assets/uk.lproj/Localizable.strings +++ b/Riot/Assets/uk.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ надсилає дані про своє місцеперебування"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ розпочинає голосову трансляцію"; From 255fd67f151605660dff5b7c5f99d27df8643c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 10 Jan 2023 18:37:04 +0000 Subject: [PATCH 135/468] Translated using Weblate (Estonian) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/et/ --- Riot/Assets/et.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/et.lproj/Localizable.strings b/Riot/Assets/et.lproj/Localizable.strings index 9f1002800..b3a015217 100644 --- a/Riot/Assets/et.lproj/Localizable.strings +++ b/Riot/Assets/et.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ jagas oma asukohta"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ alustas ringhäälingukõnet"; From 46e1140791102f8a83a690407d630f12810922c2 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 10 Jan 2023 17:16:49 +0000 Subject: [PATCH 136/468] Translated using Weblate (Slovak) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/sk/ --- Riot/Assets/sk.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sk.lproj/Localizable.strings b/Riot/Assets/sk.lproj/Localizable.strings index 612c4f691..02c086065 100644 --- a/Riot/Assets/sk.lproj/Localizable.strings +++ b/Riot/Assets/sk.lproj/Localizable.strings @@ -168,3 +168,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ zdieľal/a svoju polohu"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ začal/a hlasové vysielanie"; From 32c553f91174ef8736cb4ca635012ce9c0a0209c Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Thu, 12 Jan 2023 19:47:36 +0000 Subject: [PATCH 137/468] Translated using Weblate (Swedish) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/sv/ --- Riot/Assets/sv.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sv.lproj/Localizable.strings b/Riot/Assets/sv.lproj/Localizable.strings index 5b3e918dd..fc7c18b9e 100644 --- a/Riot/Assets/sv.lproj/Localizable.strings +++ b/Riot/Assets/sv.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ delade sin plats"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ påbörjade en röstsändning"; From 2dc4385dd7f4b32f9701c7b577b536151f941778 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 10 Jan 2023 14:22:00 +0000 Subject: [PATCH 138/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2352 of 2352 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 53debbedb..654254007 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2688,3 +2688,4 @@ "poll_timeline_decryption_error" = "Visszafejtési hibák miatt néhány szavazat nem kerül beszámításra"; "voice_message_broadcast_in_progress_message" = "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához"; "voice_message_broadcast_in_progress_title" = "Hang üzenetet nem lehet elindítani"; +"poll_timeline_ended_text" = "Szavazás vége"; From 597dffa3cc3de26329709907980c24dbc9f78187 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 13 Jan 2023 14:42:20 +0000 Subject: [PATCH 139/468] Translated using Weblate (Italian) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/it/ --- Riot/Assets/it.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/it.lproj/Localizable.strings b/Riot/Assets/it.lproj/Localizable.strings index bb8b3e707..3232ff2a4 100644 --- a/Riot/Assets/it.lproj/Localizable.strings +++ b/Riot/Assets/it.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ ha condiviso la sua posizione"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ ha iniziato una trasmissione vocale"; From 039ab2c835f8d01bec00f5be58db176e10ed83fa Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 10 Jan 2023 13:55:29 +0000 Subject: [PATCH 140/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2352 of 2352 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index d6449680c..821491fb2 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2895,3 +2895,4 @@ "poll_timeline_decryption_error" = "Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung"; "voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara selagi Anda merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara"; "voice_message_broadcast_in_progress_title" = "Tidak dapat memulai pesan suara"; +"poll_timeline_ended_text" = "Mengakhiri pemungutan suara"; From 977d0e38f1ecfac5064510607f3474947f511478 Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Fri, 20 Jan 2023 00:27:49 +0000 Subject: [PATCH 141/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/zh_Hans.lproj/Localizable.strings b/Riot/Assets/zh_Hans.lproj/Localizable.strings index 1e13dc6f6..2b4f707e2 100644 --- a/Riot/Assets/zh_Hans.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hans.lproj/Localizable.strings @@ -123,3 +123,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ 分享了他们的位置"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@开始语音广播"; From dc1a4451b1aa1bbd08c893fd634e8972804678e1 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 10 Jan 2023 15:08:34 +0000 Subject: [PATCH 142/468] Translated using Weblate (German) Currently translated at 99.9% (2353 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index f1886a04c..b4b799114 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2701,4 +2701,7 @@ "user_other_session_security_recommendation_title" = "Andere Sitzungen"; "voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen"; "poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden"; -"voice_message_broadcast_in_progress_message" = "Du kannst kein Gespräch beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; +"voice_message_broadcast_in_progress_message" = "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; +"poll_timeline_ended_text" = "Abstimmung beendet"; +"voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen."; +"voice_broadcast_voip_cannot_start_title" = "Kann keinen Anruf beginnen"; From 728f5d98b6f85e9049144d1d9c994a92f1b5d3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Mon, 23 Jan 2023 10:17:07 +0000 Subject: [PATCH 143/468] Translated using Weblate (Icelandic) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/is/ --- Riot/Assets/is.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/is.lproj/Localizable.strings b/Riot/Assets/is.lproj/Localizable.strings index ec3fdaace..d47171514 100644 --- a/Riot/Assets/is.lproj/Localizable.strings +++ b/Riot/Assets/is.lproj/Localizable.strings @@ -170,3 +170,6 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ ný skilaboð í %@, %@ og fleirum"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ byrjaði talútsendingu"; From 3a328c2b792e73a98bf559e559d7b803a4666743 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 11 Jan 2023 10:03:43 +0000 Subject: [PATCH 144/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 654254007..4c50ec296 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2689,3 +2689,5 @@ "voice_message_broadcast_in_progress_message" = "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához"; "voice_message_broadcast_in_progress_title" = "Hang üzenetet nem lehet elindítani"; "poll_timeline_ended_text" = "Szavazás vége"; +"voice_broadcast_voip_cannot_start_description" = "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához."; +"voice_broadcast_voip_cannot_start_title" = "Nem sikerült hívást indítani"; From 752d7ae83dd50c1560edae149a2a3a27375f5b82 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 10 Jan 2023 18:08:55 +0000 Subject: [PATCH 145/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 96e8166d7..63c432c08 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2893,3 +2893,6 @@ "poll_timeline_decryption_error" = "Через помилки під час розшифрування деякі голоси можуть бути не враховані"; "voice_message_broadcast_in_progress_title" = "Неможливо розпочати запис голосового повідомлення"; "voice_message_broadcast_in_progress_message" = "Ви не можете розпочати запис голосового повідомлення, оскільки зараз триває запис трансляції наживо. Будь ласка, завершіть трансляцію, щоб розпочати запис голосового повідомлення"; +"poll_timeline_ended_text" = "Опитування завершено"; +"voice_broadcast_voip_cannot_start_description" = "Ви не можете розпочати виклик, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати виклик."; +"voice_broadcast_voip_cannot_start_title" = "Неможливо розпочати виклик"; From 4450ba55d01fc3e82cd4d8df862022c9e1ef5980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 10 Jan 2023 18:35:49 +0000 Subject: [PATCH 146/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index d84b26c76..80fee8f72 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2640,3 +2640,6 @@ "poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata"; "voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu"; "voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne"; +"poll_timeline_ended_text" = "Küsitlus on lõppenud"; +"voice_broadcast_voip_cannot_start_description" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne."; +"voice_broadcast_voip_cannot_start_title" = "Kõne algatamine ei õnnestu"; From d81e0522cfc97439eb5b83bb1f4471ef519b33fb Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 10 Jan 2023 23:40:59 +0000 Subject: [PATCH 147/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 821491fb2..bbc02ec1a 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2893,6 +2893,8 @@ "notice_voice_broadcast_live" = "Siaran langsung"; "user_other_session_security_recommendation_title" = "Sesi lainnya"; "poll_timeline_decryption_error" = "Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung"; -"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara selagi Anda merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara"; +"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara"; "voice_message_broadcast_in_progress_title" = "Tidak dapat memulai pesan suara"; "poll_timeline_ended_text" = "Mengakhiri pemungutan suara"; +"voice_broadcast_voip_cannot_start_description" = "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan."; +"voice_broadcast_voip_cannot_start_title" = "Tidak dapat memulai sebuah panggilan"; From d2454a1e11062d21f75be30248c07e454fa85590 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 10 Jan 2023 17:18:13 +0000 Subject: [PATCH 148/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 87edea36c..c3c8a3c10 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2891,3 +2891,6 @@ "poll_timeline_decryption_error" = "Z dôvodu chýb v dešifrovaní sa niektoré hlasy nemusia započítať"; "voice_message_broadcast_in_progress_message" = "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu"; "voice_message_broadcast_in_progress_title" = "Nemožno spustiť hlasovú správu"; +"poll_timeline_ended_text" = "Ukončil anketu"; +"voice_broadcast_voip_cannot_start_description" = "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor."; +"voice_broadcast_voip_cannot_start_title" = "Nie je možné začať hovor"; From 54cd660b610bc241a25bb5d5c84de0906480fc0d Mon Sep 17 00:00:00 2001 From: RS-Nocsi <13570286865@163.com> Date: Thu, 12 Jan 2023 05:25:03 +0000 Subject: [PATCH 149/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 82.4% (1940 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index d7a775d4b..bcd721356 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -2231,3 +2231,44 @@ "onboarding_congratulations_home_button" = "带我到主页"; "onboarding_use_case_message" = "我们将帮助你连接"; "invite_to" = "邀请到%@"; +"threads_empty_title" = "保持讨论的有条理性"; +"threads_action_my_threads" = "我的线程"; +"threads_action_all_threads" = "所有线程"; +"threads_title" = "线程"; +"thread_copy_link_to_thread" = "将链接复制到线程"; + +// MARK: Threads +"room_thread_title" = "线程"; +"room_accessibility_record_voice_message_hint" = "双击并保持录音。"; +"room_accessibility_record_voice_message" = "录制语音消息"; +"room_accessibility_thread_more" = "更多"; +"room_accessibility_threads" = "线程"; +"room_event_copy_link_info" = "链接复制到剪贴板。"; +"room_event_action_reply_in_thread" = "线程"; +"room_event_action_view_in_room" = "在房间浏览"; +"room_first_message_placeholder" = "发送您的第一条消息……"; +"room_participants_invite_prompt_to_msg" = "您确定要邀请%@ 到 %@吗?"; +"room_participants_leave_success" = "离开房间"; +"room_participants_leave_processing" = "离开"; +"search_filter_placeholder" = "过滤"; +"password_policy_pwd_in_dict_error" = "此密码已在字典中找到,不允许使用。"; +"password_policy_weak_pwd_error" = "此密码太弱了。它必须包含至少8个字符,每种类型至少有一个字符: 大写、小写、数字和特殊字符。"; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "密码过短"; +"authentication_qr_login_failure_retry" = "再试一次"; +"authentication_qr_login_failure_request_timed_out" = "连接没有在规定的时间内完成。"; +"authentication_qr_login_failure_request_denied" = "请求在另一个设备上被拒绝。"; +"authentication_qr_login_failure_invalid_qr" = "二维码无效。"; +"authentication_qr_login_failure_title" = "连接失败"; +"authentication_qr_login_loading_signed_in" = "您现在已经登录到另一个设备上。"; +"authentication_qr_login_loading_waiting_signin" = "等待设备登录。"; +"authentication_qr_login_loading_connecting_device" = "连接到设备"; +"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您帐户的完全访问权限。"; +"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:"; +"authentication_qr_login_confirm_title" = "建立安全连接"; +"authentication_qr_login_scan_subtitle" = "将二维码放置在下面的方框中"; +"authentication_qr_login_scan_title" = "扫描二维码"; +"authentication_qr_login_display_step2" = "选择“以二维码登入”"; +"authentication_qr_login_display_step1" = "在您的其它设备中打开Element"; +"onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。"; From 6dcd249d02f3ace4be2a90615824e26efd281a30 Mon Sep 17 00:00:00 2001 From: Vri Date: Thu, 12 Jan 2023 10:50:24 +0000 Subject: [PATCH 150/468] Translated using Weblate (German) Currently translated at 99.9% (2353 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index b4b799114..ccab1a9ca 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2702,6 +2702,6 @@ "voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen"; "poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden"; "voice_message_broadcast_in_progress_message" = "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; -"poll_timeline_ended_text" = "Abstimmung beendet"; +"poll_timeline_ended_text" = "Umfrage beendet"; "voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen."; "voice_broadcast_voip_cannot_start_title" = "Kann keinen Anruf beginnen"; From 68abe6d5da134cc8120dc8ddaa500c3cf12b2c74 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 13 Jan 2023 14:41:47 +0000 Subject: [PATCH 151/468] Translated using Weblate (Italian) Currently translated at 100.0% (2354 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 35bd94c35..4a649f1c4 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2665,3 +2665,9 @@ "notice_voice_broadcast_live" = "Trasmissione in diretta"; "wysiwyg_composer_format_action_inline_code" = "Applica formato codice interlinea"; "user_other_session_security_recommendation_title" = "Altre sessioni"; +"poll_timeline_ended_text" = "Sondaggio terminato"; +"poll_timeline_decryption_error" = "A causa di errori di decifrazione, alcuni voti potrebbero non venire contati"; +"voice_broadcast_voip_cannot_start_description" = "Non puoi avviare una chiamata perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare una chiamata."; +"voice_broadcast_voip_cannot_start_title" = "Impossibile avviare una chiamata"; +"voice_message_broadcast_in_progress_title" = "Impossibile iniziare il messaggio vocale"; +"voice_message_broadcast_in_progress_message" = "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale"; From 62ca94f5e0f4728b786397c2b40d1bc812f523d1 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Fri, 13 Jan 2023 21:39:44 +0000 Subject: [PATCH 152/468] Translated using Weblate (Swedish) Currently translated at 93.9% (2212 of 2354 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 111 +++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 53b8d822f..872645f66 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2310,7 +2310,7 @@ "authentication_terms_policy_url_error" = "Kan inte hitta den valda policyn. Vänligen pröva igen senare."; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "Vänligen läs villkor och policyer för %@"; -"authentication_terms_title" = "Serverpolicyer"; +"authentication_terms_title" = "Sekretesspolicyer"; "authentication_verify_msisdn_invalid_phone_number" = "Ogiltigt telefonnummer"; "authentication_verify_msisdn_waiting_button" = "Skicka kod igen"; /* The placeholder will show the phone number that was entered. */ @@ -2363,3 +2363,112 @@ // MARK: Authentication "authentication_registration_title" = "Skapa ditt konto"; +"voice_broadcast_time_left" = "%@ kvar"; +"all_chats_empty_list_placeholder_title" = "Du är ikapp."; +"all_chats_empty_view_information" = "Den säkra allt-i-ett-chattappen för lag, vänner och organisationer. Skapa en chatt, eller gå med i ett existerande rum, för att komma igång."; +"all_chats_empty_space_information" = "Utrymmen är ett nytt sätt att gruppera rum och personer. Lägg till ett existerande rum, eller skapa ett nytt, med knappen nere till höger."; +"all_chats_empty_view_title" = "%@\nser lite tom ut."; +"all_chats_all_filter" = "Alla"; +"all_chats_edit_layout_alphabetical_order" = "Sortera A-Ö"; +"all_chats_edit_layout_activity_order" = "Sortera efter aktivitet"; +"all_chats_edit_layout_show_filters" = "Visa filter"; +"all_chats_edit_layout_show_recents" = "Visa nyliga"; +"all_chats_edit_layout_sorting_options_title" = "Sortera meddelanden efter"; +"all_chats_edit_layout_pin_spaces_title" = "Fäst dina utrymmen"; +"all_chats_edit_layout_add_filters_message" = "Filtrera automatiskt dina meddelanden i valfria kategorier"; +"all_chats_edit_layout_add_filters_title" = "Filtrera dina meddelanden"; +"all_chats_edit_layout_add_section_message" = "Fäst sektioner till hem för enkel åtkomst"; +"all_chats_edit_layout_add_section_title" = "Lägg till sektion i hem"; +"all_chats_edit_layout_unreads" = "Olästa"; +"all_chats_edit_layout_recents" = "Nyliga"; +"all_chats_edit_layout" = "Layoutalternativ"; +"all_chats_section_title" = "Chattar"; + +// MARK: - All Chats + +"all_chats_title" = "Alla chattar"; +"voice_broadcast_voip_cannot_start_description" = "Du kan inte starta ett samtal eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta ett samtal."; +"voice_broadcast_voip_cannot_start_title" = "Kan inte starta ett samtal"; +"voice_broadcast_stop_alert_agree_button" = "Ja, avsluta"; +"voice_broadcast_stop_alert_description" = "Är du säker på att du vill avsluta din direktsändning? Det här kommer att avsluta sändningen, och den fulla inspelningen kommer att bli tillgänglig i rummet."; +"voice_broadcast_stop_alert_title" = "Avsluta direktsändning?"; +"voice_broadcast_buffering" = "Buffrar…"; +"voice_broadcast_tile" = "Röstsändning"; +"voice_broadcast_live" = "Live"; +"voice_broadcast_playback_loading_error" = "Kunde inte spela den här röstsändningen."; +"voice_broadcast_already_in_progress_message" = "Du spelar redan in en röstsändning. Vänligen avsluta din nuvarande röstsändning för att starta en ny."; +"voice_broadcast_blocked_by_someone_else_message" = "Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning avslutas för att starta en ny."; +"voice_broadcast_permission_denied_message" = "Du har inte behörigheten som krävs för att starta en röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera din behörighet."; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "Du kan inte starta en ny röstsändning"; +"voice_message_broadcast_in_progress_message" = "Du kan inte starta ett röstmeddelande eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att börja spela in ett röstmeddelande"; +"voice_message_broadcast_in_progress_title" = "Kan inte starta röstmeddelande"; +"spaces_subspace_creation_visibility_message" = "Det skapade utrymmet kommer att läggas till i %@."; +"spaces_subspace_creation_visibility_title" = "Vad för sorts utrymme vill du skapa?"; +"spaces_explore_rooms_format" = "Utforska %@"; +"spaces_create_subspace_title" = "Skapa ett underutrymme"; +"spaces_add_subspace_title" = "Skapa utrymme inuti %@"; +"launch_loading_processing_response" = "Hanterar data\n%@ %%"; +"launch_loading_server_syncing_nth_attempt" = "Synkar med servern\n(%@ försök)"; + +// MARK: - Launch loading + +"launch_loading_server_syncing" = "Synkar med servern"; +"key_verification_alert_body" = "Granska för att försäkra att ditt konto är säkert."; + +// Unverified sessions +"key_verification_alert_title" = "Du har overifierade sessioner"; +"sign_out_confirmation_message" = "Är du säker på att du vill logga ut?"; + +// MARK: Sign out warning + +"sign_out" = "Logga ut"; +// User sessions management +"user_sessions_settings" = "Hantera sessioner"; +"manage_session_sign_out_other_sessions" = "Logga ut ur alla andra sessioner"; +"manage_session_rename" = "Döp om session"; +"manage_session_name_info_link" = "Läs mer"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Observera att sessionsnamn också är synliga för personer du pratar med. %@"; +"manage_session_name_hint" = "Anpassade sessionsnamn kan hjälpa dig att känna igen dina enheter lättare."; +"settings_labs_enable_voice_broadcast" = "Röstsändning"; +"settings_labs_enable_wysiwyg_composer" = "Pröva den nya riktextredigeraren"; +"settings_labs_enable_new_app_layout" = "Ny applikationslayout"; +"settings_labs_enable_new_client_info_feature" = "Spara klientens namn, version och URL för att lättare känna igen sessioner i sessionshanteraren"; +"settings_labs_enable_new_session_manager" = "My sessionshanterare"; +"room_first_message_placeholder" = "Skicka ditt första meddelande…"; +"password_policy_pwd_in_dict_error" = "Det här lösenordet har hittats i en ordlista, och tillåts inte."; +"password_policy_weak_pwd_error" = "Det här lösenordet är för svagt. Det måste innehålla minst 8 tecken, och minst ett tecken av varje typ: stor bokstav, liten bokstav, siffra och specialtecken."; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "För kort lösenord"; +"authentication_qr_login_failure_retry" = "Pröva igen"; +"authentication_qr_login_failure_request_timed_out" = "Länkningen slutfördes inte inom den krävda tiden."; +"authentication_qr_login_failure_request_denied" = "Förfrågan nekades på en andra enheten."; +"authentication_qr_login_failure_invalid_qr" = "QR-kod är ogiltig."; +"authentication_qr_login_failure_title" = "Länkning misslyckades"; +"authentication_qr_login_loading_signed_in" = "Du är nu inloggad på din andra enhet."; +"authentication_qr_login_loading_waiting_signin" = "Väntar på att enheten loggar in."; +"authentication_qr_login_loading_connecting_device" = "Ansluter till enhet"; +"authentication_qr_login_confirm_alert" = "Vänligen försäkra att du känner till källan till den här koden. Genom att länka enheter så ger du någon full åtkomst till ditt konto."; +"authentication_qr_login_confirm_subtitle" = "Bekräfta att koden nedan matchar den andra enheten:"; +"authentication_qr_login_confirm_title" = "Säker kommunikation etablerad"; +"authentication_qr_login_scan_subtitle" = "Placera QR-koden i rutan nedan"; +"authentication_qr_login_scan_title" = "Skanna QR-kod"; +"authentication_qr_login_display_step2" = "Välj 'Logga in med QR-kod'"; +"authentication_qr_login_display_step1" = "Öppna Element på din andra enhet"; +"authentication_qr_login_display_subtitle" = "Skanna QR-koden nedan med din enhet som är utloggad."; +"authentication_qr_login_display_title" = "Länka en enhet"; +"authentication_qr_login_start_display_qr" = "Visa QR-kod på den här enheten"; +"authentication_qr_login_start_need_alternative" = "Behöver du en alternativ metod?"; +"authentication_qr_login_start_step4" = "Välj 'Visa QR-kod på den här enheten'"; +"authentication_qr_login_start_step3" = "Välj 'Länka en enhet'"; +"authentication_qr_login_start_step2" = "Gå till Inställningar -> Säkerhet & sekretess"; +"authentication_qr_login_start_step1" = "Öppna Element på den andra enheten"; +"authentication_qr_login_start_subtitle" = "Använd kameran på den här enheten för att skanna QR-koden som visas på den andra enheten:"; +"authentication_qr_login_start_title" = "Skanna QR-kod"; +"authentication_choose_password_not_verified_message" = "Kolla din inkorg"; +"authentication_choose_password_not_verified_title" = "E-post inte verifierad"; +"authentication_login_with_qr" = "Logga in med QR-kod"; +"invite_to" = "Bjud in till %@"; From b8658d9a18c49798b998422eee11acd90987ec82 Mon Sep 17 00:00:00 2001 From: Vri Date: Mon, 16 Jan 2023 13:11:34 +0000 Subject: [PATCH 153/468] Translated using Weblate (German) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index ccab1a9ca..35c0383c1 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2705,3 +2705,13 @@ "poll_timeline_ended_text" = "Umfrage beendet"; "voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen."; "voice_broadcast_voip_cannot_start_title" = "Kann keinen Anruf beginnen"; +"poll_history_no_past_poll_text" = "In diesem Raum gibt es keine abgeschlossenen Umfragen"; +"poll_history_no_active_poll_text" = "In diesem Raum gibt es keine aktiven Umfragen"; +"poll_history_past_segment_title" = "Vergangene Umfragen"; +"poll_history_active_segment_title" = "Aktive Umfragen"; + +// MARK: - Polls history + +"poll_history_title" = "Umfrageverlauf"; +"room_details_polls" = "Umfrageverlauf"; +"accessibility_selected" = "ausgewählt"; From 14966d6ef74fa927ee5d9a5f92903b24527dbdbb Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 16 Jan 2023 12:35:16 +0000 Subject: [PATCH 154/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 63c432c08..7bf2ff0fa 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2896,3 +2896,13 @@ "poll_timeline_ended_text" = "Опитування завершено"; "voice_broadcast_voip_cannot_start_description" = "Ви не можете розпочати виклик, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати виклик."; "voice_broadcast_voip_cannot_start_title" = "Неможливо розпочати виклик"; +"poll_history_no_past_poll_text" = "У цій кімнаті немає минулих опитувань"; +"poll_history_no_active_poll_text" = "У цій кімнаті немає активних опитувань"; +"poll_history_past_segment_title" = "Минулі опитування"; +"poll_history_active_segment_title" = "Активні опитування"; + +// MARK: - Polls history + +"poll_history_title" = "Історія опитувань"; +"room_details_polls" = "Історія опитувань"; +"accessibility_selected" = "вибрано"; From 7e304db347c8fa442b5ab0c5968b27497640d8d1 Mon Sep 17 00:00:00 2001 From: Linerly Date: Mon, 16 Jan 2023 12:14:00 +0000 Subject: [PATCH 155/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index bbc02ec1a..f666917c3 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2898,3 +2898,13 @@ "poll_timeline_ended_text" = "Mengakhiri pemungutan suara"; "voice_broadcast_voip_cannot_start_description" = "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan."; "voice_broadcast_voip_cannot_start_title" = "Tidak dapat memulai sebuah panggilan"; +"poll_history_no_past_poll_text" = "Tidak ada pemungutan suara masa lalu di ruangan ini"; +"poll_history_no_active_poll_text" = "Tidak ada pemungutan suara yang aktifk di ruangan ini"; +"poll_history_past_segment_title" = "Pemungutan suara sebelumnya"; +"poll_history_active_segment_title" = "Pemungutan suara aktif"; + +// MARK: - Polls history + +"poll_history_title" = "Riwayat pemungutan suara"; +"room_details_polls" = "Riwayat pemungutan suara"; +"accessibility_selected" = "dipilih"; From 421d540c0271ef43fa8cf869f801a7162d3c330d Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 16 Jan 2023 14:25:50 +0000 Subject: [PATCH 156/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index c3c8a3c10..e41769142 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2894,3 +2894,13 @@ "poll_timeline_ended_text" = "Ukončil anketu"; "voice_broadcast_voip_cannot_start_description" = "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor."; "voice_broadcast_voip_cannot_start_title" = "Nie je možné začať hovor"; +"poll_history_no_past_poll_text" = "V tejto miestnosti nie sú žiadne predchádzajúce ankety"; +"poll_history_no_active_poll_text" = "V tejto miestnosti nie sú žiadne aktívne ankety"; +"poll_history_past_segment_title" = "Predchádzajúce ankety"; +"poll_history_active_segment_title" = "Aktívne ankety"; + +// MARK: - Polls history + +"poll_history_title" = "História ankety"; +"room_details_polls" = "História ankety"; +"accessibility_selected" = "vybrané"; From 6d82ef96da5c64b24e7292aa55f0e94f53e97d9b Mon Sep 17 00:00:00 2001 From: Vri Date: Mon, 16 Jan 2023 20:26:27 +0000 Subject: [PATCH 157/468] Translated using Weblate (German) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 35c0383c1..65e18d639 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2700,7 +2700,7 @@ "notice_voice_broadcast_live" = "Echtzeitübertragung"; "user_other_session_security_recommendation_title" = "Andere Sitzungen"; "voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen"; -"poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden"; +"poll_timeline_decryption_error" = "Evtl. werden infolge von Entschlüsselungsfehlern einige Stimmen nicht gezählt"; "voice_message_broadcast_in_progress_message" = "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen"; "poll_timeline_ended_text" = "Umfrage beendet"; "voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen."; From e2b06efb9ad08adf0d83492b1616557e4085ac8f Mon Sep 17 00:00:00 2001 From: Szimszon Date: Mon, 16 Jan 2023 19:14:13 +0000 Subject: [PATCH 158/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 4c50ec296..80dbe5b3c 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2691,3 +2691,13 @@ "poll_timeline_ended_text" = "Szavazás vége"; "voice_broadcast_voip_cannot_start_description" = "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához."; "voice_broadcast_voip_cannot_start_title" = "Nem sikerült hívást indítani"; +"poll_history_no_past_poll_text" = "Nincsenek régi szavazások ebben a szobában"; +"poll_history_no_active_poll_text" = "Nincsenek aktív szavazások ebben a szobában"; +"poll_history_past_segment_title" = "Régi szavazások"; +"poll_history_active_segment_title" = "Aktív szavazások"; + +// MARK: - Polls history + +"poll_history_title" = "Szavazás alakulása"; +"room_details_polls" = "Szavazás alakulása"; +"accessibility_selected" = "kiválasztva"; From 4d2528934fd5a9c2e99f6768a07fc7a43b1128fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Mon, 16 Jan 2023 21:43:49 +0000 Subject: [PATCH 159/468] Translated using Weblate (Icelandic) Currently translated at 81.2% (1918 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/is/ --- Riot/Assets/is.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index aba02e2d6..b569fd281 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -1283,7 +1283,7 @@ "settings_add_3pid_invalid_password_message" = "Ógild auðkenni"; "settings_add_3pid_password_title_msidsn" = "Bæta við símanúmeri"; "settings_add_3pid_password_title_email" = "Bæta við tölvupóstfangi"; -"settings_labs_enable_threads" = "Skilaboð í spjallþráðum"; +"settings_labs_enable_threads" = "Spjallþræðir skilaboða"; "settings_labs_enabled_polls" = "Kannanir"; "settings_integrations_allow_button" = "Sýsla með samþættingar"; "settings_new_keyword" = "Bæta við nýju stikkorði"; @@ -2317,7 +2317,7 @@ "device_name_mobile" = "%@ fyrir farsíma"; "device_name_web" = "%@ á vefnum"; "device_name_desktop" = "%@ fyrir einkatölvur"; -"user_session_item_details" = "%@ · Síðasta virkni %@"; +"user_session_item_details" = "%1$@ · %2$@"; "location_sharing_live_loading" = "Hleð inn rauntímastaðsetningu..."; "location_sharing_live_list_item_time_left" = "%@ fór"; "location_sharing_map_credits_title" = "© Höfundarréttur"; From bbd6d831c8b095d719e50b01f78d51f07058337e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 17 Jan 2023 06:56:18 +0000 Subject: [PATCH 160/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2361 of 2361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 80fee8f72..abe430b8f 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2643,3 +2643,13 @@ "poll_timeline_ended_text" = "Küsitlus on lõppenud"; "voice_broadcast_voip_cannot_start_description" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne."; "voice_broadcast_voip_cannot_start_title" = "Kõne algatamine ei õnnestu"; +"poll_history_no_past_poll_text" = "Selles jututoas pole varasemaid küsitlusi"; +"poll_history_no_active_poll_text" = "Selles jututoas pole käimasolevaid küsitlusi"; +"poll_history_past_segment_title" = "Varasemad küsitlused"; +"poll_history_active_segment_title" = "Käimasolevad küsitlused"; + +// MARK: - Polls history + +"poll_history_title" = "Küsitluste ajalugu"; +"room_details_polls" = "Küsitluste ajalugu"; +"accessibility_selected" = "valitud"; From 067235028c6739050092a6415c4c011391abe9ef Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 17 Jan 2023 10:00:51 +0000 Subject: [PATCH 161/468] Translated using Weblate (German) Currently translated at 100.0% (2362 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 65e18d639..3e96b83e0 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2715,3 +2715,4 @@ "poll_history_title" = "Umfrageverlauf"; "room_details_polls" = "Umfrageverlauf"; "accessibility_selected" = "ausgewählt"; +"voice_broadcast_playback_lock_screen_placeholder" = "Sprachübertragung"; From 27c6eb88a8ddc5e4c79f65e22c57414eb311920d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 17 Jan 2023 10:40:51 +0000 Subject: [PATCH 162/468] Translated using Weblate (Albanian) Currently translated at 99.5% (2351 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index f484f7275..b2a297d07 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2676,3 +2676,14 @@ "notice_voice_broadcast_ended" = "%@ përfundoi një transmetim zanor."; "notice_voice_broadcast_live" = "Transmetim i drejtëpërdrejtë"; "user_other_session_security_recommendation_title" = "Sesione të tjerë"; +"poll_timeline_ended_text" = "Përfundoi pyetësori"; +"poll_timeline_decryption_error" = "Për shkak gabimesh shfshehtëzimi, mund të mos jenë numëruar disa vota"; +"poll_history_no_past_poll_text" = "Në këtë dhomë s’ka pyetësorë të dikurshëm"; +"poll_history_no_active_poll_text" = "Në këtë dhomë s’ka pyetësorë aktivë"; +"poll_history_past_segment_title" = "Pyetësorë të dikurshëm"; +"poll_history_active_segment_title" = "Pyetësorë aktivë"; +"voice_broadcast_playback_lock_screen_placeholder" = "Transmetim zanor"; +"voice_broadcast_voip_cannot_start_description" = "S’mund të niset thirrje, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni një thirrje."; +"voice_broadcast_voip_cannot_start_title" = "S’niset dot një thirrje"; +"voice_message_broadcast_in_progress_message" = "S’mund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor"; +"voice_message_broadcast_in_progress_title" = "S’niset dot mesazh zanor"; From c73a200ae1a15005a8db6811adbafd12d4f79ac5 Mon Sep 17 00:00:00 2001 From: random Date: Tue, 17 Jan 2023 09:58:16 +0000 Subject: [PATCH 163/468] Translated using Weblate (Italian) Currently translated at 100.0% (2362 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 4a649f1c4..b5e7ba84f 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2671,3 +2671,14 @@ "voice_broadcast_voip_cannot_start_title" = "Impossibile avviare una chiamata"; "voice_message_broadcast_in_progress_title" = "Impossibile iniziare il messaggio vocale"; "voice_message_broadcast_in_progress_message" = "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale"; +"poll_history_no_past_poll_text" = "In questa stanza non ci sono sondaggi passati"; +"poll_history_no_active_poll_text" = "In questa stanza non ci sono sondaggi attivi"; +"poll_history_past_segment_title" = "Sondaggi passati"; +"poll_history_active_segment_title" = "Sondaggi attivi"; + +// MARK: - Polls history + +"poll_history_title" = "Cronologia sondaggi"; +"voice_broadcast_playback_lock_screen_placeholder" = "Trasmissione vocale"; +"room_details_polls" = "Cronologia sondaggi"; +"accessibility_selected" = "selezionato"; From a73bf3661d0b136479d59550783924b103555eee Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 17 Jan 2023 14:16:47 +0000 Subject: [PATCH 164/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2362 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 7bf2ff0fa..92f0606a2 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2906,3 +2906,4 @@ "poll_history_title" = "Історія опитувань"; "room_details_polls" = "Історія опитувань"; "accessibility_selected" = "вибрано"; +"voice_broadcast_playback_lock_screen_placeholder" = "Голосові трансляції"; From 907a3e49d2bffa735a074b88db555adc1efac882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 17 Jan 2023 12:46:50 +0000 Subject: [PATCH 165/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2362 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index abe430b8f..46123bd4e 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2653,3 +2653,4 @@ "poll_history_title" = "Küsitluste ajalugu"; "room_details_polls" = "Küsitluste ajalugu"; "accessibility_selected" = "valitud"; +"voice_broadcast_playback_lock_screen_placeholder" = "Ringhäälingukõne"; From 0c1b8974c857bb9987740387bf68687c2ed06027 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 17 Jan 2023 10:21:33 +0000 Subject: [PATCH 166/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2362 of 2362 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index f666917c3..375f5f3eb 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2908,3 +2908,4 @@ "poll_history_title" = "Riwayat pemungutan suara"; "room_details_polls" = "Riwayat pemungutan suara"; "accessibility_selected" = "dipilih"; +"voice_broadcast_playback_lock_screen_placeholder" = "Siaran suara"; From 2aa16da24b328420110c854b8ef079e93987a1e2 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 17 Jan 2023 17:46:07 +0000 Subject: [PATCH 167/468] Translated using Weblate (German) Currently translated at 100.0% (2364 of 2364 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 3e96b83e0..ee49f37f1 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2716,3 +2716,5 @@ "room_details_polls" = "Umfrageverlauf"; "accessibility_selected" = "ausgewählt"; "voice_broadcast_playback_lock_screen_placeholder" = "Sprachübertragung"; +"voice_broadcast_connection_error_message" = "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut."; +"voice_broadcast_connection_error_title" = "Verbindungsfehler"; From b42ee13ffb594a9bcfe86829889e93bcae77f090 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 17 Jan 2023 20:25:53 +0000 Subject: [PATCH 168/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2364 of 2364 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 92f0606a2..ee38e4637 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2907,3 +2907,5 @@ "room_details_polls" = "Історія опитувань"; "accessibility_selected" = "вибрано"; "voice_broadcast_playback_lock_screen_placeholder" = "Голосові трансляції"; +"voice_broadcast_connection_error_message" = "На жаль, ми не можемо розпочати запис прямо зараз. Повторіть спробу пізніше."; +"voice_broadcast_connection_error_title" = "Помилка з'єднання"; From c72f62c92bc02e5dd5b89900c1406a5bc6b93cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 17 Jan 2023 15:34:11 +0000 Subject: [PATCH 169/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2364 of 2364 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 46123bd4e..c581b99fa 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2654,3 +2654,5 @@ "room_details_polls" = "Küsitluste ajalugu"; "accessibility_selected" = "valitud"; "voice_broadcast_playback_lock_screen_placeholder" = "Ringhäälingukõne"; +"voice_broadcast_connection_error_message" = "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti."; +"voice_broadcast_connection_error_title" = "Ühenduse viga"; From 7751ee9afe959573de22eaa772d995c40dbb97ea Mon Sep 17 00:00:00 2001 From: Demo337 Date: Tue, 17 Jan 2023 16:58:33 +0000 Subject: [PATCH 170/468] Translated using Weblate (Arabic) Currently translated at 37.8% (895 of 2364 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ar/ --- Riot/Assets/ar.lproj/Vector.strings | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings index a9a7b2cb6..7810fc3cc 100644 --- a/Riot/Assets/ar.lproj/Vector.strings +++ b/Riot/Assets/ar.lproj/Vector.strings @@ -455,7 +455,7 @@ "sign_up" = "الاِشتِراك"; "dismiss" = "إبعَاد"; "discard" = "اِستِبعاد"; -"abort" = "إِجهَاض"; +"abort" = "إنهاء"; "yes" = "نَعَم"; // Action @@ -1078,3 +1078,8 @@ /* The placeholder will show the email address that was entered. */ "authentication_verify_email_waiting_message" = "اتبع التعليمات المرسلة إلى %@"; "invite_to" = "الدعوة إلى %@"; +"password_policy_pwd_in_dict_error" = "تم العثور على كلمة المرور هذه في القاموس لدينا، وهي كلمة مرور غير مسموح في استخدامها."; + +// Others +"or" = "أو"; +"accessibility_selected" = "تم تحديده"; From 959d7c80f6f0ccce58a930f636cf3ea37528e46d Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 17 Jan 2023 17:16:29 +0000 Subject: [PATCH 171/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2364 of 2364 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index e41769142..269e57f0a 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2904,3 +2904,6 @@ "poll_history_title" = "História ankety"; "room_details_polls" = "História ankety"; "accessibility_selected" = "vybrané"; +"voice_broadcast_connection_error_message" = "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr."; +"voice_broadcast_connection_error_title" = "Chyba pripojenia"; +"voice_broadcast_playback_lock_screen_placeholder" = "Hlasové vysielanie"; From dfa2c523cfd9b273b72161297cf693c460ba6cc3 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 18 Jan 2023 10:13:33 +0000 Subject: [PATCH 172/468] Translated using Weblate (German) Currently translated at 100.0% (2368 of 2368 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index ee49f37f1..92fb0aa46 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2718,3 +2718,7 @@ "voice_broadcast_playback_lock_screen_placeholder" = "Sprachübertragung"; "voice_broadcast_connection_error_message" = "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut."; "voice_broadcast_connection_error_title" = "Verbindungsfehler"; +"wysiwyg_composer_format_action_code_block" = "Quelltextblock umschalten"; +"wysiwyg_composer_format_action_quote" = "Zitat umschalten"; +"wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten"; +"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; From 3e7d410a00779825140222adce8577c013699c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 18 Jan 2023 09:53:51 +0000 Subject: [PATCH 173/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2368 of 2368 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index c581b99fa..9e8c25329 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2656,3 +2656,7 @@ "voice_broadcast_playback_lock_screen_placeholder" = "Ringhäälingukõne"; "voice_broadcast_connection_error_message" = "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti."; "voice_broadcast_connection_error_title" = "Ühenduse viga"; +"wysiwyg_composer_format_action_quote" = "Lülita tsiteerimine sisse/välja"; +"wysiwyg_composer_format_action_code_block" = "Lülita koodiblokk sisse/välja"; +"wysiwyg_composer_format_action_ordered_list" = "Lülita nummerdatud loend sisse/välja"; +"wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja"; From ac7ef4ece93443ec2c3e7ba7e1e7ba688090f980 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 18 Jan 2023 13:01:50 +0000 Subject: [PATCH 174/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2368 of 2368 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index ee38e4637..b1ba62469 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2909,3 +2909,7 @@ "voice_broadcast_playback_lock_screen_placeholder" = "Голосові трансляції"; "voice_broadcast_connection_error_message" = "На жаль, ми не можемо розпочати запис прямо зараз. Повторіть спробу пізніше."; "voice_broadcast_connection_error_title" = "Помилка з'єднання"; +"wysiwyg_composer_format_action_quote" = "Перемкнути цитування"; +"wysiwyg_composer_format_action_code_block" = "Перемкнути блок коду"; +"wysiwyg_composer_format_action_ordered_list" = "Перемкнути на нумерований список"; +"wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список"; From 89f94f467926547bea26b18c277f1393fc50c39a Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 18 Jan 2023 13:42:47 +0000 Subject: [PATCH 175/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2368 of 2368 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 375f5f3eb..0fcb766eb 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2909,3 +2909,9 @@ "room_details_polls" = "Riwayat pemungutan suara"; "accessibility_selected" = "dipilih"; "voice_broadcast_playback_lock_screen_placeholder" = "Siaran suara"; +"wysiwyg_composer_format_action_quote" = "Saklar kutipan"; +"wysiwyg_composer_format_action_code_block" = "Saklar blok kode"; +"wysiwyg_composer_format_action_ordered_list" = "Saklar daftar bernomor"; +"wysiwyg_composer_format_action_unordered_list" = "Saklar daftar bulat"; +"voice_broadcast_connection_error_message" = "Sayangnya kami tidak dapat memulai sebuah rekaman saat ini. Silakan coba lagi nanti."; +"voice_broadcast_connection_error_title" = "Kesalahan koneksi"; From f3b12ec9bdc0899502f5aed060787b7988c89199 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 18 Jan 2023 11:59:19 +0000 Subject: [PATCH 176/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2368 of 2368 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 269e57f0a..0775c77ad 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2907,3 +2907,7 @@ "voice_broadcast_connection_error_message" = "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr."; "voice_broadcast_connection_error_title" = "Chyba pripojenia"; "voice_broadcast_playback_lock_screen_placeholder" = "Hlasové vysielanie"; +"wysiwyg_composer_format_action_quote" = "Prepínanie citácie"; +"wysiwyg_composer_format_action_code_block" = "Prepnutie bloku kódu"; +"wysiwyg_composer_format_action_ordered_list" = "Prepínanie číslovaného zoznamu"; +"wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami"; From 66e6b9c6aa3639c730aa577744ad9c3ab4ad6a33 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 18 Jan 2023 17:48:34 +0000 Subject: [PATCH 177/468] Translated using Weblate (German) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 92fb0aa46..c162e0484 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2722,3 +2722,4 @@ "wysiwyg_composer_format_action_quote" = "Zitat umschalten"; "wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten"; "wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; +"voice_broadcast_recorder_connection_error" = "Verbindungsfehler – Aufzeichnung pausiert"; From c00b4db5eb12ad497aa1129b14bc615092fd7708 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 18 Jan 2023 18:34:46 +0000 Subject: [PATCH 178/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index b1ba62469..02aaeed86 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2913,3 +2913,4 @@ "wysiwyg_composer_format_action_code_block" = "Перемкнути блок коду"; "wysiwyg_composer_format_action_ordered_list" = "Перемкнути на нумерований список"; "wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список"; +"voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено"; From 06555cde67ecc1e954d30dd1274043168b408ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 18 Jan 2023 17:34:31 +0000 Subject: [PATCH 179/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 9e8c25329..2db1f21d9 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2660,3 +2660,4 @@ "wysiwyg_composer_format_action_code_block" = "Lülita koodiblokk sisse/välja"; "wysiwyg_composer_format_action_ordered_list" = "Lülita nummerdatud loend sisse/välja"; "wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja"; +"voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud"; From 6a72c9b07254c7f7e1c8329191ca6721acab7ba9 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 18 Jan 2023 16:32:40 +0000 Subject: [PATCH 180/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 0fcb766eb..8a0e44098 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2915,3 +2915,4 @@ "wysiwyg_composer_format_action_unordered_list" = "Saklar daftar bulat"; "voice_broadcast_connection_error_message" = "Sayangnya kami tidak dapat memulai sebuah rekaman saat ini. Silakan coba lagi nanti."; "voice_broadcast_connection_error_title" = "Kesalahan koneksi"; +"voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda"; From c915ab9d99899e7dd91ed48da1a3f0814f16d068 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 18 Jan 2023 18:39:55 +0000 Subject: [PATCH 181/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2369 of 2369 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 0775c77ad..65a907e73 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2911,3 +2911,4 @@ "wysiwyg_composer_format_action_code_block" = "Prepnutie bloku kódu"; "wysiwyg_composer_format_action_ordered_list" = "Prepínanie číslovaného zoznamu"; "wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami"; +"voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené"; From 0bfebfb21019216b4b460fd02c0e1d53e2bdb691 Mon Sep 17 00:00:00 2001 From: Vri Date: Thu, 19 Jan 2023 12:54:02 +0000 Subject: [PATCH 182/468] Translated using Weblate (German) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index c162e0484..a5b6629f2 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2723,3 +2723,4 @@ "wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten"; "wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; "voice_broadcast_recorder_connection_error" = "Verbindungsfehler – Aufzeichnung pausiert"; +"poll_timeline_reply_ended_poll" = "Beendete Umfrage"; From 9d73c2284cb919d8702f5798aa7cddf67cb2ce46 Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Fri, 20 Jan 2023 00:29:57 +0000 Subject: [PATCH 183/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 81.9% (1943 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index bcd721356..7294bf84e 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -196,7 +196,7 @@ "room_event_action_copy" = "复制"; "room_event_action_quote" = "引用"; "room_event_action_redact" = "移除"; -"room_event_action_more" = "移动"; +"room_event_action_more" = "更多"; "room_event_action_share" = "分享"; "room_event_action_permalink" = "复制消息的链接"; "room_event_action_view_source" = "查看源数据"; @@ -2272,3 +2272,6 @@ "authentication_qr_login_display_step2" = "选择“以二维码登入”"; "authentication_qr_login_display_step1" = "在您的其它设备中打开Element"; "onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。"; +"user_session_learn_more" = "了解更多"; +"manage_session_name_info_link" = "了解更多"; +"threads_beta_information_link" = "了解更多"; From efd066b6b5b45334b0563c18555cf5734d8dea22 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Sat, 21 Jan 2023 18:15:54 +0000 Subject: [PATCH 184/468] Translated using Weblate (Japanese) Currently translated at 64.8% (1536 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 88f0760ee..2a82d520c 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1763,13 +1763,13 @@ "home_context_menu_favourite" = "お気に入り"; "all_chats_user_menu_settings" = "ユーザー設定"; "all_chats_edit_layout_show_filters" = "フィルターを表示"; -"all_chats_edit_layout_show_recents" = "最近使用したものを表示"; +"all_chats_edit_layout_show_recents" = "最近の履歴を表示"; "all_chats_edit_layout_alphabetical_order" = "アルファベット順で並び替え"; "all_chats_edit_layout_activity_order" = "アクティビティで並び替え"; "space_selector_create_space" = "スペースを作成"; -"space_selector_empty_view_information" = "スペースは、ルームや連絡先をグループ化する方法です。以下からスペースを作成できます。"; +"space_selector_empty_view_information" = "スペースは、ルームと連絡先をまとめる方法です。はじめに、スペースを作成しましょう。"; "all_chats_all_filter" = "全て"; -"all_chats_edit_layout_recents" = "最近"; +"all_chats_edit_layout_recents" = "履歴"; "all_chats_edit_layout_unreads" = "未読"; "all_chats_section_title" = "チャット"; @@ -1799,3 +1799,5 @@ "service_terms_modal_information_title_identity_server" = "IDサーバー"; "location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; "location_sharing_live_error" = "位置情報(ライブ)のエラー"; +"all_chats_onboarding_page_title3" = "フィードバックを送信"; +"all_chats_edit_layout" = "レイアウトの設定"; From 9ec30dd70ba7e8124e3a0baa7830c132c11ccdcc Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 19 Jan 2023 12:14:57 +0000 Subject: [PATCH 185/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 80dbe5b3c..6dd053e54 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2701,3 +2701,12 @@ "poll_history_title" = "Szavazás alakulása"; "room_details_polls" = "Szavazás alakulása"; "accessibility_selected" = "kiválasztva"; +"wysiwyg_composer_format_action_quote" = "Idézet be/ki"; +"wysiwyg_composer_format_action_code_block" = "Kód blokk be/ki"; +"wysiwyg_composer_format_action_ordered_list" = "Számozott lista ki-,bekapcsolása"; +"wysiwyg_composer_format_action_unordered_list" = "Lista ki-,bekapcsolása"; +"poll_timeline_reply_ended_poll" = "Lezárt szavazások"; +"voice_broadcast_recorder_connection_error" = "Kapcsolódási hiba – Felvétel szüneteltetve"; +"voice_broadcast_connection_error_message" = "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később."; +"voice_broadcast_connection_error_title" = "Kapcsolat hiba"; +"voice_broadcast_playback_lock_screen_placeholder" = "Hang közvetítés"; From 21eb599aac66e8ced8525de5528d777b9096e3a4 Mon Sep 17 00:00:00 2001 From: random Date: Sat, 21 Jan 2023 08:52:33 +0000 Subject: [PATCH 186/468] Translated using Weblate (Italian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index b5e7ba84f..8dbc7f07a 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2682,3 +2682,11 @@ "voice_broadcast_playback_lock_screen_placeholder" = "Trasmissione vocale"; "room_details_polls" = "Cronologia sondaggi"; "accessibility_selected" = "selezionato"; +"wysiwyg_composer_format_action_quote" = "Attiva/disattiva citazione"; +"wysiwyg_composer_format_action_code_block" = "Attiva/disattiva blocco di codice"; +"wysiwyg_composer_format_action_ordered_list" = "Attiva/disattiva elenco numerato"; +"wysiwyg_composer_format_action_unordered_list" = "Attiva/disattiva elenco puntato"; +"poll_timeline_reply_ended_poll" = "Sondaggio terminato"; +"voice_broadcast_recorder_connection_error" = "Errore di connessione - Registrazione in pausa"; +"voice_broadcast_connection_error_message" = "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi."; +"voice_broadcast_connection_error_title" = "Errore di connessione"; From 8847ff5b8acf1186de9e83148f50653d88eca5af Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 19 Jan 2023 12:05:21 +0000 Subject: [PATCH 187/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 02aaeed86..484093120 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2914,3 +2914,4 @@ "wysiwyg_composer_format_action_ordered_list" = "Перемкнути на нумерований список"; "wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список"; "voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено"; +"poll_timeline_reply_ended_poll" = "Завершене опитування"; From 8b8a898e319016a8dffab08737c5ea096a8566e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 19 Jan 2023 11:05:30 +0000 Subject: [PATCH 188/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 2db1f21d9..063de0b20 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2661,3 +2661,4 @@ "wysiwyg_composer_format_action_ordered_list" = "Lülita nummerdatud loend sisse/välja"; "wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja"; "voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud"; +"poll_timeline_reply_ended_poll" = "Lõppenud küsitlus"; From 867b37fa70d84b6b08d05435c55efdf857e528b1 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 19 Jan 2023 09:51:08 +0000 Subject: [PATCH 189/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 8a0e44098..efb28c6d7 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2916,3 +2916,4 @@ "voice_broadcast_connection_error_message" = "Sayangnya kami tidak dapat memulai sebuah rekaman saat ini. Silakan coba lagi nanti."; "voice_broadcast_connection_error_title" = "Kesalahan koneksi"; "voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda"; +"poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir"; From 2dd72547e51f5b3da166627c81aed112aea39f63 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 20 Jan 2023 00:30:12 +0000 Subject: [PATCH 190/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 65a907e73..d34e26d64 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2912,3 +2912,4 @@ "wysiwyg_composer_format_action_ordered_list" = "Prepínanie číslovaného zoznamu"; "wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami"; "voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené"; +"poll_timeline_reply_ended_poll" = "Ukončená anketa"; From 5109c85bc4c6eb4ec9e1d6a7f18417b3ca0d2a67 Mon Sep 17 00:00:00 2001 From: phardyle Date: Mon, 23 Jan 2023 04:45:31 +0000 Subject: [PATCH 191/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 81.9% (1943 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 7294bf84e..f7fd8166c 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -2232,19 +2232,19 @@ "onboarding_use_case_message" = "我们将帮助你连接"; "invite_to" = "邀请到%@"; "threads_empty_title" = "保持讨论的有条理性"; -"threads_action_my_threads" = "我的线程"; -"threads_action_all_threads" = "所有线程"; -"threads_title" = "线程"; -"thread_copy_link_to_thread" = "将链接复制到线程"; +"threads_action_my_threads" = "我的消息列"; +"threads_action_all_threads" = "所有消息列"; +"threads_title" = "消息列"; +"thread_copy_link_to_thread" = "将链接复制到消息列"; // MARK: Threads -"room_thread_title" = "线程"; +"room_thread_title" = "消息列"; "room_accessibility_record_voice_message_hint" = "双击并保持录音。"; "room_accessibility_record_voice_message" = "录制语音消息"; "room_accessibility_thread_more" = "更多"; -"room_accessibility_threads" = "线程"; -"room_event_copy_link_info" = "链接复制到剪贴板。"; -"room_event_action_reply_in_thread" = "线程"; +"room_accessibility_threads" = "消息列"; +"room_event_copy_link_info" = "链接已复制到剪贴板。"; +"room_event_action_reply_in_thread" = "消息列"; "room_event_action_view_in_room" = "在房间浏览"; "room_first_message_placeholder" = "发送您的第一条消息……"; "room_participants_invite_prompt_to_msg" = "您确定要邀请%@ 到 %@吗?"; From 16c300b78cce4685c527d39c9691d724eb1bf03c Mon Sep 17 00:00:00 2001 From: Vri Date: Mon, 23 Jan 2023 11:21:15 +0000 Subject: [PATCH 192/468] Translated using Weblate (German) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index a5b6629f2..bb52fc5ea 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2724,3 +2724,10 @@ "wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; "voice_broadcast_recorder_connection_error" = "Verbindungsfehler – Aufzeichnung pausiert"; "poll_timeline_reply_ended_poll" = "Beendete Umfrage"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migriere Daten\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Krypto-SDK ist aktiviert. Zum Deaktivieren, bitte die App neu installieren"; +"settings_labs_confirm_crypto_sdk" = "Dies kann nicht rückgängig gemacht werden"; +"settings_labs_enable_crypto_sdk" = "Rust-basiertes Krypto-SDK aktivieren"; From 8e49d172d322fd69953284dbbdc362345d969e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Mon, 23 Jan 2023 10:16:05 +0000 Subject: [PATCH 193/468] Translated using Weblate (Icelandic) Currently translated at 84.7% (2013 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/is/ --- Riot/Assets/is.lproj/Vector.strings | 116 ++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index b569fd281..10722ec1b 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -2368,3 +2368,119 @@ // MARK: Authentication "authentication_registration_title" = "Búðu til aðganginn þinn"; +"notice_voice_broadcast_ended_by_you" = "Þú endaðir talútsendingu."; +"notice_voice_broadcast_ended" = "%@ endaði talútsendingu."; +"notice_voice_broadcast_live" = "Bein útsending"; +"deselect_all" = "Afvelja allt"; +"wysiwyg_composer_link_action_edit_title" = "Breyta tengli"; +"wysiwyg_composer_link_action_create_title" = "Búa til tengil"; +"wysiwyg_composer_link_action_link" = "Tengill"; + + + +// Links +"wysiwyg_composer_link_action_text" = "Texti"; +"wysiwyg_composer_start_action_voice_broadcast" = "Útvörpun tals"; +"wysiwyg_composer_start_action_text_formatting" = "Sníðing texta"; +"wysiwyg_composer_start_action_camera" = "Myndavél"; +"wysiwyg_composer_start_action_location" = "Staðsetning"; +"wysiwyg_composer_start_action_polls" = "Kannanir"; +"wysiwyg_composer_start_action_attachments" = "Viðhengi"; +"wysiwyg_composer_start_action_stickers" = "Límmerki"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Ljósmyndasafn"; +"user_session_overview_session_details_button_title" = "Nánar um setuna"; +"user_session_overview_session_title" = "Seta"; +"user_session_overview_current_session_title" = "Núverandi seta"; +"user_session_details_application_url" = "Slóð (URL)"; +"user_session_details_application_version" = "Útgáfa"; +"user_session_details_application_name" = "Heiti"; +"user_session_details_device_os" = "Stýrikerfi"; +"user_session_details_device_browser" = "Vafri"; +"user_session_details_device_model" = "Gerð"; +"user_session_details_device_ip_location" = "Staðsetning IP-vistfangs"; +"user_session_details_device_ip_address" = "IP-vistfang"; +"user_session_details_last_activity" = "Síðasta virkni"; +"user_session_details_session_id" = "Auðkenni setu"; +"user_session_details_session_name" = "Nafn á setu"; +"user_session_details_device_section_header" = "Tæki"; +"user_session_details_application_section_header" = "Forrit"; +"user_session_details_session_section_header" = "Seta"; +"user_session_details_title" = "Nánar um setuna"; +"device_type_name_unknown" = "Óþekkt"; +"device_type_name_mobile" = "Farsími"; +"device_type_name_web" = "Vefur"; +"device_type_name_desktop" = "Borðtölva"; +"user_other_session_selected_count" = "%@ valið"; +"user_other_session_clear_filter" = "Hreinsa síu"; +"user_other_session_no_unverified_sessions" = "Engar óstaðfestar setur fundust."; +"user_other_session_no_verified_sessions" = "Engar staðfestar setur fundust."; +"user_other_session_no_inactive_sessions" = "Engar óvirkar setur fundust."; +"user_other_session_filter_menu_inactive" = "Óvirkt"; +"user_other_session_filter_menu_unverified" = "Óstaðfestar"; +"user_other_session_filter_menu_verified" = "Staðfestar"; +"user_other_session_filter_menu_all" = "Allar setur"; +"user_other_session_filter" = "Sía"; +"user_other_session_security_recommendation_title" = "Aðrar setur"; +"user_session_inactive_session_title" = "Óvirkar setur"; +"user_session_unverified_session_title" = "Óstaðfest seta"; +"user_session_verified_session_title" = "Sannreyndar setur"; +"user_session_got_it" = "Náði því"; +"user_session_push_notifications" = "Ýti-tilkynningar"; +"user_session_verification_unknown_short" = "Óþekkt"; +"user_session_verification_unknown" = "Óþekkt staða sannvottunar"; +"user_sessions_view_all_action" = "Skoða öll (%d)"; +"user_sessions_overview_link_device" = "Tengja tæki"; +"user_sessions_overview_current_session_section_title" = "Núverandi seta"; +"user_sessions_hide_location_info" = "Fela IP-vistfang"; +"user_sessions_show_location_info" = "Birta IP-vistfang"; +"user_sessions_overview_other_sessions_section_title" = "Aðrar setur"; +"user_sessions_overview_security_recommendations_inactive_title" = "Óvirkar setur"; +"user_sessions_overview_security_recommendations_unverified_title" = "Óstaðfestar setur"; +"user_sessions_overview_security_recommendations_section_title" = "Ráðleggingar varðandi öryggi"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"location_sharing_live_lab_promotion_activation" = "Virkja deilingu rauntímastaðsetninga"; +"location_sharing_live_timer_incoming" = "Í beinni til %@"; +"poll_timeline_reply_ended_poll" = "Lauk könnun"; +"poll_timeline_ended_text" = "Lauk könnuninni"; +"poll_history_past_segment_title" = "Fyrri kannanir"; +"poll_history_active_segment_title" = "Virkar kannanir"; + +// MARK: - Polls history + +"poll_history_title" = "Breytingaskrá könnunar"; +"all_chats_user_menu_accessibility_label" = "Valmynd notandans"; +"voice_broadcast_connection_error_title" = "Villa í tengingu"; +"voice_broadcast_voip_cannot_start_title" = "Get ekki hafið símtal"; +"voice_broadcast_stop_alert_agree_button" = "Já, stöðva"; +"voice_broadcast_buffering" = "Hleð í biðminni..."; +"voice_broadcast_time_left" = "%@ eftir"; +"voice_broadcast_tile" = "Útvörpun tals"; +"voice_broadcast_live" = "Beint"; +"voice_broadcast_playback_lock_screen_placeholder" = "Útvörpun tals"; + +// Unverified sessions +"key_verification_alert_title" = "Þú ert með óstaðfestar setur"; +"sign_out_confirmation_message" = "Ertu viss um að þú viljir skrá þig út?"; + +// MARK: Sign out warning + +"sign_out" = "Skrá út"; +"secure_key_backup_setup_cancel_alert_message" = "Ef þú hættir við núna, geturðu tapað dulrituðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum.\n\nÞú getur víka sett upp örugga afritun og sýslað með dulritunarlyklana þína í stillingunum."; +"room_details_polls" = "Breytingaskrá könnunar"; +"manage_session_sign_out_other_sessions" = "Skrá út úr öllum öðrum setum"; +"manage_session_rename" = "Endurnefna setu"; +"settings_labs_enable_voice_broadcast" = "Útvörpun tals"; +"authentication_qr_login_failure_retry" = "Reyna aftur"; +"authentication_qr_login_loading_connecting_device" = "Tengist við tæki"; +"authentication_qr_login_scan_title" = "Skanna QR-kóða"; +"authentication_qr_login_start_title" = "Skanna QR-kóða"; +"authentication_login_with_qr" = "Skrá inn með QR-kóða"; From 40fc5861e655dc226f0d5cd9483d6e95280c9f8f Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 23 Jan 2023 11:38:32 +0000 Subject: [PATCH 194/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 484093120..95c51d890 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2915,3 +2915,10 @@ "wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список"; "voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено"; "poll_timeline_reply_ended_poll" = "Завершене опитування"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Перенесення даних\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Crypto SDK увімкнено. Щоб вимкнути, перевстановіть застосунок"; +"settings_labs_confirm_crypto_sdk" = "Дію не можна скасувати"; +"settings_labs_enable_crypto_sdk" = "Увімкнути новий заснований на rust Crypto SDK"; From f271b665d263ae5a5af9678aeb191f5cbabc4a55 Mon Sep 17 00:00:00 2001 From: Linerly Date: Mon, 23 Jan 2023 09:30:50 +0000 Subject: [PATCH 195/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index efb28c6d7..508d3eb7d 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2917,3 +2917,10 @@ "voice_broadcast_connection_error_title" = "Kesalahan koneksi"; "voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda"; "poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "SDK Kripto diaktifkan. Untuk menonaktifkan, mohon memasang ulang aplikasi"; +"settings_labs_confirm_crypto_sdk" = "Tindakan ini tidak dapat diurungkan"; +"settings_labs_enable_crypto_sdk" = "Aktifkan SDK Kripto baru berbasis Rust"; From 92eb090a5cb2385ec90cafb5c6e65aebc86c94b7 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 23 Jan 2023 16:40:41 +0000 Subject: [PATCH 196/468] Translated using Weblate (Japanese) Currently translated at 64.7% (1536 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 2a82d520c..9c76e1920 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1720,7 +1720,7 @@ "pin_protection_reset_alert_action_reset" = "リセット"; "authentication_recaptcha_title" = "あなたは人間ですか?"; "authentication_verify_msisdn_waiting_button" = "コードを再送信"; -"authentication_choose_password_submit_button" = "パスワードをリセット"; +"authentication_choose_password_submit_button" = "パスワードを再設定"; "authentication_choose_password_signout_all_devices" = "全ての端末からサインアウト"; "authentication_choose_password_text_field_placeholder" = "新しいパスワード"; "authentication_terms_title" = "プライバシーポリシー"; From 75872f2c964a5ee9ee5914922213b30182daf433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 23 Jan 2023 15:55:24 +0000 Subject: [PATCH 197/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 063de0b20..71ea69ebf 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2662,3 +2662,10 @@ "wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja"; "voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud"; "poll_timeline_reply_ended_poll" = "Lõppenud küsitlus"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Uus Crypto SDK on kasutusel. Tema väljalülitamiseks palun paigalda rakendus uuesti"; +"settings_labs_confirm_crypto_sdk" = "Seda toimingut ei saa tagasi pöörata"; +"settings_labs_enable_crypto_sdk" = "Võta kasutusele uus Rust-keelel põhinev Crypto SDK"; From 2f4ed1d64b5e156e313c4da27e84e0777d9822df Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 23 Jan 2023 22:39:48 +0000 Subject: [PATCH 198/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index d34e26d64..83b316cf8 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2913,3 +2913,10 @@ "wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami"; "voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené"; "poll_timeline_reply_ended_poll" = "Ukončená anketa"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Crypto SDK je povolené. Ak to chcete vypnúť, preinštalujte prosím aplikáciu"; +"settings_labs_confirm_crypto_sdk" = "Túto akciu nemožno vrátiť späť"; +"settings_labs_enable_crypto_sdk" = "Zapnúť nové Crypto SDK využívajúce Rust"; From ce626bfb6f7526c0f43ef0fbc9bf38757b4b1f69 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 24 Jan 2023 12:25:27 +0100 Subject: [PATCH 199/468] Update tests --- .../Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift | 2 +- .../Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 7b9e8fc3f..986fd37bd 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -40,7 +40,7 @@ final class PollHistoryUITests: MockScreenTestCase { let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] - let winningOption = app.staticTexts["PollListData.winningOption"] + let winningOption = app.buttons["PollAnswerOption0"] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertTrue(items.exists) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift index 2814223df..e2eff7475 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -42,7 +42,7 @@ final class PollHistoryViewModelTests: XCTestCase { func testLoadingStateIsTrueWhileLoading() { XCTAssertFalse(viewModel.state.isLoading) - pollHistoryService.nextPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() + pollHistoryService.nextBatchPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() viewModel.process(viewAction: .viewAppeared) XCTAssertTrue(viewModel.state.isLoading) } From bfb0ee9d57c8504afefb4687097486cbd1f73145 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 24 Jan 2023 12:39:28 +0000 Subject: [PATCH 200/468] changelog.d: Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index 02f9e605f..35ba935b2 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.24.7' +$matrixSDKVersion = '= 0.24.8' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..4c12806ec --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)). \ No newline at end of file From f6a49c0d7a07b499fed00ca89f15b47cdc7c57f5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 24 Jan 2023 12:39:29 +0000 Subject: [PATCH 201/468] version++ --- CHANGES.md | 40 +++++++++++++++++++++++++++++++++++ changelog.d/5148.bugfix | 1 - changelog.d/7222.bugfix | 1 - changelog.d/7229.change | 1 - changelog.d/7234.change | 1 - changelog.d/7238.feature | 1 - changelog.d/7252.bugfix | 1 - changelog.d/7261.bugfix | 1 - changelog.d/7263.bugfix | 1 - changelog.d/7271.feature | 1 - changelog.d/7279.change | 1 - changelog.d/7280.change | 1 - changelog.d/7283.feature | 1 - changelog.d/7285.change | 1 - changelog.d/pr-7225.change | 1 - changelog.d/pr-7256.build | 1 - changelog.d/pr-7257.bugfix | 1 - changelog.d/pr-7267.change | 1 - changelog.d/pr-7268.bugfix | 1 - changelog.d/pr-7272.change | 1 - changelog.d/pr-7273.change | 1 - changelog.d/pr-7275.change | 1 - changelog.d/pr-7278.change | 1 - changelog.d/pr-7284.change | 1 - changelog.d/pr-7286.change | 1 - changelog.d/x-nolink-0.change | 1 - 26 files changed, 40 insertions(+), 25 deletions(-) delete mode 100644 changelog.d/5148.bugfix delete mode 100644 changelog.d/7222.bugfix delete mode 100644 changelog.d/7229.change delete mode 100644 changelog.d/7234.change delete mode 100644 changelog.d/7238.feature delete mode 100644 changelog.d/7252.bugfix delete mode 100644 changelog.d/7261.bugfix delete mode 100644 changelog.d/7263.bugfix delete mode 100644 changelog.d/7271.feature delete mode 100644 changelog.d/7279.change delete mode 100644 changelog.d/7280.change delete mode 100644 changelog.d/7283.feature delete mode 100644 changelog.d/7285.change delete mode 100644 changelog.d/pr-7225.change delete mode 100644 changelog.d/pr-7256.build delete mode 100644 changelog.d/pr-7257.bugfix delete mode 100644 changelog.d/pr-7267.change delete mode 100644 changelog.d/pr-7268.bugfix delete mode 100644 changelog.d/pr-7272.change delete mode 100644 changelog.d/pr-7273.change delete mode 100644 changelog.d/pr-7275.change delete mode 100644 changelog.d/pr-7278.change delete mode 100644 changelog.d/pr-7284.change delete mode 100644 changelog.d/pr-7286.change delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index f62115f7c..e4d8ef4d8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,43 @@ +## Changes in 1.9.16 (2023-01-24) + +✨ Features + +- Rich Text Composer: Enable bulleted/numbered lists support ([#7238](https://github.com/vector-im/element-ios/issues/7238)) +- Rich Text Composer: Enable quote & code blocks support ([#7271](https://github.com/vector-im/element-ios/issues/7271)) +- Voice Broadcast: When deleting a voice broadcast, all data is now deleted on server side (MSC3912 implementation). ([#7283](https://github.com/vector-im/element-ios/issues/7283)) + +🙌 Improvements + +- Labs: VoiceBroadcast: Handle VoIP buttons when VB is used ([#7225](https://github.com/vector-im/element-ios/pull/7225)) +- Polls: add UI for active poll history. ([#7267](https://github.com/vector-im/element-ios/pull/7267)) +- CryptoSDK: Add labs settings to enable Crypto SDK ([#7272](https://github.com/vector-im/element-ios/pull/7272)) +- Voice Broadcast: Improved detection of voice broadcast completion during playback. ([#7273](https://github.com/vector-im/element-ios/pull/7273)) +- Remove "Leave" button on Room details screen ([#7275](https://github.com/vector-im/element-ios/pull/7275)) +- Polls: poll history UI for past polls. ([#7278](https://github.com/vector-im/element-ios/pull/7278)) +- Polls: render replies to poll events better. ([#7284](https://github.com/vector-im/element-ios/pull/7284)) +- CryptoV2: Display migration progress during startup ([#7286](https://github.com/vector-im/element-ios/pull/7286)) +- Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)). +- Voice broadcast connection error handling while recording. ([#7229](https://github.com/vector-im/element-ios/issues/7229)) +- Handle a connection issue when we try to start a new voice broadcast. ([#7234](https://github.com/vector-im/element-ios/issues/7234)) +- Rich Text Editor: https:// or mailto: scheme is automatically added when creating a link if no scheme is specified. ([#7279](https://github.com/vector-im/element-ios/issues/7279)) +- Rich Text Editor: Adding a link over a blank selection, prompts the user to create a new link with new text to replace such selection. ([#7280](https://github.com/vector-im/element-ios/issues/7280)) +- Voice Broadcast: handle the lost of connectivity with the homeserver while recording. ([#7285](https://github.com/vector-im/element-ios/issues/7285)) + +🐛 Bugfixes + +- Voice Broadcast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. ([#7257](https://github.com/vector-im/element-ios/pull/7257)) +- Fix a crash caused by the missing Avatar Service dependency. ([#7268](https://github.com/vector-im/element-ios/pull/7268)) +- The (edited) tag for messages is now light grey like on web and Android. ([#5148](https://github.com/vector-im/element-ios/issues/5148)) +- Live Location Sharing does not work on first selection after granting "Allow always" location permission. ([#7222](https://github.com/vector-im/element-ios/issues/7222)) +- Voice Broadcast: Fixed an issue where the voice broadcast audio player progress bar behaved unexpectedly. ([#7252](https://github.com/vector-im/element-ios/issues/7252)) +- Voice Broadcast: VoiceBroadcast chunks are no longer resent as voice messages ([#7261](https://github.com/vector-im/element-ios/issues/7261)) +- Timeline's links and hyperlinks match now the blue colour of Android and Web. ([#7263](https://github.com/vector-im/element-ios/issues/7263)) + +🧱 Build + +- Fix Element Alpha workflow not being able to run. ([#7256](https://github.com/vector-im/element-ios/pull/7256)) + + ## Changes in 1.9.15 (2023-01-10) ✨ Features diff --git a/changelog.d/5148.bugfix b/changelog.d/5148.bugfix deleted file mode 100644 index 7f1ddcb3e..000000000 --- a/changelog.d/5148.bugfix +++ /dev/null @@ -1 +0,0 @@ -The (edited) tag for messages is now light grey like on web and Android. \ No newline at end of file diff --git a/changelog.d/7222.bugfix b/changelog.d/7222.bugfix deleted file mode 100644 index e1402cb0d..000000000 --- a/changelog.d/7222.bugfix +++ /dev/null @@ -1 +0,0 @@ -Live Location Sharing does not work on first selection after granting "Allow always" location permission. diff --git a/changelog.d/7229.change b/changelog.d/7229.change deleted file mode 100644 index 7099b8500..000000000 --- a/changelog.d/7229.change +++ /dev/null @@ -1 +0,0 @@ -Voice broadcast connection error handling while recording. diff --git a/changelog.d/7234.change b/changelog.d/7234.change deleted file mode 100644 index 52af7685d..000000000 --- a/changelog.d/7234.change +++ /dev/null @@ -1 +0,0 @@ -Handle a connection issue when we try to start a new voice broadcast. diff --git a/changelog.d/7238.feature b/changelog.d/7238.feature deleted file mode 100644 index 173f1195e..000000000 --- a/changelog.d/7238.feature +++ /dev/null @@ -1 +0,0 @@ -Rich Text Composer: Enable bulleted/numbered lists support diff --git a/changelog.d/7252.bugfix b/changelog.d/7252.bugfix deleted file mode 100644 index 0e823509f..000000000 --- a/changelog.d/7252.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: Fixed an issue where the voice broadcast audio player progress bar behaved unexpectedly. diff --git a/changelog.d/7261.bugfix b/changelog.d/7261.bugfix deleted file mode 100644 index 3594c5980..000000000 --- a/changelog.d/7261.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: VoiceBroadcast chunks are no longer resent as voice messages diff --git a/changelog.d/7263.bugfix b/changelog.d/7263.bugfix deleted file mode 100644 index 425e2dd95..000000000 --- a/changelog.d/7263.bugfix +++ /dev/null @@ -1 +0,0 @@ -Timeline's links and hyperlinks match now the blue colour of Android and Web. \ No newline at end of file diff --git a/changelog.d/7271.feature b/changelog.d/7271.feature deleted file mode 100644 index f2ae089f9..000000000 --- a/changelog.d/7271.feature +++ /dev/null @@ -1 +0,0 @@ -Rich Text Composer: Enable quote & code blocks support diff --git a/changelog.d/7279.change b/changelog.d/7279.change deleted file mode 100644 index c605f8920..000000000 --- a/changelog.d/7279.change +++ /dev/null @@ -1 +0,0 @@ -Rich Text Editor: https:// or mailto: scheme is automatically added when creating a link if no scheme is specified. diff --git a/changelog.d/7280.change b/changelog.d/7280.change deleted file mode 100644 index d387d563d..000000000 --- a/changelog.d/7280.change +++ /dev/null @@ -1 +0,0 @@ -Rich Text Editor: Adding a link over a blank selection, prompts the user to create a new link with new text to replace such selection. diff --git a/changelog.d/7283.feature b/changelog.d/7283.feature deleted file mode 100644 index d139728d5..000000000 --- a/changelog.d/7283.feature +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: When deleting a voice broadcast, all data is now deleted on server side (MSC3912 implementation). diff --git a/changelog.d/7285.change b/changelog.d/7285.change deleted file mode 100644 index 9d3f369ae..000000000 --- a/changelog.d/7285.change +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: handle the lost of connectivity with the homeserver while recording. diff --git a/changelog.d/pr-7225.change b/changelog.d/pr-7225.change deleted file mode 100644 index df6cfd7a7..000000000 --- a/changelog.d/pr-7225.change +++ /dev/null @@ -1 +0,0 @@ -Labs: VoiceBroadcast: Handle VoIP buttons when VB is used diff --git a/changelog.d/pr-7256.build b/changelog.d/pr-7256.build deleted file mode 100644 index 9a2cd140b..000000000 --- a/changelog.d/pr-7256.build +++ /dev/null @@ -1 +0,0 @@ -Fix Element Alpha workflow not being able to run. diff --git a/changelog.d/pr-7257.bugfix b/changelog.d/pr-7257.bugfix deleted file mode 100644 index 8a41b21c8..000000000 --- a/changelog.d/pr-7257.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. diff --git a/changelog.d/pr-7267.change b/changelog.d/pr-7267.change deleted file mode 100644 index cab02bc6a..000000000 --- a/changelog.d/pr-7267.change +++ /dev/null @@ -1 +0,0 @@ -Polls: add UI for active poll history. diff --git a/changelog.d/pr-7268.bugfix b/changelog.d/pr-7268.bugfix deleted file mode 100644 index b6af7cd57..000000000 --- a/changelog.d/pr-7268.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a crash caused by the missing Avatar Service dependency. diff --git a/changelog.d/pr-7272.change b/changelog.d/pr-7272.change deleted file mode 100644 index 04a8dcc2e..000000000 --- a/changelog.d/pr-7272.change +++ /dev/null @@ -1 +0,0 @@ -CryptoSDK: Add labs settings to enable Crypto SDK diff --git a/changelog.d/pr-7273.change b/changelog.d/pr-7273.change deleted file mode 100644 index 2c3d47b2e..000000000 --- a/changelog.d/pr-7273.change +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast: Improved detection of voice broadcast completion during playback. diff --git a/changelog.d/pr-7275.change b/changelog.d/pr-7275.change deleted file mode 100644 index 6435f2f71..000000000 --- a/changelog.d/pr-7275.change +++ /dev/null @@ -1 +0,0 @@ -Remove "Leave" button on Room details screen diff --git a/changelog.d/pr-7278.change b/changelog.d/pr-7278.change deleted file mode 100644 index f7254ebb9..000000000 --- a/changelog.d/pr-7278.change +++ /dev/null @@ -1 +0,0 @@ -Polls: poll history UI for past polls. diff --git a/changelog.d/pr-7284.change b/changelog.d/pr-7284.change deleted file mode 100644 index edab71856..000000000 --- a/changelog.d/pr-7284.change +++ /dev/null @@ -1 +0,0 @@ -Polls: render replies to poll events better. diff --git a/changelog.d/pr-7286.change b/changelog.d/pr-7286.change deleted file mode 100644 index ff8cbb820..000000000 --- a/changelog.d/pr-7286.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Display migration progress during startup diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 4c12806ec..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)). \ No newline at end of file From c2a2edc5432c7b39f634e6a6aa77d599a9e38b16 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 24 Jan 2023 14:13:59 +0000 Subject: [PATCH 202/468] finish version++ --- Podfile.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index dbc139f68..24b937005 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.7): - - MatrixSDK/Core (= 0.24.7) - - MatrixSDK/Core (0.24.7): + - MatrixSDK (0.24.8): + - MatrixSDK/Core (= 0.24.8) + - MatrixSDK/Core (0.24.8): - 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.7): - - MatrixSDKCrypto (= 0.1.7) - - MatrixSDK/JingleCallStack (0.24.7): + - MatrixSDK/CryptoSDK (0.24.8): + - MatrixSDKCrypto (= 0.1.8) + - MatrixSDK/JingleCallStack (0.24.8): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.7) + - MatrixSDKCrypto (0.1.8) - 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.7) - - MatrixSDK/JingleCallStack (= 0.24.7) + - MatrixSDK (= 0.24.8) + - MatrixSDK/JingleCallStack (= 0.24.8) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -197,7 +197,7 @@ CHECKOUT OPTIONS: :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: - AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce + AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58 AnalyticsEvents: 0cc8cf52da2fd464a2f39b788a295988151116ce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc @@ -220,8 +220,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 895929fad10b7ec9aa96d557403b44c5e3522211 - MatrixSDKCrypto: 2bd9ca41b2c644839f4e680a64897d56b3f95392 + MatrixSDK: cf1c1b2a9742f7f4fad21e94bd94cd8f13c47369 + MatrixSDKCrypto: 862d9b4dbb6861da030943f5a18c39258ed7345b OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -241,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 56782e2abd382278b3c5b23824ca74994fd0a97e +PODFILE CHECKSUM: 079b57b800c666ad864e1f059ae69e150a98a4f0 COCOAPODS: 1.11.3 From 35cb086a576b17fd67c2782766d9ececa1cd4526 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 24 Jan 2023 14:14:07 +0000 Subject: [PATCH 203/468] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 7271fabb8..210603b23 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.16 -CURRENT_PROJECT_VERSION = 1.9.16 +MARKETING_VERSION = 1.9.17 +CURRENT_PROJECT_VERSION = 1.9.17 From 04f85ad8b8025d13980e209b26c4acd271f867c5 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Tue, 24 Jan 2023 21:25:58 +0100 Subject: [PATCH 204/468] App Layout: Removed the onboarding flow --- .../AllChatsOnboarding/Contents.json | 6 -- .../Contents.json | 23 ----- .../all_chats_onboarding1.png | Bin 33485 -> 0 bytes .../all_chats_onboarding1@2x.png | Bin 96082 -> 0 bytes .../all_chats_onboarding1@3x.png | Bin 169678 -> 0 bytes .../Contents.json | 26 ----- .../all_chats_onboarding2.png | Bin 22361 -> 0 bytes .../all_chats_onboarding2@2x.png | Bin 55653 -> 0 bytes .../all_chats_onboarding2@3x.png | Bin 96423 -> 0 bytes .../Contents.json | 23 ----- .../all_chats_onboarding3.png | Bin 30534 -> 0 bytes .../all_chats_onboarding3@2x.png | Bin 88244 -> 0 bytes .../all_chats_onboarding3@3x.png | Bin 154809 -> 0 bytes Riot/Assets/de.lproj/Vector.strings | 8 -- Riot/Assets/en.lproj/Vector.strings | 9 -- Riot/Assets/et.lproj/Vector.strings | 8 -- Riot/Assets/fr.lproj/Vector.strings | 8 -- Riot/Assets/hu.lproj/Vector.strings | 8 -- Riot/Assets/id.lproj/Vector.strings | 8 -- Riot/Assets/is.lproj/Vector.strings | 5 - Riot/Assets/it.lproj/Vector.strings | 8 -- Riot/Assets/ja.lproj/Vector.strings | 1 - Riot/Assets/nl.lproj/Vector.strings | 8 -- Riot/Assets/pl.lproj/Vector.strings | 7 -- Riot/Assets/pt_BR.lproj/Vector.strings | 8 -- Riot/Assets/sk.lproj/Vector.strings | 8 -- Riot/Assets/sq.lproj/Vector.strings | 8 -- Riot/Assets/uk.lproj/Vector.strings | 8 -- Riot/Generated/Images.swift | 3 - Riot/Generated/Strings.swift | 32 ------ Riot/Managers/Settings/RiotSettings.swift | 5 - Riot/Modules/Application/LegacyAppDelegate.m | 3 - .../AllChats/AllChatsViewController.swift | 20 ---- .../AllChatsOnboardingModels.swift | 43 -------- .../AllChatsOnboardingViewModel.swift | 63 ------------ .../AllChatsOnboardingViewModelProtocol.swift | 23 ----- .../AllChatsOnboardingCoordinator.swift | 92 ------------------ ...OnboardingCoordinatorBridgePresenter.swift | 63 ------------ .../View/AllChatsOnboarding.swift | 80 --------------- .../View/AllChatsOnboardingPage.swift | 62 ------------ changelog.d/7298.change | 1 + 41 files changed, 1 insertion(+), 677 deletions(-) delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@2x.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2@3x.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png delete mode 100644 Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift delete mode 100644 RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift create mode 100644 changelog.d/7298.change diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json deleted file mode 100644 index d6a6b5903..000000000 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "all_chats_onboarding1.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "all_chats_onboarding1@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "all_chats_onboarding1@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1.png deleted file mode 100644 index 95fb854c7090fb84a0cd7fdf5bdc6de82a4a40a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33485 zcma%CWmgHXe|BBlHOKy{MU0l>hZ z!@ zjm#IR$6)iVRsN5=PKx>HqO|-USsj`}zWRg1_~>$0f9Iye1WD>TpD5N zezBwk(*V|4X+uTyrCIi)wQu@hLC1gI}16tmXsPD zhh`U>_U<&H+4`R#uFgw!)GC*V|{OV8Sv@s|Q1m4UG3yF`h7m;@&{27fN~`*^(|F8`VsgP}r(di`)x} z9&HIU?X*=XR^YQgDw2Hizk*(N-j_fTtzg2F)*mke_Nf?rwl4W5kyE0iBzqwB(xMbo zSV+|8%!onw3>-OPyB_HSU+Z;qRVI6uT?_;#mi$D`vw)5dnoP1KAeF;J7l0Fmu64`zW z`6LhQ5m|hZN;3ij0?sDw$;1CMBZ3cDBCf|qAgm>$ zA%Jy-R{_GgeY?8>6fVb-&wd2b10Nqy3bt!gATlmNti^}x-E?2G*4}>asQuci)x2v! zS4-&?RP>~?WFdN&mfZ}&%#n85=I#lSA%j{t4bm4iC)bO-ERVJU0OIHW^f1IA(0!hPK$72wuz=s~P!0*Yk?l#m?eOce{GK=y%IgC#W$@a=qj{5a&9(hS`P zODb*aLCB96%Wo8)7-}&{e4nP-X$@xcG$u$%yuQ{1 z_5+d0MDB!1mIJVAmK`rPy&#T#M1xt>k0J3Q&d5%0R-TigPX~G)_E2hVSc*(7_#-T` zNWCJ`e|VI&S{b)s%mwD1|__~k$B|_0zY)p8fP!!Q9=f+cLSkYY1mI$ ze$!|w7_kVh19%~)kYmuf4 zqp$@Wv#zytqcFqh!1~84-iYpaQ4Bo#tyTYn5-(CLegcX{?yS;UDSgOu(bG{W8 z)c$FtHr!krUMen!C%!6pd-NIZwN;3|AH^n>ut(K5ug`q96r+$(fy61b?9zYF&7og5 zPHs>6(we*eb;?Q>!Ap{c^5s_){7Pbn*~CyK{n+GvmFxALOr*=|=$7=tU}X zgnidH`(ummSuAhYGp)9nHrIaKfZkClmV7E(f|0(*Kj6Vj5T>BSib8KWIe_FyW*;*X zVKDPNWn9RA%y0329U0#$i@o!{s&q@S@s->QjK3cOj*j8)b~~F`g7EyeQe7ku=EYN`y*+aT3TbOPE zzRQgVo!??ip%j`)bLjQXw+TYqm)&I^`BR;en(AAuv9Imsw$KP$|2;osGB0qoFs~!A zEOl8Og@3Woko(wf+iz|re{lf_S|w#=RNCuXMAy>>8vqKEen!feT0{Qr-^-Pm%K07q zbwvFUZDX;*kMz{x%Wv2{)7x(m`A=TYPCJ(#=C)@Z?~rVw2x_qHPDaR*@v7#Qc!=K_NI30k7k1G22m_3tWH7rCt~AMtXGQ z^o3^y4)X5+M{q1TKdpQ5j^Ck}wi{tUa(w49rE~R?gvV6I={e#x^7Frkrl`_9?^C6% zry*Vu>VO*1mfw`n?YKs5T2OAm`JElG6n5)XSg^&9;=@IYT%1=T|0FP&KY)TbwL8ui z7S!V~s-@WaL>nmC91sA!L5k9#e{AEszpd}}&jS(-y}hz$vIHw8sCw;t1rQrvg%YKB-2zm49}7A(Xqa4To9nA!x=8 zztLH3Kg$Yey+5>HvtYjQ;K;`U_{(P5UrKHUT5PknS3_#*qwZ@fKTjpoZIg$84c2G^ zR!H0vLu=Zuz#jdaSKDSrT}(fDL|OGn)sl`3O%1OuHh4E%54~%Vv6W8QFt`#JKnFLa z{r;IBuGteuDRSyZP6aijKS-)AvPS%>yG-_!$E(lS`qP zi&TBrbdnq2mpk{`wuGOS&GHepQm2x^ZI8tHY2fQA2jKN8JtZ{(UpKX5b;=RAa=%IC z!VtdDO0(tQg4dsC^*0z7M7ayW7JcK{!VjR7Qu~VbbN8tKXr9QDpkMHA+k;Q zN_OvH-bfB+b|}?jSA#vt9z(iYq3A|kq@1SjUYsn=XvU*lZ`}?YP>WP+v;PJRi62#= zd@x+NxeLY?mtLpWJLkkDPo&{xBzf3!zg&M{_c&kvf^LNp3^Qtgqt(M6NEv?C<^k$A zk?0@^7+DEjZj$Q-uk=;ahPv+6CJKi8s5Y-(hQ}oAzsWJ0g^x(}HgK{Bpxb_ko*cdH z?_zJJN)A{gaz9@d@Hwlt7o~62d(y{ z`C@C`&K7m7jRN9n?zWP1ma1eUC7}#yG@|>ok|F-ozA&EbL{EmC5jU?7Krv$oQ zkCL{xZtZzDms_Hcn4^)&US%P@1Lx_x(h^4|?EvfhQHAPgm@4^4n<#l{B4aw~Cb0EsT%i_B>5lbK9ce0|As zqQ=ngTBiMr$n8>R%}($YWd34fMfV@pqbN;P`jI>%wAN>-+0O|{lH-~t?TCQA#zvu_ z=LBv02VfEQWIXbN;gZf@LMooW<_ZGIQ?L`w9|8RMDi&Um z50}s}ucUE*wDuD7Ysm1LM?PVpsX)Owj|e+|_ z*-%dX+gQkRReLoQzyLFI{5#$l*op=I^od~_Sv@4WE_;7?Yo~`*6wQjS$m>v5XKM5j zm2Ul4zO^YW{`oE6vqTaYyN`*|!{zA1`S<@q+ z9l9K=JyeS}lDdBBv(H@b27_C_@ixEN2@(BPz2_fTE=PAYvrDN6r+3-)L3FAwj4I$X z4-)?X;n&$7?>}yQREz1wTNx7S@Hukp#zy>fk#L#p8!tj-4p4Vbc%!*PjbYy!l(C5A z|H26rD9O|x-8ksgb5?8<`q#eTzXfjulzWHLwAZUVgEbK}#8s#F=p;XVD~LR`dG2~f z{$vU*n0*=*nM~j=xjz3eUk)(NQsc7)kBFn~;ycQP^LwH3Gmg+rp{{ z)E_li4Ak1DV+?{1K=$i%275F(9c~+m1*x;t4v~+fEvwa6HpiqYKnp3J;CLwNK)g5f zr0Z%?(MBUTw8G~-i!FBF{Xc<5K2Lj`8LXOj!HjLvzEl$+1-%cPrREe~P?mewy{IB2 z^&Mj~HhBOv&OFiye-)%}JaL(vpKO$D(QMJU>v!ITj8!$(uVMTT_Fp&hd$^%RU?vK? zJiU7r?c)F}xExx{43=7gUVd;uX0|_iyt5Fj`2w|_DQQTr3EvRrb&&~1)lvIK# zuOp0H3OHjw-zZZ_4HN0)zc1B_l7Tf+J? z;Po=j6;qEvG$Gp(>wy(-qsTX37^d+ZHh};7-zR=B)%pP(-q8_+dX(Sx2mBU-9U!*~ zAL8Q|SkwRGCT*ZnSvhSS;8)bP*?-YW|M%Zk zER)=qq}gmcv-?`c&zsJ}q!4YguEXtLMVxH=F<{NGecx!X@`viZ2dRt4E$ASzMiuHV zam#1AOvT8te~?EI9!WYY&vWLp^7HRa3ek0k@7O)Efr3E#U6ijXF(&}LTs__{j@>#e zV#@(eR%4TdhbPY-PlH{VkMr_{mB|jdcbA`e3~+9o)iW0R|EQIA6g?BZL?k6&RC!k8 z*Ko0jP8p?9iZnRgj10ffl)&Nqluo;h*k^T@vhc-Ca-E7*T)~evJ3vIHe}v$<*a4$a z^;M)ruKk4d*WK8MKM(DB47+1Oj+ zZs7$lC!bkKK$g`g0uEk$-n*Us<@0-j$Gl#HPO@;!m;Q(wtwjXD7}I`^lbjr>k<{QA z&&kLY=G@W}iZF=#$2A3bZC=3tFD0a)U_7?q_RBPC-T1z9R!14;-mpXKv0_6$+;_5c zNZ+kGrJA3sJ$qy38uNbo=W3= zO1&xbV2=@(Yd(O>2WpI-@HHw6h;gCxr|FNvSq#G)B2EpGoz7fmwZqx~wxRi36{)}7 zZ5+J&&*eV~-%JHXbTQ1iz0f@_P`H2dFSH=g^oCgdhz&NcWeQV!hp*2)1?|!mrH4FT zkL`s?vtLoeE98JqisC!Ni{-7-7Be)%4^U{tvq+_%o|cJ1sePm5Ys zFFI``P8^d68>nWq1=ZB*HyHC5p;4lmck0g;b{5R(e%UQ*yPny$yx04)uVyANZYej= z68714(CX!Ny&TfQgW;fjV$nb6gu!M#g1lu z&W&9**}#QY`R$*h;)H`fw9iHF`eT5_E{8Va{-TQqNg{tpt0lqvnr}Dul|Z1@zPra~ z=@fNb?2Ykzs`+zgjYg#$CHYEphSBn)*%_JL1VtjXERhzCUAK3g3<3DYd9Kr)uU_^%;48(D+}7EpZ3jN1$0jwUpn?D`@gXJS#iW@HEyDR z;v)?#$!L;4Cz8F=QRM#hi!&oqhYua#IlAE-Xm#>ERk%Fo^WHl8JAIRoTiUcek2f++ zo`oK1m>d^?b&3w50(m-ogdD_>ET#3v-qf3aXSLFGEyH7|9s1&SI*+&=!^z+!@v%HI z?j>T$xOJ`1)n)DohPA%LWj0Q%?+<{2r1-MSPZ&S}vJtRcfx-wk5}HvMSxswYMp~ z>-rCXZpl82AvQM3rCew##NAzj@HwnOnHAj!-w!{$@G;d2=sD4`8ZZ*B$4A`T8Tfxkpd?))RX~6egs_a)XCRII_lP7S+&;q~CeM)BhgFX{F7ehTd zy^hpo;AOl0)Y^IvWY4nj87X}yv>7L#fH7KWP7O|O6CjpSp}~+2l4w44ZgAYG3*?fz zs&*2VQJ48^6|ds(vgfzeb%ghBD;T}UZHd-y68|>tBFc#0xJiPOspHpyw7xB_SIc)g z*~0nZ1q@BI#MetT5f>xlCbek$d3#CSEOW-c9kRc$<99!AdTi8lbKORZToAa)#J7yyh^d?ajg20XE@WIN&lLW2K{^7ekw{E?gMVBhy?_FQV5xNDV^11BK zFKG|7k1tCN6-sw(DS-p0>+xdsgg0OyE)H9TsxMZF$9gjEVLGtAob836%Ijq@(BTodj zEaQ>&+?n>32pnz~-aI;pA|BwcKiTp^_t*vPT)lNkeEtzUDcm|;grgzA@!~uA`)=hC zcXeF$;K#qaG09)$%{!ijf}qCG}jmECFCGzrlu z60v*Hd0RM1eUtL}fk-YND%IsVY(jXoah<6AMzx+qWmbCny@nND2Y*WEqt61lFZ3}0 z8Re0sGV0o_Lf^hGk5ntlwz~6&VqBWBX=9%ZeaS#rdUvMLQ_0!D3#OHGi*u4nY<#c~ zJ~4Z%VvgVFfvKrW5<9AOIyoWk9Pv_=^j!L9F-g7{gBt}w#}}-3WGA3h;tVqWOh7?` zr%(M__r|z)lM46rxJ-pjveZ_XWS8KlgK}h~pcZA<6b&dI+?O!`R6U98ASPPVVd^Xa z@HzI;BVf&Kfs)N%t=kG#>^jIJ7*JPi@N*=AySJ=Ixy+V^e#&-PL_6z_J-grzhE(R= zEMkYiT)`Ur^gJ%cl4p!9?vIh^?48<*4gz|1LOu=4-jih+Sjv%!Lvoa_T49&S^-`A# z$-g~CKQVBG^4ShAa;o8`?zgiF(p(D(c_->o=!(XJ$DL+o!tU$)&xCKzTZP_ahMBwP zlgPjANUov9VzWVsb(q1i=C73IkOwT!Nj_42C$3bi5+yhEzxl$$*=*>Uhg=#*_VM6X1RW6yjgI4b zEm?Kjp{{n8AZ5ijmDSy&(1vP^!n2m1AxyRNoCe{KEDOKAI@r(`dj0&ura~q}0UK^h z;7wc=-~Xx+R4h3*aRN7T*il@o0|InL4PPwt^*@E9&nF`9Vx81a!XBAb4#b}%P(C9u zDW3_+mh44BF=5x3wvIaEa7R2?J%{GCaD9KI7m-B{d49dO{bqhZ+3R^Rjs=VZ;8TIp zh;8JT0|jX^=pCeWhevCweL3M2jq{>U57C1G>}_J+PaM466mgw5fB4>J8+Jz&rlR(S z6UTPHzTc1OrO7q)WvB@(@R-2q6JZZ%DQj+PL4#|O8EW{oCrbL z8#HDv!uZ5R&UbXr^`e+$0o;UTErvP~x1!i`LqMW>3A#aBg_G#0x)_B=QxcwaE_JTY z7}ZDH7@qFlhU{nz8yiKH(ABNIm+a-Th%vegI9DO*SrTiq$ zpljW(KQjT(J2S+0A=QxhtIpesJWUYgB!qU?@%N_X2x!X5dprxG|0lC6ZylG2tu3eF zHi&+g-_c)eA1$25#yGyOB1E1Su{R1(!e>gb3*SnlV`|Nu7v~H+LXh^4Yu{#>)zfr} z4@Ny4v&{hQuHkffbddM+O>S*T4ZT3I7z@zh?-tQ1*Eu4vd7TnIqcAOu;hcKwMM5YKkcs+2dT?Nj9*WnPu?1Cy=5@Sj}D5&7XZ@NMiaP;)UzmRRi!CaPx6H=bZUdgz&Fb)3uT6y;4ZloO zf9>sRdon$CqprR36OU=Rlv%AH6B}lrq9sDFripW=uT`0)*mBgDng%JM zqEyVExsi6Xu9F--DhL?;7zb^9bgG*MfDQ`8|&Jsexm6RB5;WCvo6{T88simcUUL& zS{R*=%UOqrq7`0baNcqIS$hmi*@P0|0a4!lglw2E)X9Y}4k?}0fe$wCR!6QNLI;K& znItVGHx$^7sZ3#idtt^Dw?~p-5=T7>0!<_AMY%nZQnXK}<$V6h0I=wsn06cqbGmVC zt?|ol*BuYSY&Pe+TKd*AJRjOXq0d2<&LX7{ON37wUr24{hkY0R#ygt#JX%DR@_Q4YBdan2!BxgvH+O#`^&+GfS(rs2J2v&j)h z9Na}v2XrTIrKMS3W}CN*XuC(z_xGLmwMcWlg-_SY_QJ%`ufz%b$k95qr=$`5mDZn; z&O3nf22^$YOCWv@b2aeV<}I9g3V%+m@H?h{Z!rv+6Xh99c?-BYh1@v@`M-cv66x^o zBaZe8Zp^C$={*oUxPt**&JEpBKB%}c6`!5Sb|oQFHfk9yaUnFK*D;NrKg||U^qs%P zBrR_j;nA;|!>Y}sGv!S4gFA9bI7;f~v*6pK3GAJOH4b`Y@W(({mW&ULp@|8ZC03fb z?Tqs(};ogwE zI?(#*B8A!(t+V}~aJ!xyES9gAw@x{(Rfm@ZY(6|L=TDWmV1n9anryIcDCr)Z0?siA z)!j3L@W!VC4!NKdgil`^N6XThX)N?P#qw^h8=9|_(j5`I4UekzcE?%!pML7t3(-vR?5Y-7@_a~uG$x5f1dUT31!fbnNdbwf`E*|O3u}V@B11m z{ci}Mi$-sR^F#{d5%0qOka{Rvh#Er7yzNpq5ca70{1;M2b4;GBe=T>iUe`TV<+)(V z+G^l3z={>=C|^3&&-sP7Aw~WI3)CC~GZdmB=Fj@5p?y=2>wi5L(6r^Yew0-YFO{`o z0`Uj_dA=*O-PsSyRg$aG!DzHf)<`LMfOosMVRN)L62g3bu zl9QcJHtk-kA3})$0U**$@J5T@75y;VTDumvv$H$0HZ8*~q7LWf8xXrM8hRyly)gBL z$TzWfUa6F9T6F%axERqIkRP)8Bf0a6w;MbA0UVgI?iv&WGPfnQQbR_^H}E*=ey>4< zf3_oc6nTnJpq;7Ig%HoMOuNLJO?>IVIO7Iu?aPQ%PrPYj*|pp08L)?{C2wfglsN_0 z9Nim}ZhtqMOv^wIpk158Mxt7kmn|Gms=4ek zMg$VAEu?Y&y^dQM23+jV%4AWzNqxX+=S)zFNtr(e4Yo~9dhOls4kAO*hQ^3Psk1Qf zJ&73w$FnQZ@K}25l1m3{AE7GEJ3gWe9X|+#mQqQZ-Xa{k@6MMB#DXZLt%nSP|B7E+ zzp$ET3$_htZ=hI(qmUKRA1_@#+8%{ytgy zeetdS59`BhGkER(4Z+NFa3kSY0bp?@FK(V7eX-wR z%>rQ)1#*G#do8oQbdFINp1xB{Fgr_|ht2eZ!%hnCO{FrD$eioNHE`U!~aK&+HmZeKkviBl~;~k*rzwURV=|J7XnbKNkl`^yr7C zx~^76XGZg(lf;w!@9rT7RcuZ%lePClkJY4vOtNBRB}p^Wex4Obc#q)G*G@x^ep%J) zDJp*5@9|>kEc)^qZW+8kO5#=fw!+m}?f(>7JVWM$ojT6%~VcRiS# z-?n!O!1|AdAixVbNmg@m8Q7$8Xy|NQcfQ4F5-^M4KpS8+oSBJ{-2KbJrZ7|`hU}9d zVB}<}ERH=gTkuyig7h>ugY$u)A;%{x;A&DZu?YKZ1lj*#tO2a$pv|k=)&r2o_aY<4 zEV{1z5j>a}t0qI-w5m4wG*Vy$IX=pN$HYnXb4ZZx?HjaJ-KIzORl z(4a5lQ}G^mag*e~YV#=K%eH+&Z&~iM%FlGJwrq7ozOP+a@OP5iL*hDV9Q*wuxv`iu!UcE`7nr@xQ8K%KXd{y6+`F|TOMoe!Jk>#5yMu7h zf4uKX1_XRQJ~Dx8X0a`&+#8fcr@FRoS9VX>rNZ5Fo$dfV$u#Vz#=pi84rJMTn*m8x z3x2U^263o1u1ApET$3AUYkQ)box^znM5%79NpXSWz6tGiv`D)X$ry{MYS`@0*u{ZQX3E zTt94e&BAuj3tWQINpfjc6SYisv<%jG=hM2C?G&vnftJ%J%=}`~-oO*XM}iuDK#fGe z2c7T=!1q_NcS4=?GUv>EgcM9zB$w`Nl{VU0&<(d?kjyXD@n~$X;rzQg7 zM|G;-bd>*;}UcvgeGw!zog##yv^6 zdhsv95yUh~GWQerf-|hlPx@DPWt)w~CLX*ORpa!r7}r4Vi^fuJN_ib;&-_n|^FIKY zq3zRarK`@fnwOj5qb~m~;Tq8zSwmbPQ&CDG>ZxHifiT)yKs7x8LzQ<qy z5oiT2LVc8H(!G$3e13kuOklk?N-ot3+(nV<@Mfud6L;MYZ|B%3W~r8oPcQ<{)l&yI zk8$*cq$%u5{>lqJ{YVP`M@0hJySiU*9X_c>`%>AO*neFXJEr5zR__g1g>fSM1@!Sg z#}%}jGi!d1*QDKl7e)jbns7-UEz8GBmSwJd#}4`XEb=v{iFin_)K)V*e67n6md5yP zk`G)nRCcQ(FDgd=Q`J+pV8lU-PDIDG&LZH5WbYH{-+rVO(|}ST-*{iP=|A$QGc;MA z4QW?|rYYl6@;MmezZ; z!XbD#O(cI+Wi(N7n;CUqV@3aPu3uy`t--Q>zO(=r2Cvm2;!jqwYv#0TsBvS4x<{EmG$4v-X zpPPK)5@!f^=I7xMkjzqa1Ctq*wc}AzC26()n102-*VQ}REHUjyuq*-ZX>@`^wG)%5 z=VH?qY6pz~B$U_9=45-8xiik<5@DYM7Lv>jB5(wbI5U_(BqsPGYK5^RkelxT`uqC& zl#kzjKi*W{y$V_Q0TqLns1LLc2sn6oJ*4Itvvl<$)E3ittz%v9F#l>8lQLyWg|h|- z@9Vfa>!mq_Fx@(E%E!Sw66he`cA&m{ERMCx7Cy@L=|5q|M7a{#;rrO!`ht8ehCEO6 zWOvn1K@&?xkb(2gYn-)T^D-t#n{|%cb9tX44*ruu(?Q-S63we&M!#mU6B-8I&4ws; zJgGo&AD!-_az*nZ3r;j)e3EZk90zshysP(2{8;TtlOm+;WTvUZTd7;^tuT?-fH~^R zVXw9qPL>61X)tYuHP7!MFb^v~wmm0S+}BF@u2H()>!Z*VyO^ zYRC1ux2XU4EVQzy>9^SE6Ilw{(u_k*DSDLlP={pwIR9fe!;Yj>2J4{7J|CkcN(58W z*Ur{j&uZkRAB0;Xy?_6?kQju1x$J@90t5Of(W7jfH=N6pu(!&&B0@OY$?BBV@0@dQ zJ}=XPmq&4paB0s%NL$a7boqgP;#}f>UwC9U?Mf$6Q+obki&3AKKLpJ&S_br2&4RFy z2Gw+owZe$}WIKJpdjwSnh_c7uFCzJwmm*ArQt2XCe8!y|>8B~L6_51X`r73ER1RZ# ze?qPC(2PU)d1%ZC_G}-4e>1n@vzU1xtEjbC(=iOAb8m?jz7FYqbE;mqXrWz|g{<3! zIvc6~-#<$?U*blt+w=HWW_7D{9)Q)X$tT%Jg48$p>l((0fvg@JkstnO5>O&YkJKfz zm<%Y4aP{dyLJwb5A%bP(b5zQX}&{sXWzuv9~3LW4TG?SVUAkDI+3~9LU=^7KV^1j-ireeZZ@uXH*jC$&C1C%;JgC07aQ>FyHQ&0^G=dF%7^1U*|4;#8Cw{CXnn)53e~Ox%}Vd!o>k-O zEKYtm^`CYQqZQ#A7toDgCMoDAG{NB<@8BDC+qFh-wSP7^B`Zcw6B7IO)4l}bhq7gs z;&8#qspmIMJ0>a_!03MH=r3a1Z4Z|ayi7GUqU9dJ?7WOb6$1xgqNOI5B%f)d{e?CI z)t;65q9@`a-u!y<{<1@d`hyyd^s`h2#pyPSZZStQ{oGfBA9RS;H<}LXT|+=B)-`QF zR+ln7B=_O#vXY*&1h%?VX~FHs=RxmpUTQBy7P$DxspP1$?zhcRs)>q9=k}ke$J}DR z?y&CH^wW>!B&KfG@^O$>QBxkZVz?Q`xGX+4Oq4jzgdMj0+r)6l)AcYv;BaFP9Z{J< zd2U*-L(E&>D1P)DG|nu5B}79Fo#wl?npw&c`H?ieqg1EgK9`=W{5ML|u}J26b!P7> z)8`^YD=z0djqK^ikM{o<8a=Dfy9y5%;}uzb%SJ!~u8B?VIBFkdq?u~X*1pzyEb&ws z<#bcmkXz*gtwcLsR-I>(s-kHXGsYH8${Lha&VJ<(B-Ljj6|GNhc6@vrZQr6Rp&!OI zqAh~^^I2Z8q|GI=4>6^z;43}_8@i&V77UG@1XPwTq3f$3``*i9YfBwQA~i3A=~R6O zwHA8WzOw0aT-UgzDPxA8=ZCd;N^6xkq-fMOo>nX?wp-BG%nv_Q-#!wSaTMVcT;uRO zL7(ySL(+|e$r1~9D+W|Sy|#)lTehdkyg>mV`+g{q+Y&I z)qG`6*L1G}11Jhn3c&(nkoj_c4J_5O++@yu=>nTf<1}jPy1A!A;YhuE82bj#u*|rF ztZ>*lR`%^NWF0!{szsJp@JOyjb`U5>gE1YBIpS%KIElTCe{&U2`c|NIZIIO33gs@xE81sFU2sFwIJr98k_7uXcR==0Yt&w|G}R zs;Wl+h4V-F%MVu0n+jc8pm>3c9ll?1rxog0@~<6yCjBc3=a7?qNa*mi z(w7lxlJyxTiB*yVJP)LCzrOG|-%(TBtMf4zR&7p8TajrG6GdM8_Y2@A^V3`~a($k^ zStf@gM|LDqZxOu8+o`Jmc*tCqN8;wawwY~WW5dZE5n9hreR68V!sYPl^mA0J%8g7f zp-hSXmtIxcK>J1O1PeRtzbc!#SYivC*J~_)2zdO}t^_@yvwj3F(Ox2n+9&V!=smAX z8(oIcQo#GPE<#hASj~zdT%W@CM^Xr2fXn<*-}z+l)WgPsZ4)UrtQoKA7ddu%&Y+=A znszMDt`i=S)wgA(@;6(JPxVm|pm_n4KUGe%1-fi|S|MNAldi7mr_KM&9{ropND+U% z&tdSN7bJejkwzP&)|UM?Z{=+xk*@T&e0|zSRMmuR+ zUq_?lb(v~Aj`N?n8GsLfKAvSY+)s=H7Qb}!!x>7YC9^}n7`qYg8Cl$xO0J-^W`3{ z;U@O9x0!>^yXq5C-1NoaLoScYvW=Zw;ufFB`YVT|<2M3qm);O;e`|%gy>x_z`;Vzb z@g4(V3Cjn)xMa5>ZZ~u_=y+%NB%W93U$N!4+78mrjDp}ge=p(pTgG3~RXw>=fAKdAdZq8yZ=2M?LhbQs1-gTqu7@Eg`3#lU##6LA*k3Hd%=XxBa|ic z%~sgz;8iW(=K^?o%iis7KX*SY?n$99s;Kr+AT&OaStXR5EY;N0@Gp07w+j?L-q2;n z%Zjw^X*U|V>lKvH`_Q+#K9Sz4wOjkqsXKI9;Y{-`qpQjyAFm6SQ4-%nuhpR*2@$DN zW2l6fl{opuPW@*)xein6_;=DmLZY(2xWbON@cY31nyi?V0yWsZ%55HvM{@7eoFi=V z71(O7>XG&Mhnc}x6a5;gn=W$lFk)e2fyyKGd{JM!;UhBdpIo1g5ybsng+FGui}3Eb zy~0M>3c9}{lJ?zvYT@>fonckF^uJv_87_FdIg?U3PKrCBYinhtuQBGY7W#yLpPy&U z7NN0h!#;e!&UHb;Ca6A$w$bz2eOIjHZ}uj?qp7cScuL<#9X@k(SK5b$qnX?85Ea`& z$c)aA5Z;)smpga8Fr;m(*TF4uB2Sfu%Ga5MRn+SCOELh~m43h4W%f=bQ+qyIF^{u1 z5_d(swDA>uuWML>bw>sO32Un|V1eon=(WB|IC$IaKh^~r(Pf5Lg5mD1;X{+(4!Oai(A*uKqSs=0yK0PB%0zEicr^16V-8pK_o* zS&kG}iRG1}ac1pftL>6y&e9l-BDu~bTq)o#Gm9$aOY{*8UMJNz_fB;ch5`QB)4ePHhw2LZD%pe1cTN2vdS~oDJ%y zf>N&}H!1FM4Qj?et=BGyssc8!tjT3|&V@~CrUs$c67KSAUwBR^ya!|eGhev6fPDpK zcKZdl;#zj+Lxj!sW{plhpP~Af42L}A%f)bQD=&0+LZ|tI)0wMLBECMWGUxQgZ)`+v`$=7i9Cv>Z$cbcz3oO~* zdnNa|v12g)qsFZC3g|R!nd?vU3C};K_5HQZV?biYELfjJ6CW4#ba&L)e0^3%e%}xp zk>Y#R=%iPy-dsZM8 ztNo0W=z;p;*sD~WbwmD<@-j6(YB*E`LNqVcspq7RI1 zwACj>&(pd>2oo&Y*yT;?aOl8mmHAYNMT%@1I@2&UqPL3fB5k)#+1{9fZ-7CTi~kxjuBL$Jj``$?e~*;`!OVX7b_wPo~;D4+ozplv(mlt zRAl|t@y0p01N)YtgW{ZH z8*eGyij*9BaCvoFZB(qN;-A9@(iHC`$2oo_Q4mBHeXaquD4Pwhg>NIp2Z;7>(>*eV zq1`vU5=vdK3J|i|vTM~@ds86ngGHOwJF_O1e9smuj}$#)28UsZ}qEAigKeOPl+E9G~K`yOJR9k(#3{riGdT{Lk50kR_$b9T!WBN zw+OU$Lr@~AX=8UFET2C}9lm$4)Gs7`_-nD6IPQz<5646_=V9(+P73c(h1LWYE?1RS zR6nHyzL^r5tg~{vY~W~$%I#WnTXCUaeJ|Rc;jl#_z65~q4wFQ&kJF!(4f#E<;#!>; z41CR`-S|HM`alK02_Hwdw~WsrKS?)+q~>}mOxQ3*HjgGOA<0R9-_>K=eH8~u-_;YK z)cP|3@r0%}TS>22Iuda2T-Sk0SmT7QzL04fX|(i|y7;IIn#4Uz!`e@N2oQIdjMZ0T z_c1@1BUg_o(i6N|=>-s0fUaJOL8vl)Vb|WhR&tl78h>G7A@O3;(GxUmi~O9_Pj=FA zYPKY)Uu*_4It)#5f~EH243(MY z*C~BV9%wl*UFoE>$kR+?=;lECz367P$+6|`h^)K%iB;IFBR`wF6YUg}A)XlE4OEmQ zy8~^+8!jgxKfaiL@HDx(s`ctRE)Qfomxe(%AN)jk4Hvv7WB?MOv{8R0D3vbs95#h+ zP5D=ne>j7EDX6g#w<`1z-F#Wec%CQ1aLM<^SEeN)!uTODoYq%|M|marNcei;G{85I zK`R}NKnCSn(W09*EY+(+yta;FOTKKT z96SclZl0qis2Do$kZX%L$h2@+IL1A9ZqKpho-ua=FBLzixsEf|axgk8L62AWC54L-x7a_Lu^P8l ziM#2a0NLL&A+M7@`dUNlrQ_*-3Y{LAg{t>@WoZOR!m%jL#21J5$JVb7NAe(adRlL- zgF`x2Q7&G~BQwf4PJ_Cc>4?_!Uace0GN)yUFw-FU)SGNG(@oz?QWh;!rYsXo&%CEP z49!cVLw+syjD07o!t9XidhTjFNaz=8jOYMs=W^AZ2^xZsdq~J=pAJWqw1{4>FQaYO zdc+A0Yx+@{Wyk4dV$T_ah&U8^MI@xjh_=Mkd#$etdB$?0mtP;-#$*F&;cHUR?m9iK z5|PKu7PY?pIz8=YelbK!>NvDBk`gl`<_1CYMDkAa8drGa3y12?B7duy$ta_Q`G^ps zE@MsXW;$_Qru8z@qxXU4m6owCb6Q?nPE?r*OB#ZfIlX^0ACMz)W?dh~5kYP`Qsmll z&$^G`AZxyw;~e)q*kVn`v+T0cY#jx**GrbL=vRuaF?>m<1;?R43}Tkyv~_D4P8Uyv z0b)ajq#45;3P?#^5c#o5$LTy2lKPnhP16&RUD?z#HZeu{H1+1tB0VJoT9u1Q{IRLU zN%j*^-MNkn_Hv^puQvO*Lnbwma?#<#Zt4eRf#XP3f*ribBa($ z-S_B4*CcG*8H;|gSts&~;xZx-ZfgzN!Y5jpE^5*D z7XuReYOF4k<7OSJGmzhg+jm{_da1&dJsgMmxvJHUdvAEHJvn0caF}4r&5QlQmb;e- zi!xZi>zD=!!(}mt!q58k}nU7{{RJSh+j-jmc|A0GDl;i^|HT$*sp0^h{9^QoDvz4 z1-!LHZA>)U-DxdlODl|25+!NyNY6G?ibY?O@U;GBJe?T1tC%F;XEC1*^Zq1YJ__W_* zv-!=vDHC}LBwt1giE%)?L@vmX1-AfYmRaQ+;OiB-WDDGH#-^I6IUWs(Y@$Iu z{6TB#K1AVV&#&*);hS&Ux$^`RNQ2oz6DcIl>5Ffg2u!kqBvDaf09!aRqNa^V43e=D zJQy8&G#(`~!!*LFI~b%-%7+po=yPZ%v{;rY)6fMhWy)=3WhpO!WQA##7m$2`4oAy{ zx=~$i6;m%i`n6_UuQ;-D5n3#AH6KODap5qTIWjq<=Xhw&VJkQ6Y3I(XPTcd%GYg|H z&s-@yuM*DM$NNCnxiX8Xb)QhTAP@`n%@g0a1HT3eq{1P?`gpihFBT+S!#j0mqlQ61 z@fdl4M2jkQSROIUB;4t>mf4LsbUpxDW8QiD4BEsv{zxvJeAJeAT226|%iz*DjJKO7 z7$H?1Hc{3zA7CfSnBZ;q!YE7gekAUVlbp56%<%MRL1I`c)fsYx;;;;riRwTcVVg65m1(Mex( zluFyk9NOA7WI_+=n7V+eC$vx0qURj!l@BeqI9^JHn$X3Nm!B;=GlvcPBqp?-XJ~;( zi}mDk18)sU`sh86J~~+m%Iib`W3IzS!`+ZPD6z87d84sdZ_bGkcH)j5H_SqTejqH< z_w}@Wy1cW9HbmxRW8~P)CRc~k8*D>F2-KaVq5a}%?jkw+&`e7h#{@{4G(AT1Ow%w{ zN)V=AG#z8AJ}o0K%ZOgnw5eV+URpkkbVOR7Ec%L+GD(!PB^_Zr56nD9e%zKv$O+3? z{7~>5E1?KEj&qkJon_H_2QmykS|0A$u>(oFAnpN!5(=GKZX+7olz3c9fPA(yBRC~V z?VskJ7AKZ1TFVF|TI^tH)f#ybhFe;G+vKFk7G{}1vO-8ZCS+1KQx3_qF#Ksb(F!oF zOc>F%?Oq%Al$HZsw!sWX%Z}y|AV*?X$Z9utGIHNCkza?Z8pz*&XM~EgD%;BzSOMrx zOFY*dZnlvlW*8OM(rL(vJX3}iK>Yw(BWG;pA)uXv#xXu8@wg>CC%OUonR=R|QvjfC zW+{-e5N7&3*%=LH+1bKV-APzl)@IoYqYSvk-z+DZkFs?d9SQ}Co2i$ zqDrcr)t!m} ziOw|JHf?&bti-w}w8M>z9j2tuXj(d4bZsucMQz3!BBevm?CcY!9GVUdLpm#2mk|@f zlpUSN>$s54T3t4A`4QQ1K24ozCnV|A<(9l~!*-r17b3@$B}jS2Yw#HPZi_?i1C5`U z2u0$z-OIcX1Ts%<|LtKMEfcK5IQJZF?%8TCu?jWbTEK%3J~+YM>Q?z6yHM}_Ic{wp z5(1~;GQ-^rquubRHYKryrN>iVNevHWmbCS`DIY;dhmjE>=8M}lAqI@auWZCb!UGAL zAYl?N9^qn;2pGCBgRl~-8`@;|a-IfKF06-2Awhg3934VwrY&rh0kq0kSjs|JDq)(o zN$hdiQn_XxNnARyjHMAe!Kx5@m@vykM?#2Iu|4FsmKEczyA6$_>2F2P@ry63}L1lN@)$P zLp_4FFJl@uaVF5`?9ChCd)|FJOihiqeKFY9XE(o42t?YwGS{rva%^Fp-iJ1(el6qx zk&TLQZ^j|ExueC(5!sdz$Qti-#g>O28Vwx39IjaD3C->zh8O-kFFw~{TR$T#`GHze zAq=Bev($0axu<1o5j1WT*7I5`YI=q~aonU{Ky0nFb1rI$>2<&3wMOW<`E!gFCTScn z?79)b2(rP1I{)Nn{*J%$+6fg7+hjNn!;w`KQ~uHD_L z$I-pzrtQz|k30JWh}2%TwLigl7lkL^qOj;lE`1)7V59VP(6hQg6I%`u2VZQ zf_$nc?7N(EbMlipIfk}8S(>Q_Q7V;U_fvC=;=~LNhzeE55q8*ZrvfMfbVF7|<5M`Q z8;{Z>3Ll9UQInnQXRWFOJ%SxIQ3mpmRHGdzFtjU<9-D!yE^nEdP^XzlCoCOls8CHJ zt|ZWQ7>95~V6^p<(o-g?;)VIZFisRFY~x0o%0zjTpBgHyQ>W%+>wO|I(W+H!$q8Dt zob^33+%aqbma*sbJ~_z=UxJ(~e`>ai?wL-LnsajPOhttIKK1{luv(_&T*tX**)BS? zR-IXP_obz!`uKY0poVLZu1IR^AhL*cnrN(KVLm|17X~Ft4uS50DJMRo8 zD$dhEoi{wu`cuS-kjKy(f@Y6q$hzuCeh|7V3rl1k(c!fD#S<;lik9Q)bENtxj%nCI zk5HrVys54ucdzBH{bKVE`SdrA2{Q_`6A14R*7|9037?2ltycNF-mynaPfx~X86akj zu;m+?T4Ga&ASMzKZus8j9{nPXTV|Cw3{$`B-gLRR_RUu$bgfoP;vc%W)VS%r>>wSev0w=c@WS8>z~UawoXPDpc&YRNR^c}bqH z8<`L=qRcpn)-Q&o*C%TyS=neGql^s@#$7i#p~5RUfe$LI{LE^ukJ6$35_9tZ4MF%F zcGh|$Ees!}ug{6wbu_GHq(@@i5gLvg)BVYLd~_`9rdXgyXRtS3cCnB@I59I9y1UXn z;O*gM=OV|C@~Dq83di&@%`NKpYo%+2ZalwQh66SSuz zk32l8RgnZ14JYfEsBjRKX(SKzT0b$7Xi)>iOE!oO<<@2jAtp0xen=QPL^JJcO`oR? z8x*EY$d%mD&JTTKwKyy{a%arrBFio&8Ejm`gj;U8WzfUaR}Bjb3yGH#S~hY{UU*G1 z@e54GTTM#cB9}z7mWwroGOV?~A#)=uEaww6; z<^Fqd|M0G7A`Tmg62K)QfV8+%SgGzdeE;F9nVY;&VBTk8ew<{j&ZCt4l|Y#i!zqVmycL0(Y#$-+m&K3WEm#rEVl;i76# z;Is!NN9u-0>Q2tp%TCG87LK~nG}4Zy97A?|)G7v+7n-D#Sg?zP?POZ8r62nl$+Y=J z6&s^zrIZ0T;b^ccoghMnB0n|bWJ?`{yt%3C*~4%fXrAM{Z$D+crB zr!7F!zZd;Fc2F8|I*u~2goPe;u=%CQ=$1a~B2$Nb#2*DVZKbVzeEN!?7tCeL((6n8D#fn@nXiVRaurwT1s$W~@CiOB{}WShi<%V7865 zmy7&{LaJ@{G7KYXAqj30n|{F~87iFXy2>wF49rHZ+(-{BY3oI< zP!3Naa$)FhBe66xO%SEwuG3DCTC{RyDuIy>Q=#*2sSJQDnx@N2&~l4bZq)e$635mt z%8himO$RW@H^rTkGU7Chz8ADiiH^21%>~FsL55biH0Fq46ep zZiz>)5He*tFIv8qNA)LVLd!{$S(}N3=#WScuj%r-x=f<96Xq4F$&``^ z9S4qSYVy#PPNY}tZcMn+$F^acbgVb2#ORktA}o-hk{C{x52{A>)iOxD#IQmqocyQG z^*DT8ju=;8Ydv(@R7WjKr$z4V#94ePCOF+t{?f&ZfO73x7&Sb(;Vsr7p81@M{ zA>^X|ISgCwS?2gG7rAz*x{jNn)>DhF=3g=e1`a*5keq8WR{nHy3JJp^LZRV>MJ_~P zQ(JPHptLMaG7u3esXV}(fD?sDY)nCPx8*4z%SZ=E`mruFElO|@jX@{+0*yn{Hozj0 zv3^u0y&t;lbVX}&rjxQ_q%p}W;b#)F)4t?^u7GsfiHT=A93k+y999G2GXMSWcqvw6 z169b`EF-d!bV2OH14SnE#Z4h4GY*jOZEg{?d4ygkNG3%lH7kITmPm%8&8_CluavYQpf-6nowd7Za%t0}7dNN#4odW%=*p4mZt7|3WTI1Ea{<;*&_8lt zDglw_3{{ck6H2U3rB`4D@Z59HakT{<)5{NptsuO{qc)ASFSJCrx(ft3C+_q`r!yDh zG{^!@(N=g$kFeF5)Je+?B2$V@7rh;yP2X4tNZW6)rE?I;h|5DH(}?Z!p)?nwr4}&F z&|V*!Dcq=R>^enxjY+&!=H{1?m9;GF;JC(2Xh;%VY-lA7rUQxQ4 z_m1xyBI8^n^NTbu^wx@x>RXy}Z=Eo9OTxAZ)Erb1ZwRX@hn$ z8u|_D`Ru1v+D{8N%`G*reYkgX(=GH)qGYsr3r12yA8s zI($i%$O0=o;3*COH<(!gE9uQf042j*%SI;nA$O)=bA%ndtikExA z6c{`x=}=-Jq{->HY)HP)ZDY)7{KySRyaXxJvE=5MX_E*|66`R(-KH1NJd)v$w4!K> zPj#~BZPSwu)r{rSap)Zgo*Q`tvn+KuL#IT!Tk=oG@ME~ZNS}@{Y9n()UvAUeOoxu@ zS!rbDBDB-{tH5Eo?xE^r86$dzZH_(Upf9mPxm=FT9n|DO#!Dy8qoIs+oVFlg+MX55 zkiJBRq!Hgt(ub~3k`6cAwJ9n!UYf2H zrn|+YMG@zI#|g>K{gIs4HC<5v<&7igSl!ortKa&bZE3*FOARpwK3O$ zl6;`l7mj?8cm>>=zqFgwRR&r7)bzOVLR#Uq98aK!NF*JCGpOtcha zSYqN_>tm*EgBecCiQGg#anoSx)(axpf~L{xtnED=?bbSqpl)WC`x!)P6%^Z5SR zc2&nucht2j3|zYdXgLNrWM3B*xNHHywuhQGFu+G`R`%QX#H5 zkc+Ym&dHI(DDpGQ;AAAu`u>Dct4Xe~z3Q$*he~g%J6r0tpxSg8D{&$8QHP<|CH&7( z%@NNXfwLcY?6D0`InGNzvVD6K3Uq?m=b{O@Ct|dmQ#sIs9ATtuu7=1{!rQL9jwv_N zGOtA!w@zWZfyU`258d2sa?;%6rmdZ+fknP{Uf4q6(#My!Dpc|3w-IUF9 zE?K)$2By+zITd~W`e?V2bQN(-&hWZ<+`6~ zg#23W5ui_~rHK5&*LrA*flB3UB=3?WbUY%lqaU7|M=r`L4E^bNDqQrNIec{U{{5?R zFHZ-=Ew0F(t6E;sdAHUX#%XJL5;?YC0Ld0$r%EL2j~n?=!v;F&1s&XJp14t7yqm6F z%A#8t>pXNE)81n`0G;#Ni!MOZGtva;d_PwN%i=JRf6IN%le-%FwgNV*%bM=Fjx)ka zrLw>u*XEe$TK}{g)ayX~wHzH6%{F-Md9LGmk8gnjHpDL|j*2nP6aKbeG?FGkep-%x z`6GWVOCL>QRG<10ktHe@64z>0=tOKJADrgTlmYEz2}GYbpU|Ded0LqovdwsH`e;8Y zLzfGU7mdd0c%O6}OGTmdp*nT?QJu{D4DA)cio*{A$jV`UkZoq`a*!XnyKbh1!?MQf zgnJ%+bkb?mOLa-u_~X%foJi98y5$$TA+@`%$EmyaL)2A#uH2TmU%BU=M<=0x31NG& zDKSH5B5ETaT<-&a+C3ry-j6Iz%F$3vo~9p$AtVDj{Z9GlAdBk3ZMxC2GAlze5~Z!e zq1RM)CsL;5Y0B$?%+u1g=2h!Nb*Jf?6%F-EsrYh1$@79NTof(coLk?3MvgXjfg4@- zJzB;M*C2&KVq{6IuRBb(x=ZnmYMs}u*V6KCaj!tL*_5q+tQ?a5rIE&Zd03Y1ec!%) z&W$(TSel-mPAP!%YAxjwXg)y-zktWK)VU@-#P`akP znJ+WX`kIq;X?ZbPj;2hDzTF_Geo>yx@}Om((8ZW=rP4O@f-ojECGz2h3!t$mU9%jt zPPCk;4!Ob2Fs?qdJ`C}ykZN`p%Th49@je(l{P4pgb=d$Ysov|5k8rX@qtRe!SC>~% zt+bM>)$v+sLfk;Qm7&q7GuJDtc0a~J8F8Qrc$n7CojaEx1Musj+;ldMj5w?w$<=zc zLcJ8}|5~IR_WZu4^uVxq9=gfyfW}LSc%gsUa8JsmL{8-;bpV*!yjkqu|A?%+Ylrvl z-P@`EJMm)cYnQC8VhNizT(MJUo9S2KDuguR zHIE|2pXS3POPjkbZa2L<<%jy2CI!(UU0X$yc3*Aj+R`)QtqNH>u9IoSQj7@z2;J3$M#1J1NpQi{jJ>WWbeI-aM}5 z*wRb5>z|PiGd_)HhBM{b(g92U|SY$&=8epW8mcA{#NcnMSCwY;gKXeEuI-~`VUS4464L!)Tjpyena)sq+%NtLT z574=6&HJ?mEr|veJZdrI-Aj4yWvtF;EvjBMvt?1#o zM%-YH2u{btFc{WsAlFG;cPZPD$T{JA<$Cxq#A#?fiOz|%wOf>atfvl#`b$5Nl%Wxp z2T8+Br-Gf#?dbt6H#}~-Vbt1SIB?*AbHfccm@PT;MK)Vz%;GLm+#$seQqnY^E0H1- z(P{j;EiF@jGYmkoWe)&7%}^Cx!kX!KlEtC3&070&LuXt0F-ts=BCp#$H+8nT*-TI? z4u`v3GFaanhOJuH&3_4k{95i!wZcOGwI14J256UYMjcivSK3Kg{$vAG@W`(oyM+dL z^uu#rZW-jYfA|yXmrQH4?*~DpHOd=WE{|kV`Q{Obp8C9QxFL9~Yci}(r`=B^@~Xr0 zD7+dW6zLV0%k$4$n<#ts?BUVKlEm_|ZgM?wy0$rx)e7S<+HM#-X-d-H988 zqw+NLh!?P=#iP+ULhGi}HPY5;ak4rjKs%AqRVowdlJZF0Gg4M`31dz=r>Pw376T08 z*Lv#kM1I_gj^uEv56Lsv=@|DdaZN`O8TNL7lzUF&fB~9+^2?1h@xexdhd!Zf?t`=~ z_lO)jRGT78$Aa2jeSlvyHPzruwonn(YRgZa3p@|q!mvRrp1Q79wY-{xFUM@$!ki*V#K%8Do5IZY~`2n;d%`w+^F~{}}O8ap_P*1isMGutgy8 z^lFFBd{5DTHwyqgttooi|+tx9z$qu@WZ!UVH60`^(QA6VOT6idi*yC2^!nsqehyB~&Bmr2nYBouAL{8iNZ6z$9=YrEk^IqJ9;J?o>`{lX zgm|Qb2QR;N3_kX$zve&pqu&=-h_qadrI$^T*JP~D_51*OZ=!obSK4G+CYg-J%;!SK zr8<#tK*N*!n+t?1xhUwLi!h!C`A=HUvE`m|w2Z6~0(WqqG_2{?=>n3k>(a% zBMGsYnR#*i)Qlv;h!|z?fVs*jp!ih&!=0Vw_YuQNo)JF`C!fkt9uMQGgru9NK%dulK;^fI$h=Z$1aben+f~q5=n@%6EnJCE* z+fWxz$BWK!QXR$(mr)PnNa`2i^6CLQabiv!IX1&KPLH=$q-mt7!;$+)zrcj;oxR5orq#Z@1+CL17N4$2`V%Om@h}~T=H8mFN ztIbNKQboxK$r$6g2|2=JTwgP(Gp38~t{2MIY3XzAk2;H3UYM`xiE%(;hvX;~o^pS9 z+WoSen?Fc7PfHUl?(~b#OfMf~TvOh0WZ%F5IjrY&At$KSzl7gExqBj0M{FAE_4*<& z%NCLPRn#%GGc18Vr+%RuK+8S-XUf2|pg;`Wf&*xECh88q=rcX;P>XEy@e%_xUgTz; z>yAzxM`$^;BalvGzvDY@l{V)|fo2USGG8F^scYWARyGgMm5n|6h3Dw!DT2WlKKyAn zX;k20+%Q~Z776uq}yWkv3fUe+LS-*h93+$&nW04qId?x@{+@54T*&dMceW3GsX zQ)lLp&OO5M+zkhaUpvqbkFh^vKBff)VvyE=8G$u&2S$S3`0j= z7^5Xv6kijAa@1*PH%ttt*1uWWyX6m;Pf+b#OdgYEEMUaQp^PY@Tck{MEksKXeI1$q zAsqIr?a024qmCmJBlz&?sr`Z;uYTiJapl&FVSaudw0x>pC@0*uPRpZZY;3C+$Bq2r z#3m+2SY#(>(5eLWc6dyeGnPB)pw?xl%T?b)Qx@oWW}GMu^FBwqdOA2=LV;u_9+jj! zR+)Y-@_aicatFt5cZTCQ16x`WYy?vr{< zedBYTPPJO)B<(m~w34LdLyk%xOI{P@LOQ_B(Jtn!ULMKgwmjrHQU>ADYNnrf*x1Nm zwOB6lYq{h2P}YCXHpUxtsmK=f{QmuqjXPsdYSbN8u2h6>sq3ex3?tB}*BK{%;lP>D zU4^Z1fo?S-1LX^6PP@~5)W{Ji#NtFXl5X=$4yV6YM~*uuhsC`*E_EiI{*cWnt; zN9l$JwwfetyjHDy?OwrA3|w6!`G-sWhS!eU2ujUUuS0=0;EI{is+n{tT8-FFJ2EoD z@u$P-c%EfxT>aB>JNcPmI$fvvMS7%$lY|v$y*>LCTQzxy5kp?H% zbcIxtRC_98nvUs4?s;2StPJUMfn{SWyDn+x0_34aGuwLZE5l)(kt?}#-oJTer3waz z8oKM(pF~ndg2%==9?|@Qt?=;Ix>ol2*e#3`##>VHP$nKn08VsS8S#vTyp1(!Bc(68 zat4Suw@|H83`4`AT{W_;#%`o#sl%kLA0ErnNBxVg!3<~mMY>b>c>Q=&CY49^qemle z+KuLq>-bT++|Y{~`2+1oWt!!o^Pt`Jdxhy4>FE3=HXYIFSjrh}`84t%NUJ`Mhpl(? zr@GtjBP}1AclrM2)~}@&#}Zk{JzKoxBHu|&qSqN$Q}4h1_DoCso(YIPq1FBLjbUYE3^X3iNlS?qT3Q&x zU58DKzt*Z4=X4}bLS-UHP}!yq#GM)8!5HVQVUERz6xkim$;cFQe7Z;}<5)h?%PgNX zovB=wqIX+5(7I2F`$+T^hCXfmCSa7amM;l@0dj)p_Rn7-R;R6(-)0@Sia?NUrs+!7 zdUx*Jxg^COIdI^Mo0il_HXNT`4b$Verb>w2A>FSX9Af;Ersy!!b2ari=kRLGZc z!Ev08V30xzM2bJ{(8Lx!ezJF3kP5(_UK?3JJBgZl2%8R;G}1aM1Zrt>nlHmI{EhG2ADj+v< z=Qv*{M|Ufm*bmJFY+F)#tWv-(HMo z?*U_DV@GFaXV0e{b7m(SwfCBxsS)C#fPQNonKY73Or$Gh;YgyytK zDy7YvtKJ-x#>PgxMx(C!Cg%CPU`uY`IF)*{`Fib+J9g&QQ$GyAO0-t1%?3d*q1`%- zQn8Jo!E<6Ormf>n&?7w(<`d5tj;RSp%QnJbJT=r%x-l|D(YDBynU3}OjVh)@{z#h4 zGExXgeiQQrz*32r<%J;Q&-Ktr=%l|PPv@6j>vR2eVU{h1XNEp#esdyQKcpb`NUMzG zSKv|kOy%|t;!M*_MucYm#E!^~-0?Yvj95w_N1da`!|d!{-SsPTlLY^C%v($s)t+XgHCk1Mw54T)b{H8d!Nb zG#u%0)E$tCG@`u3@=Uo#I7rJQk-aSRQnqP2Y8Yi@mD|rfG}Dylua?W@+~<;H<)YQ% zu-wSK-oOm@pIlhumEcUnlTSWbzV8cPSl4tX9C-EBvfOu`QqZ@ zYx2wQ2&@81s(}i9qm-c&b`Z#~R*f8c=!rktS40^BZRAsVA2 zNpUpd%f;we9pS=Rav1uL%7E&q{6dn)aHE8a7bW?RMzTy^R&mFoxU>RUSZMhX*{UJ| zJ$?w3+}5z6h1iYCkKLsA)8BewVc|r%cJ8_7j*Uow9i4_b_R>=A6i&&tR2*$_EM%10 z%5O^~B6J&}rIB2ZD)x4w0%QZCP*yKY_C8yoYcs#Om=T#-5|VaKZp z(*|twPmCc)A}W{rdXQ)(0${IXD3(REJ<95&gE_W7hLQJv^h~W7JCtWP`JMcQXe|XD z)M_GFTwL;BfBpE<+}xbNXRT5O=kB}jcJI9N&fG~}l(LcZYH?Vuqh+$Jgp9-EAw#^= z8Jjk}&iO*f;#DO0Xlcr7Fd}NTrR2-DU_V!tfeA(Y;g>)dM#>voV%3Ex>_WSo>1u`7 z7Q!?Yp)W)_8DUYDT7-v&i*%@RL%v1d`a&ak_T+1Q&F)qDo8^%a%#!-k3KAkrPA&b_9!5@}M|c%3GHplL_X%cf16R;3RLSDtS)`~O<0i@Il(Nl-h z^5Xo^H0E1rAdpBfj?O>LizX7CU#cTL*Tf#joo|&B&BK@wOLyM6Gxe+C&O5Ja$jA#? zzRnNTL#ILY)M-&aT3)8U)Q^^@_S5>)a=|>B_eG~=eIDk+a-eDHI691eMszwDUYDhr zjxJYQI$&A!TIub#o@X6bm!(-|RIa<^1xrVdo`h8d@)-09WC(9V#7p_GNa-C6|b2pM6#x!w_8B&nuwao_+S4ptb(9DN=hBSfdkGy%-LAZ$9R<7lNVd_q1+qwU;C%c@^jJ(F@&Tua$(+qNx`m1)<; z!|k`zfpT;jkuvqeeZRd(tB!#)lw3A zRH)q6ebI7~y0@1Px*Rzob=c6l$H{bn+&f!3?axo^*#OJ^<8At*F47dk_kR1XRb6d0 zaDz;fm~>po8M!V$kYz+8)(5g;IK!;&Eo|SueRbw87idG&iB~KRcZLF&!O60c0CHcc zl>dmC+{`j)b9Ymw9CwQc1qu`>u*#@6vhfkdsjtYW(ay+?+&xzkdxjih4|Rzb1Ye*) zfz^dt^dJKlWsSF772fE7Ih&g7Osx#_ICt6e zpJ}EkuJ*hFdt?_VP@q6x;5A9psh#2JKzlo{<-f>mer6dj;GFkQ(rWYR!tP$6K!H_; zulBa>f7FW~NbV~Q^H4}$mL%Q3eZ})iXY5Fs0tE`J9h9Z@f!F)rkGqVG+DdhZ$z8U- z!8AD3A(eK`8fcmf>aD*SHGCha6k(+3e=3UF<^#uwPC@@G=DiyKR*Ny`r%97ZT{~%}NNbW+^>;2o#d0~<%P@upXz`}x%9i#no znD3YQYHSsn+axB$WGTb4mrILKDO&FZ3KUpvNG{?F{Wmrhe`RR#FBkbOKj8bkI@*7{ zsi`twTrAA)1qu{cUEt;^vKu`9@GC`G#xeOdV$6I@d7AR1lz-l3=W+UjV z@s^8RheFme4CF65R0|X+uojScB4$SJTP|`Vcif1%8uZ_*P&M|I%ma7deYZP4KJEpZ zH@kSfG&iUECZ-w7|IF`-h6Z^VWT8^rM=M;IQhktZg~ z4?p~H>E@ent}(_66H~$bhWzmW$kmLXOCqD@*N#ab{Mq*2C8Nr#|&a_4x5J zd-c`OjUMrb#IDZ~4QZay*Z9M@k34d?isl?BP@uq2B3K581v4|l+j1YxNYJAzNbE?) zl}e>_-g!vSNa79AXy6eEJUk-Ia|DJ>N_z>;JMX;mzWw`~`wBBnfdWH^WpUVOhH>D_ zL~bqjAQ%_;gFbh+Pq4|JENm`$5Dl@7~O{4GhcuAg&`(AW4gua#T9E~ppy;7uD|c zivXT~9yZZ=!sfLOgK@MUImWW@xu<-`9e3ovX<~r_1y&3phh@QxjGMn%CO@0|NS*r= z6Mbh;c5n)RVz8zeG{=k_KFpSu&Pxz^;@Sp6;0O$Fi!(AZ!V3F)fda#Y3CTlQxX4%W zCWg1=e&WOlSigRK{{`dwq6ve>RqoPKS?Pl0JTlTE`lY2MdE5%r4d59-w|0c?_DbO> zlKXPU!L!q4Z}5Ib3gPG^I1RwUHKK|UPz4lTmt3?S`p1Nfhdc&3BR6ut@WKoE$&)Ah zeyL^4o%CkYk^M*X1Z*#{ZmGi+5eh>*d)5sYq z!$qQz^2GS~cudmzI6prx+Vx73xe4vp((%I7Ja(2D=0DWqiC7O=nYoe(Q}erj|9%%u zRwx^T)AG0Jjd|3hYnMp%m=`uVSZQNE>3uQE0caX#d|45>GGDxy!%$#BHZ;p8yuKTn z7_a#{4DSq8#KgqXsxR-jY11Y}E|*+#2@h9;S3pM5rV!*e`JchK%MjFzM0 z%5xy(1-p0ehAhBVdnB80-n<#p1})QsPFL%w_16B8eqg%kylYVT`dsIa%BAu{6BaNM z-s$wUa`fm?XCkbqnVDvYt7LjhaXJ_dNn4jWO3uH`2tUf#~;V5@a4txZQCwMxT&*CLbq36 zeU|;Be-xh6Fx$3;;ZB|m{qXw12a)hDiQKc-);jIzD%NS;x@Q@thvzb_I6S7UznQl=zgDz(+N0*mAr*0IcyRDq5jDrWE zKmnLoSl|#XO>#OQH*%kwo6FQ{-_U4?1`HC9!)2FUCZ2x!X?c^j#cp$Rm&q46OV4DgXx?d)V?rEmam6XTB_+bSCaoB$G)Kl*jCr_fDb4t%p zUoHD&BzIe+$6SV1lvhUryx7JU*d+vFrg2y}V*gBAL(%0t zbx|=i&7cVha)d2k{p5Aeo;_%rPJQ^%WD}V<+8|9fBG3G$<;!$Oy<|r*=Bco`pW#>z zhvDeEe&a0{xwYJ-?U;un7giVevg&gr6Z4veq5jlw_wJ;-CMMevG6I(qmSpIB>QkRe zl-YaV`(9zXnXrY?Zk^&^c_rqryIWeCoroQQws;8~d7{!c^;k}M+4SjDPG-Jzc+fR&+m25?_0+O% zXg9Jv9T5TZ01W?iwid160;e62*rP!Nxx_ndaMxXTl@1*`RDwQ%W|TpgQZ&#sfGuME z!*TEQ+Wet;#5|`*>QBQ(?!_821quuakiW8U(V#i3SLBx9x^?ULHaaw-4-E2iJu(fj z4}S22v70{s)nEP9wsUzDB&yMa% x3cGTF0z-s|pv4;EumS}N6eutVoK9cl{}(G}j>1G~zMB95002ovPDHLkV1gimD+mAp diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@2x.png deleted file mode 100644 index 40c13c07a2f88ccb9d348dfefcfb65f464a86b42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96082 zcmd3NWmg3ny?vmgR!QI^nuEE`5kl+>|xI4k!-GT&nhu}d6A7GG|E4iQl@P3%p zYkIAju2ZLE?>f7qRF!2=k%*8WARtiXWF^%hAfPTFARwO+;NI_iJ7pMozaTox>bXHc z;L-l~fs|9HxqQC~>836t4pB2ja{T@U=8Kq;7z9LpJo2+CECi(Ik({KMrZ?o-3t|w> zh!#Cs@8|^oQ?^18xQSUI6@Bb`l|E6NgMGqkGithdnt7UEKhJO2)T~tz6FfGM&E>#X z5#8AKE)zoY{aINexm4+EN^_Lqhzq-#p3Vx6Jpg7Rp&n}yRb|DO(#Fn;ZIzSqiBUcy zNolLs-Jdb|5GZgV{U&S#|9@PD+r%M45>4L#AVUP*PAFA-bf`8_4>S+7buHf2Z$bE( zvQGwo>Tn_v#3Oq~2K(ZSzmxSJH(DM%Shxg6B#xOy`Vy`_rI_9j7v=&^+3nXFy8K=^E4CWBxfS#gn&(Epr5WCOB+u2ox#hk_zp`&V>2%$~f zBM5l`c@JrZMre@8$XbY1J@eJVDAv(vFt?Ulgt*kDkQ~C8y{>j2;TcE^Fz{8V7uU(TO!#C8@V*?)mz+=XZ?T>KU z-GS+ahMWlJx%oQE!{gCWuL}6F=2OG<4Lb4dGoz9a(sO9^ODjje%0>61mKT({g6Z#& zAp#-mINzXV-inbIB`9vw5zw!MEtq5W)Pfui7=9XlI*#FKI#SOD4O*bZnpRC{~~$&(z{? zs>_VI09>#*JT=kxW&F=nFuQ7$&E1BDQz3gNzg!}BSu;i_53jmw{SRFJ)^@B{ReCNnDw!5Y>_Rr6kh2bmI(N=eK8A158`(_JrG}=9R?{>p{?qnevTVF&43wx}eMe4^_ z%nzO!Mb{W^1VdawW5B{Y_T2wcf%;pbV(Y&y{gPk;$reNjp3RyPa*BfZ28+~p-{47? z2I3jg+niiU1RpM8Nhy1!Y$mpAlv^$PeeW<CYamoS`>s%i0?`_QNa$Hb^Nvw(c#tgMB+2 zR?W-gBMgZQA~1#Ov9Hpp?GWOBMc4asHXd+x%@%cA^}>4;yd+g}CvR?b6(S*Sg@N&6wI%J9qIKVoNc)gVF$Q2%2%e|{#k z%1Y53F_I_uIS0fMyq#^b+k@!53_$Nyit`K<7h)FhUv3Wcpd6Y$Yv1`L0{O26#3uCx zcalF_A%x_q!t99ya-HBxxFRy7o{Qww9vb}|y>V~8DZ$ZQ5Jz@i_?nL-RtX*a8fgP5xU=D3m#TTnyZWXO(6Tgyo%TR6CM6v#4r@_{ZjcLe9d## zk+;+WF+czb^YB6Tk<`zz>yX97o*=zV2d2oR0EUHtx>#Y|dgnak- z!#}?e!|rXO{ier}dMwSR#X9Jed|vJ5HF|06Tfty+sfBl%sk>)T_vUC>$+m?Ma^UmQtfx0~lIK4gvJhLhybt_-oO)O9GKqYFiriHrHN=QHd;#N* zKuVXG#DV$yA1w0{M8dHelyfFIZ6w$!$#B#C>DQy!|MvEkv$h*^^i34@*WlgrBhUpR zVz0+Is?G}=8v`UZ$fJ)G9^D8d@(`@K*JF_Wb9Fd&vtl(6tjp+ql;v~v@t-#_`wJu@ zT9{jgSU+CB#x1pn>$PS2j`%SRFR})rW=&NIq_^dnZFs!B)QakX-4+{;D)nd+y=+52 z;oiIXIkug&2LD4E<}fb5Un6?(u^wD-45vAX)-DVaLA55=Jv}D70Z8?4za_Q_kRD(+ zGw$HzkCiP-SX3Se^^M_cTxJLobn+pY-Z`{vh;B5`H=8SKE2=1|RDI z$*L?Q)ppYp5x|7-8+3-Skl-1Gf$-32Ui}-6?)+bH4ntZRc}W6HcKdi$yePt*Bd$i8cpD|;qz7i#8iJ4XV*MthxR^V zUN(HW>Ep9}UouvkMxy6pkjQ5Q9JUC#>paKqW$dmZ`r9 zqsQU&wl8!NNCTGcfyhVDVKG^B&o{ZLaH0 z&~8fw^?i>ZIObldhXkcbBV|Hhww-S#bwuF)hyV03yvX_8d+cM-D+X^I(ybKU%WKCS z9UYJ!neM_)Qhx1cyoDb+L77kf$N*f|&S7On)4v6-thIq1rM4uLw$y-4wunjEuCLFz z#5K>xeSD^M0;Al&`?}HJBB{>bpi%7b?}4+b95(;zR{zY*68svH7$Z-?DYH81DEXoR z5&=ERQX6^{G$`O%q#O;jo}s6B$PcGA5?V{bdqW_kJ`7<*CfV;J2hS1&@nx}T0M@rC zjOiDw9o|-U6js!Zw<(E>m7U(sFDxvZXlcm2Cl|zfk}(-$z_H3eVf#IYN9};M^X3Td?1d-X)Y5@ioFMK{^!5VV% zzl+le#E?cI|Kx714?UPB2EU!3OCl6!+4N^sVT9zn+l~)ma5tK+_d4!w$8qSQmU+$A zpOZnkc0x7{vreDc%K?SNQQ+Ysb!%2FwLVKF^0q>I6`-$nsepq(s$0KCSepn$`fp*K z7k+;}_-xy@{la~^Xu!PT8}J$F1b)8kl8FH>kYU9)DS}a5dys$cM}5l<<4*?Ib)=m7 zfF$xj0V*WM26Je?e?fGI8ZTeZGyK6+Lw-F=)#<*bh^4sS4*XLp%5bgR`vyg32Y2`8 zJju-(NJ1bH5(S1R!(XOnYHwCm*^S?HeM#wsrw;8Tdwz&pN!-rAcpzS|Gs!}2)h63C z2DcpdXnqjf*-RtxkrQCG&#bAH9#WDS0I>cNU+r-PxY+oE_=H$vp}H&+@Lef$DX?}E zeDf}lw@BDr-ZLf(3jPA{hXAFZACC}Q0<@>9{8=NV<>uA>W@4+A_7`=b!>?(+vslWiHOpb;GGmnO+{lOY4p|dkS9K}#okDIaMXL?#cCi` zob~C~^xERHDI@vf^-}M*NCJ|~Gwjhrep^~6+dOc1JRYoQRjJJ@idk`6El%?WC2!~B z#Kf1^`Oy_VvBrK~dRii`ecgnHC(qTAsCpB#YBAQ%f;zw58wp~^(cLrp`T`Pq*n|uf zsU*V{JDe15I(n+?!Q}ZA>nW7_CNTyuw@v&33a7erZYG98s~FG23vT;D6-l;ZQ)yce z3C85l^BYHfcCmO*lK9SrPPw-=V0b3#{vGdWZ2Sh`&$BPHKcRe=T(QOLaKP1htHcW_ zisU77t3mIhlFfzS!;Bv;cA27&( zOgo?fx?w{Q;8{BAI?zOB3nHPDwf@AN=Emg)av-m~yk%WKX6%QaUr)a(bfw}di|)ZY zYUe?Y>>v%MhhgkbD=|8`&0`OF9F;+Hs8k8%6^Cdo7jP}2qCK*aGQcBk3^QI^xQ3H_ zd_yA&U{Qm4SNX7a_q64PrPs1^9s;NyeIy6)##eVh5;&T6se79 zY&G`U{CU&2k~{-K`|rt|kWq59PRnf{6FzRmUZ2;1$zOew^K+CpR=_X#N98x$g3iD} zV|L8N0JBD$2G9qeg}zJtlxz`Xta-~;D5KALcp}9tZ!m1~?>_kINAA5Jt~~b&;jWW& zdU4yM@6>kUP#5L=_L7(7F#Z#$UUadLJtAC}+NE%_vBe1W7}IL-#;{cL&OX{yY&I-G z{?l>VZ|hURTS#oAIc>Dm;Gr!bAER&g5FLiK*s0|4NlPq@bWRBm^+jE%`By0 zuOlJ!*KCY_FNQE1GCYavQXNjOkFfCtoYov9Bj?*(Ii&M%n!LVUgcGlP_EdlRrHGF} zjFf?+Fp&nGHGw5_J>;REU*zw;0;rSjF@rDM2F`jyt9UlX;htV z+`+XJs=hY`tIb&wqEh;RGTI|h;B61QQ>aqtRP=+?`Ltm?%S65eB8k~0ab^v4e^v?Y9DYWH)*;R@H-ej z0Fl4nsUqu3F8m!Jv77~KN~8F}X+iN9knv!vluc&#{tTY%;bEO3H82aED><9_r&o}Y zRhNkd;b_Y`Yt4TfmS%!2Ivybhc7nn=I>=&KpB!0`<%W9%2v&1-@Hqn| zrlE?hl%JkcPiqCC-+8d8;Poi=(V}g0FDC34D8mmNZ{9dkP64wqMnky_$`D?gpt=Cw zjJTcK__>qoPMgqoiaTgri{oqSX_TD>i)hAwLlAz)3tXjX0ot-%HAZjy!>gG^(WE*B z+%Zo$7}S-c{Mnj)&AJsGT`&;K8g<`7&4Pfx@cDB}alBFRVbkZsoRDvWa&>c4uMU2W z4yO&d145z$RT+@E)4-f9JUpUFpwlVZZIADjxH<)XI5Y8+4O@hGU9QtY9cY;Gt*aZR zR$FtLfwKD4^u>*g=3L;S>m6){Dh1th*b zIj!k*98|LSuR3sIL>lX`PU$fkKQU?^p>6rUbA#6ftAr#MSJ@*tsizU15|m-phvmSR z<=Xink`9ZU*EJD|EnJ0FoPCR13XT(&4xC(e^_kKS?>zCKlcK^HHJo7GDfWJn)heaq z`c1$YSb$1DT8=#W8$35%P)9euyzQ}F0ECc+)L$EZ=?j*b3dz3-30qDK0-e7@6i`2$ z_aXf zMqYcKKiXb)9F2PX1zo^X*U4j*ye=9dz|Pw_g+MGGgv?lM=DJE7uY(>k0=o#dw^NpZ z)C+1f2dtvDpLT1m$MBdI{0Wn-XJ`SJHDEGd8^ei!ZRwg*4d_@u^bPQ3M6#l>BRs(g z`zs|a2)vTxfv>N>XB5+`{Nk5OIaI#ohBokESQk8Vf9CM%ZCSo_IcrP~+_BIQ{QPmL zA)XrJJn#4P-l&Al*S8-}k3cbu30)PjClJQnM= zm+_N(Qr{pbVi?*%OW{?31Kg%mWo$FVd`&_iH6b;sQh-zYPIMlY_^U+)!bH!f~jH$y5Xzmis+xGJ2yQ((Ew?3l9_F4&LYY= zE1XfJBKF`ztd(!IY7r{Ml=pfgHxw7oixIq+#KoU<^V@W7#ch9n1+H8|{-hgNT5MZh zdlPb_N?OI=+w-gGyiPL>$(QqM#Cu6ODL%4t(@8I15=KpYKI!J|IhpSxP_Q71pP)-~ z*cgFWLAS(Afy<~N88>w>w{N3e%sA{6Z2)6BgEBRwjlRY0&Gw`%nTI&f=!mo$5xx=< z*opJ0<(+SRxik9yRp=%b^Zrat{+IH#Ryqp;cQN(LPq!?&+caXiEjzZNlw_6tZHW?( z2wgXx@ZnUNi%gozyxK5-%+SY8Hf%zPwbNPS3W^8|f@^Y@@_^R*=D5nzH(Fx%`xCH$%@uKL5 zDQGpjwHcjMxOI{Um3m8d-9wX(x?41-om0Y`{DW zPaoX~czbD7Lb)4sb?kxRHuS#7e;>s^!3WjTQhdFfmv?!>^Q$=o>Fo*cwms~|fXed# zeY(75Jh_4Ui85Pr)?8b)Tql|lJba;P&s?f;pqjw}bE`uT7AE^c0Jn*Mpl9LZn*M_5u8vXf&uWa8?BQ-bPHLXQ zf>k~TaB|fP!yhG#Gfh?gxtACrUW1>-0Aw+F^FX^F(0AFl9`ED?ZufvS@Nthd;3}q8 z17#0B*~+jMTE2Nue4{epZerxro#frFXg0%C8Qq!R8m#$8g;YTJ3!I9;xQ-x_(h*zvuuKkgGZk zdZpiK_>?_d{zu1oOzb~y)LI#7Tb^^aD;VCj;<&SP#!VK#_dqz&-lWebIg*19IZYnU z@1x#c89G1 zihBwit$B&`quVOC(y+`krXXQ4G=*Op-(FR6vmi?P+dMOIABRfj(Sj%tFYY^~LT$18}CC1gt#Dy`|A3gY+dC zXF}7I9ArzAwXGcsi% zx)&&KnI-Ax>FuuNe{3f@7|W;@7eRXrH~RNr-->$MMaOs^hUwlqtd*l^*dxy*SJ5Rv zD=_QC>@JY_QxnP)F=5}tE0}z(!LGjr#YHFi&)sUK<{n6w^yT1`k!~CwO}N*850Ux@1P4<&G$Gar*~Kdc?=X0dHaN>894 zWI!js?_#B4rwiLGQ#--4^vNT zo>4SI*PVI;c~JH{UA+R!!3p$uA@{YAA&HPu$;`>h4v)#-_ByHsNk>iCD#CoWBY)2+ zj#>7kRqI}R*~S!MdIo5@d@v&nBA~e(&s-Ok8nr4prMz+wUy++eWc@8v25S->}OgU&ayr4CMl*t?&>ArVFq zA2G%4=Asa}x6SP>&$^E*z_?CtgBSL(kJ5m^VHXB5Z0$D=X5x7#8plcC-hl z88gkCK!9;dvEG7!NJ`dd@hMuK!Pg~n$dH}jfKO0#;2?x6U^W&>-Oy$*2N zR;eaz_}woq1V|XCY5B0&ubaJ~VVxIRHUfaj4;qKONuz|fgPKmVp8sNQ4~L4I9<_({ zWn^E1q^ge0G{Ad#9!p8dDTcKT<%x<@) zRI^91Sl=Paz0>F!K@$ciJx)}L#}nbr6=<|%!WJIF{sXHY-I82g5x;8SQ<8w42;}=T z)&+^Jx#-pN&P6bSB~r&J%lM0fKoPlP&+B#k$7=A1rg8l?U5UrYuciwQ4Dxz;J&m4~ z&wjbFye{xQ)m+$t$0R1yLGlw8!oJ(|wOV0W<;0zjBrRXJA>hnW+wMAa0wl|MUS+BD z5R?0Kwtcn-Q@SbM>6-WMR0w*nq5O006W=xs?oaYf*S(aKYH0IVmhalngR5L|$0G7T zfRff{P}}BX`k{v>Bc0S1YZ!BAJ?#Z~H+ZJ`#B&OvQKDR}UBu_*9cmzJ24N(%7OlFl zfb?&5svN##Bcw`#dBrKoc!xPM$eFlNUU~%3$;__bCmYHL?>y^reS!Gk{At~Ki*Z<_ z)!W~uW)g{RSaqcn#Q)P~=mkq79>A}tS?m#b&b$nu!(fY`GEQdqYt>G7#EfNj!4LM1 zl6_$8_qwIPMGhu7sg%Cs+Mxb z1VZ|Csc*sJfb6Z@ECPgt6Rn2)(yLef3NcHz@R9dJp(k@&hLv=R7<(P9`gkiY6uHfO z>5d)V`^4?unL~KLNwQck`9%+K=1m@)r%GYoLS zh;#}^n!(tSX%NnSWlT`AlqMu+*WfM(2>h|>gE*|1@!fWrep!o|{G``zm8N+o{%rTY zXOOPf%3Q#NObfvHzWr_RdYp*Wd(r$Op2ec*7%xnr=8ucIye;)2Y&ti^N5~X4C1SZv zgIfKP+kLtPzibGu{!)k=oTEV$FuG(rvEc`#p9*FUs;9ZYq&X^9kWf=u4|)PKX$Hwk z*4B@d8id9}zUM`ONW%&2ozh7N7{;oGq5*$-RU>Xq2H15pvmW#gC`8vA$$g3Cy;)`X zMu>1sPD)P@U&#J@yxt!V-HP~^xKCr=I=d)X%czjgF$j`IjxsH+p-TX!D8ES43!>53 zA*d_DGAQvb>CF1Ct5;YO!T&|A>z{as*aAvm!W}nhF6Kik8K3u`BCqY2KD&1eV=w1_ zdjYNya=t7;{~&&+edn7VEGp|p2{0DKj7g9a*h{8Nd{FT;DZ~80;toDLF7m|^1V$V*=m8d@dn~&hh5KIj5_AMqYUnDl)j_)B6Ocg6|_-NDPr2u zE^oQmG5M)mV|T@9iyrRo86QFbc*>9bDT$$!={=ASg95hh+*rS^<&}Z@X`cWlwHaO# zL4jSxTf|Q-$DD~Z6J6>usiYU7fr6sgn5yl(= z(oEVb-+fvWvVp7`tiXU`=Oo@ZNNa&+36tbG+(G5Gd9Qp|@QS~1{oV$u^0~7#8h`Zaq3#+NC2LvbvwSe+JvANbQrsn9_q*Xv z@zyc7mx}Dwp6}TfpK3n7?6{^Ngvpx~_+e`qcydq7@-G}tE(|l<$2nPqK}Ijp9<46R z0$wEoEQpEir+fG`MOfD20VCac-V|RSsw?Pw#=2|r;O}mx-h0t=(TSE@))ZIC8pW9%BC}*A^*}V^Zo3Ut_lpsc0tp{ zw!NIY^hO}r5+>uw-jC9jqUg6OU z>fYP(n~uuAQd!CD^U5BUc;` z6D+zEH@Py-4%itD&i_Ffw(2AGDmLP<#lBX*>&h*9Wzl;2X_sR4$Nr{Z)(UE1;ubIW z4(B0A-SCAc%bug6PYC4iNr#w9R#$qF8Tb>U$FqB~VIlUKw4$z&BZ)9E@(=f(BNC4% z5r}PAkFYd{ZWH#SE{J$#s9eDevxr&arH&`5o^sSkciVNu5Zy>4X23~2xi&1eDyyC1 zrg|DdaJOU)4R1hHGNY6wUG|oa`4J?dkaTBf+8+GaZ0atWKnm^d6AI-2?#<=oJh$TS z>T(v8{k639{##)uBRrqSm?iy=>_LO6rqO5KYtw<;e%1Pb?ll!E@`t7(WpKh1-hOF= zy3hFa&1jILz*l2s-|0<%WQUzpoh3Ypq2rI!h>W+4#;=wf?LM<@j@uq5R^+*kbW=Ao z%sneSv@R~H#&U@SgqWd=NQ9{@*5G2|2#fwCSw;q8D)X2li4#hubD7DLs>4)n5` zHwHir*g&TbL^ zk&~|)?-*d?t27F8i{WgnmyqZnu`mDYvI*69Fd<>Gszy?omwC>6)q@5Uh7{C9Zt0PH zIO07@OM{CX65(iwQOwQn4#y&T;IjVfmm@gqJfct+VV}c1SA{YsMGS|y#apD~T*tbo zEAT)&Nzbp;WBN1=#Jmod>t`}Pcm2Iz)U?m?v`{`>rvMmNaMxPfz5|!bZf=kBD&;Y& za{vVNc^KzMV+fY#q6p~Dn6D5LxW$sXq{(q$!-#>Ta%ph8h87T5^^q#4VMgJD`&VE_ zg?B|tdZq5AX9g47~W9*0e1xa=8QLTM~Ow>g8f-x#jf%4L@`$K$t zc&Z-;!xm~yG+ZlGlEcS?Mk9@L)5225zt=r)M8W(vkQp6kGfnpsuPc{QK4ZF~-^WHm zO+&?K=t3I6y(_eFb{vq|Zz@K;iCgbDmu-ZuvvKeXVoa(o0%sX7D@Ujz%<*h?)mL8e zSQayX(KS!d(i*`G1>&y>8){hW@;f*px(Vwx>=XyCAmOal&lrc!zvA2*6`y6H{Z=nL zljJPS8eN?TtHYg!=iYpkh7U|ONjlxkqWO@*x)6tggADjYj8O72bF6`r-Y!%Jjg_)1 zhH&tO9TC`eH>`8b8&QANr<(NzfW2~L+}Gj9Eg_5Jf)3H~Ik+Cg#2G*YQq498x$=-F zs8)%%;O~CF4M_@U;>XZw*uf5|$Fm^!x(ViuS#QblK(vmFjGWeD?NQJa$<+q-xZzkT zY-UC$3ux^Ah`+nR6|F&KGMMir+)4c;Hj&>dKEE)D<$jcj;J&s0`CdPt_1-r)0S_LY z0@CKGi|jrvRgECsLNn*PRMch|V`|?ahq;(srpQ~m5|646vP0OHpBrL?2C?vqERcmrr48iT>(X2Z2zILeoW)Qu!X3Y)=3-vb8<#Uu}~ zlo1;L3(a^^OL>o1DKnc&QmTUO`8}^Mc@>j3rSiOPyM0j%Q%^fLMJOY_)RxMa=-9ak z4cQ^;@q(JxnH(wE=FV9%8fl+gePXKb$T+RRTC8jni?2 zVdk)wj!eVf+Tg{rQ;1$NvdXraT_Kk+!65RvEeorv0OfA$BV#uq6)g@P-iG)9<^8`R z9X`T8gj)hYKWihuPXq4x!fw+-=y`y7B^_bd(@+Szn-Bc$U!~&8N47Mrn0QSZ%!N#G z@ak$lBdVqO&?7d@$s#Sa{7(kqn_$A1Flu$ZCiaY_D?`zQH+Ov|NQ@0i3_1D0xoIw> zLReKfEEIH!rJwrvuEqGjUtvHBW-uf1-W#i7yAb)IA#SriL3i=~{}We&cN7~aW3Jt1ZC@TPCB{IbX16APZ9C*C#nK$|&= zlI0MW1t}fv<@l*m@!cO5{j*}6XH2_||0$ECM#H{3(IU>*)ubChoQg1bWOPm)I0z1_ z?m5~|xuHHN@oqNN43U;kt8%L%h+WDD{u99Hqd|sC9g&DvRUSugJ&Zk2*JpZKh+GuG zpZ4k$8Zmdgy3q|6#d@%o;=JTUDp~$#U_Sx;+4eHb%2GDkU?z&TZ5vLQI*k&b{F_Up zX?H#ZuAF|0RB|MRyyz&6HBFAFvO8W#iq@YhcN1Wb7J-u>?D5n_dBnU2-346X?*Y#< z_JDwmn;anqMrZyzc%}x@ODwvJzY3OEB4=X=FUPE$?}dr^*%h>v9*SJ-DhqE>p3;yQ zg-^T(CkS_2OgM?Pgm!RxoHVXlCBZz3r%h|N`*~Zi^+6>Vg^MN9c7%=wdQkN$1ALSk z*;Jm@-GQ#@0e+|J?%^Tc(x{J6LW`vO7Af21?jcE!kG zrN@q}=CuYbmMaxsRgt3J)z~YpQ*8y#3TU-~6r(Zntt|^MHg8zftl^;>_(#2Awsi$c z=pEm`51G`$=c=Z+csskhslF8MA#1R47esbYEvA~-@mX{pWrwHqr$zWx!6QN>t(2=M zu`=h8z0KD)Wft)H`dhi$;K+3u>3ARYM>I56&|z70fa=w6pkuoS0MC(r@@umjf|N_! z!$Qo|%+f(7yYEEpeTrZ;RN$qeOeWMLw1Ljafg^@vbCYrgtt`h!d)0G^J=@cHL<=2O zMn~jA|4Bd5JNfMa*=<~)RYb%V#^{D^4+*P+UBBJ{T`iiydt2K48SlrTBL!+=_+13$ zg>Qb}$v@E>4>69Z(-iaBx71Yn0yT1bcf(RSP~u~=S-N0@omf$i66S-$6)Etu(I!t3 z;xk7q%m<&Eud+WZVa)6<>)2^7hSxS!?<*YhP3HdDd=#<%0d=F@9h zSN#~yq_XVbX?hH0sI!vYSEG?(OA(r|R*hm08Cx(cBz1ys{Q~c&CnNT8@@gsl$3K}W z=gv&0-DG=Mqos_Zxa@PK9fyI})W=(HV-kSl;90wpzSnbW=P|%?*7etb4Uus`F%w}V zk^Ar1)tBA|w*ae?*~wzSNDZef0)r~L#-uQ_p8>lkk(W~bUEFx2J1wuO6t+ewd~=rG2Z4*JF8<^+FxGCp*yO(W}5kN51Eh&|bP zC*A2&EdyFuYQCQOCrpP;^R3x ztyvN)nD+s$7*{Q_zyuNc{ zBec(ZiO35~bR=F8g@GhGaeQvqku27j>)~!&A;9wT+pAcAY|%%<_WoPnmT`5*U{0$fiwzb=4 z>j$tQIgg63VQXBA-?9TNF@$`6rHOPI&a*g-RXT(|gt_d--K4_X9C7g3)>!APeYf9jS78u0+zoEUefN!T3g(AcwUi6K zLMV}obk;~wp0vMlvbSLN) z%}MhOg>BJ4{+KBRj8hGnWMvEfKyu<-$n(V|v8g;S;%p&i6I=XksPV6RTe7i`D|C&p zEun3uPKF)0nQZr5?FOun($ozDa1y675ZtIMDdx3O{&ve@y#CvMV}5`}!u>tb;Bx0i zk|gT)!-dIpuR&j^oHqthU#fYH8RG33NKyVajZi>FxE>N&n#hKFlugCbxl}ATxa<_qE;F7iNLw{2{F* zdzxiyqAYUS(HvczU-`?-kQXv_lhPla~pfFenM>>k^MC->ndMW5D0)f(4tf6(=d zEjM>7zNBicj0!TQ5O$~;WYYZzw!tIFDG>bj#P=6GnO9OA|8 zM)|z+>;W4g9^1WNir77eHYX@$J+`3$#LK8&z#xC;N+Wohj0 zS}kPf+9-=x8%3zLdgap(i<9Ce(Wtdj1S!*h!p;X+VMsHu^?Y(vcxcXhuK79j>v{yX z1^C);9I*X9QGYK2`b0_i2F`0HtQ;>0Tv8%m4O3Z_G<1Gc`fC)k*52xUYg5BT3SYA~o|| zohhDxipHnZCEHWxNoj{tyfA`~R9j`d`K#9X{5nO3effBY@v#dbKlrpk6zuC6v|=a? zo7!TPRF2K1yPl_3u>!m=XI?!!y)D}KBA7LiA{(Gr0hQI5zdeB(s>A{PboT5LF`Tl= zPRXlc!gWbt+H{Nb?a9sSVl#Qn#W_i{*2WE)u%30|-;#Po)o97BxFM8%b|TVEWc z#$K@E;E0Ngpc8^F%qv#@0*j-kV_uZH98TjqD7c$phK)sRiF$!6_EOPq_DZCNflsNb)q1 zR8G0he#^*8`#cV;gq;mk+xvbTg(x+*2wmUQ^_8*mv)fkiXPo~11sY{#MH39E=;ExF zv?xWj8drrAu^k@E!27Z^Ynnq z69SPUgnM@^*+`s^BYgd(b4^Zp83V^wq+V0XduM0G+3NQCcAF)$6^v4y>K&&KYTc#F zgdgtwpiD8$s__b1LUh(VVl`>HzwuTr#tsX|p4gtqM1JP{uEs0~4gEap)N`@X+I`Tl zIscy?H@5!g(AfkFALgywjW>e}!%ea9#<`CcK_OGMM}!81Ll^|^b)kFt&cjF5akh5` zypW`5NldlMF%juBMEVBoywm#9!^U^MY?1A+M4w89foW33=SNaRw;%UGHM;|Py;Z3| zy}YQSLbfAeEk`-|DPX|2D0nhTE;}E-ypvNC{=a%ruA%hhRw+)AJ?4#<6y^ z`<_Y_xrFLrrfk{8w;_tNao*VNu%>al5!kXm^;hX1OTP7z!(H35dBh7y5V?m882955 zA;Q^0AAAY~1^=?Ou4Tp)Co=cF8!YM6AT89g_*}nN*U%MN{ZlGdXFb8)EJX>qSk}mu2PObp42W6LYVClN{qVrsv4)|W-*{#xjuVOq2)#b-%Xv?2gY6_Y{)4nSM zlt%O>WXt1#)Mq^CyySo_*WJ0>78G981n1*qdO5PWu|{c&;${0!--{csi1xGji>|6g zQUE-5--|b`0hS;qTT@rmm{5gaT&M74JCKZFFh9!hbfsb`ByVmo!|ogvw;TED&vNkA zbt)EI*2=c-H6#2Ok1y)#a9xXcux} zSCn|tKqNdoJX0GR#Lv~Kx_SocL|~qw>TlLO5Gw1!%%1jn{iN%yVnrD&u8TiuZ7u5R zy1tI!YwtXm!N-#eqvb57ruUc}PUu;`p1kWgUxEQ%4a1#nQ)<-6If z?cC`8M_}-wU722${ zvhor3UJN`qkNomf!cFkOuCfN^#tlFCq>OdQX%*FUktDVklCVD)Qk`S%8RItpR#YtZ z#e_YCoLRfkwb{ZZ3_+l>u4pSTJ6{t4TNvi>o^ULjiiz6oVCMQgu?gQTaIy&r!D90x z-$6y&>Fx zD8;VKW;$PCG8nOyJfI8vvDpgR_I*yMy=tP}!X6Fa#(K-+Pr4H#!EmXYDu<>UeINy6 zS5#`07Q@XI9(ZRIWvEZQ6%)PWz-)dzQMDa|Nd(Wd?SOIp(lltPHQHQ60#OCbJ5R>& zOtby^*h$a2yZlnFGx;z1$}K89Kc^hlo!6(%3s&`I-t%M@yc|auaKvMA}f{Y zLKJMZ&MUF6mr>iMJ#+AJhEGjf>;o|W14--+eTK95@;zBUmQtQ#Aiuxm%YtH%3&OiX zR_ix)?R0f(caJLsV9smBDw?2Jw7c#ZwK98IF&>n@-Y+cRWYf6tmSy6b{_JuJVe=g7 zKFQ+wl~YU_?Sv@h9-d5U-};#i6Un>(UeD#vRZY?zXg z3UTkb8jjeU(N|eamsK^G*A@2JXl!mcYZG%%d~2_Sv5msHwaRZx){)MpkUD9Hnm;tI zEJv(wpf!DlqO7Z{O#CcyL%*A@-*R!`xTWRbDKFVEwD@k9Pl)5N#_lb(epn44{=3jj zZ;;nga!Us!^;cE8Bo_qZ@+RflsZ=;yUb+Dck8XWAidQ8iQIUdgXpGgsXsSJ#8o%Z?r)CJmvtP;><_b;Z_bHm+JqSZR*se9bn1YJL z5caThHSD=WUK6BOI(9%Vap{ZRd!@JIyJ)_^=G+c!@p4GXeD_1hYyp9gJRwh<)bEnr z#%AoR_TmyYKU3B=HC2)xhxcK)Y*DQh6$h@(;M3k;>%luZs-IU=!dTiF-EfI*DM%d~fZmhKt9sODAoVuM!V3R)c7;Y_|7 z;pqnJYrM4jR-R~GmB?$jGtIrP30EJ-4P@b1AQntorMr`A|Bv9!F9wmPT3z^+wHrVr zl-wYtuNeX->v#~#j%<#q}oBJXoZMt#9M{Q)jYQ zlWRZtqb0%G%Ph;2&82h2#y*R zs%y^eNF&MN0XFTZF^r`CPk&r=bA{o*^7Y^E<$blV6kCTjE93ejkcIF?Ma86S%U-Pr z5nE$^4dvabtbH5m#PNMib92D(Me`eqHOi@q3&KeLA-UK@1j;~5q${Cqw^Pf!=Nti% z!T%O~?iV#39l{NY|@wR_5Lwj07BbEWTZmi2<0Ed2@rwCIqyDnNPPJY{$c z<2)?y`f&_zNP$KA2T~}3Kq{$o>^ek@hTQVL;{S`m9aDgA5feamfo{n%X zlKhJB^v3jJ*_6!_(Va!HlV_x6P%fMhvza4VPF*CuXU%@UCmN^w+@=JTB!hj;Y1t)!S7AL6#_IFwVg*2??63}r&Dm%rv#47SZXa~}@Y&7zaEh+$e!MoK$nEB#c<%L7 zVD;F&DW9NXu1Gbz-p~=u@ONlt4GDr(L!ehiVhq_RasRwsJNk%bdS(_o{vG@kX1ly6 zh~6fR>?0KUYbX^k(CJ3|O;HxpKMi63XC0!+gk_UgMv_EIHp0iZ|4^`iZ0Dk57_Src zFF`eHR*s-6-f>AYj1!0d$I>;pMc%#rtv0)MGj_9WyG@gAySB~VT$^p%o?M%4Pqy7; zyzTRQuWSB+Ip=%sbKf7G_=ce{+`?n${7E8jz$<^H3dv;e9qU97cGvbV4XziqDh384 z$?1I$IZN!h6}t1j95(@3`xTg3UYD&ZNBvLOI@7_A{a6Ip~DYpy-JG#OlC^dbfL*p*jL zKP{DJsycbsGR4F%g%h>|hNYA=wUQ>}sjJiyMqaJO%G?(bb-T3XnBFsr1Et> zRC!_KN0(i!Wd9!0m5~=B>EcB)$g9W@w%tW6Hf-p81=aB*lh*%UGNO&Subpew);^?> zAP4fD4S19BAW>8+l)0S6ql487G1$YBNtnrWM7&vghCv(_bCg2qJulCK!~wRvYKAU@ zC_0w?HLZ1!k^3|ay=dym$@4-vQK0naKqYipHfy)1s57+AO_22 zDNm!GnGHtM`1j53A&2ZViS<&Y&hF-<&wP^J>Gx&U#B;VWRc>-8hNI4~P+Am(l=LzO zxLDgOK)}lPNSPJNaYp7VTc1odClLxcYHW-F(v06={bzIclzp%K`oqUQW}owKin{=n za-->iqpqXebkD^q{l`iD4^7j=BpmqK+E$UWP)|M`u#77xE&a{_J!b~!JrI1iblZK4 z?$+UhU|lgcurh1n61XR;qMNY3kD;-}_x~nVn2IJvJJ3wn8>lx_rm__u2|vBuGTIz& zQ^}B?yfHV{@CTLUyh3DED&aa0#wZWMFyHWW)x4MzA^@()$}fdWyLwTPF@`L7qTK`C zh+kQvBsU1x1`fU4T9K;gr2Zqo0s4;VA7)1STdRggkt|YIvqMxL_Q_PK4#gY@f%!}5!Hr%!XSBvijQXom-DC7wW7ILQh!ttNu7!YWyYj|UBY{W zu>0~GQg;Hpu9zm;Y+F+O^qER6YRe0~CU*{hwKK7?v{L~U+*mzVnxaRUZ%WZ0sHKZM z;b?!UWwm%ko{JLZx=m0^PN`PxK<1L!6U#)%&S21*QH37IxKQlHk?xe8 z>t(}iQ!jmW8$5&0ktv!)4%-G~QnLC-Jws7X!un~t1;;?h4~gg)VtBrU&gFAkvTg!z z6^RFsmEYbS`!X^1F$n{uQ~|Kb$w>x^{q~Fuxi<7h8tE~WE)gpeT>8jJX7i(Y@bU4= zT)eY?k4oWf7EYwg{gk>O3pK~HqoN^SW6GLD+DJ`fKHEe7!!AZki;(d<=x?Hd7nPU3X$Urm-vS9sMpqGyOQ=?_q2{n@v z-Es6CyI~{~%6xrSnM=Z=j%+lBgahnzqtR(-+f>v;C8m##uldX46u^Yf4O7(U8i5}% zghe9vZA`-60vM1U&zU3rk zxt_cq)qA|1p_3|D_2Y$rflx455`{Rnyqq=#Fl%Y;1W}m?Z0!sTm;BN#c-@X*YwT!* zL0Z@vorhM`bk;*yoE-#K=&zPDs$@n4r;(h2pbHX6l5EL6^5kyV!v_ZA%B#s6!N1s|Ad+1EgcnEedtjc}ic+{%F#cW4H$? zrP!9hW@Vb6R#Y=fsk=-2E}~nwZ@^vFC`0IFc*F_uC@l3o?AgDxjzWJ{_H$lhfaI?l z=F=dtAgSnRob%^dNaDS`-H{jeUcq(KK|ixFN}s7=JUS4*2>R?0XQAz?qnj17_n6u+%>Cg$|3|cbyw{kq|IQFN1P#6b$ zQuYO#yq2F{a3fWo_aMAZMQ1p#FphWA0`Hn2)3_G17^r~ZPJ1vIC)BK%zB<70XPJr9 zR4L>l^ zm|}unMFN15caX3>W@VIgzr-F~wG?N69hC_`hLHdH%Sy@e zmYhgHfHJRNoSFGbM8S)AB@-uUg|N_0;a4!Tg?a*^E&o!~cym7HAWdTy^l$7kX)lC- z*IupVckMH+A5D5Rr0M6aNGCScbRAG`Min2FaLs+V1zR|*g*|c&{B0eP+p|&7-ZDa7$OadeO3NUv!pcs21F^G%je9&ET|U#9yJUa{?~+* z)Mc3PK|yP*43O!H1Fxd7pQ){XJ}yt-u~x&(C-ezwWOrTb>BcxBwqbjKT%O+U)7;Kv z539T&YPP49FnR3u>MVu9AduS8%f1`ky?^EV4{DHKjSUS*T^FN zIZ05kI^y;E`_){-b#Vg6xLu!<1J0i?GR45oigv@@K+N#)@ado0y)$g3xpi=5J7U2B z=+dkUIK10Y%OIIo`LA}NWc#SCK3JJy5IMM1?3Ayk_#Xl;4 z9<u<4F~Ux<10{eD%ph7bfmT|MS+Dh}f1f>3arMOsm&%3EV)SPrG%ChUNq)n9c0Ln=QFgvIJj5G5+4`7uU$rk6Foni94iHA{xXvh$e(Boks}2scEkH z1B!YZ3u8?H>LSyt0O-MYWg#BKYL!md1Km|KVuysP#s9(o>3BGtbBp=2f^p7r7MwIQ z2c5>8<5H(|coc3762ZXY^Unc?HEOQYUh}Xw1mLh2n!>AAjs6E6v~WRH;=%l&2$EW5 zZ@_!~lo^8vU#^p)FWxr+%^1e;{1I;TJ!dwgzB87j=r6~RKgD=sY9dI492hLN1!T3G zZMVZ0FF>Ipw0E}`YD>Q@U)o0w>4l*bkC53+?XK69=M`?9QVm-Fs=!*@VC~3Lq|fzy z@nhoHi5b^00V7;SEU#%qAA{c8u7vV)&t{yZZGC3db~^|um#i8M8`pXktPs_-=?i}( z^N=)PMg4g+mNf7&B;r-t>^lY641Y2MUJR5TX=bFR^bxMO9ELH3s-`9Gsiyp)=({82 zO(@7+FHvoaVLrp37h(M|6%SoYqYUo}^N;H>Ru+{%$AV;YXb@Xl%LvVSj`BWQHP+X` zv+He;WR;0el#uZ@io&X*t*h+h+{o<|W@K5{`{JI=?v>7AnCnh+{=flt_KF>r+2*D> zUg?#XD==N*I$i;(5HHuy-`}i^=lqS;xBMGbbn`G^>sT)YJaW*~rT)%nZQLZscmFAV z0OX!W#yg^3LmIaaLwzsvZZO8|gXg+7JU`mK%2)gL##i58dgnF^8Q@f`^16WLvE2T~ zsXz(xYm32^N3E=g7E03D_d#23WFKw=gs?sViQ8Lq)CPq8nAXyzg`O8+L&z(6JJZ zGBe}JKuBDik3O(<)?)Hq70t5O?aWTs=;kgpAryAtO~-n4r@1nXnr%(T-qvIt?fA;3 z$+)CY3!?v|k^4_;*-%T#ydh8k>c;V=#CBnvmt57)m>wa}p&UgE;@h{flT3hUA?{+n zGd!ZBq@iUC^sMsi_QTB$sJLx}MX4~@sBj(&)Nic)8mz^P?pi|>GL;f)&?*4oAX5s8 z@3VRP62x9Fm5@ak3MSD$Yk@IBkEPH2$4%D;DN6i+g}P}mgFSxcHk36ieQlU3y8JbR z;ZsPj17MQ6IeMb}!UCsGccXgKOWU$eJ1&=C`GORJzC6Os7espqX!l+SMtF>=F%6UJ z^nRu8c|doqPtq)SY4E2`d+`JH73hkZoaKewi4;6XgGwQ9%Hjq0@Lp32_~p5XQwNawMS zq`tmzgTHrhQSRk(RhR`js`uxPD0_DiDQv7*N2u?Ak(>{y9?Yyo( zZL#yJM=YT^SB?Yt+l%c>smgH2J?j?&g8a&&pN>eZ$q}oxfy@_x%_4Y{D@OXmPNaba?g^>**_9e&?eBz^7HsDBE zsFnxo&BgIs1%gB)SlNNf$ z9wGKrxb$o~z*l|uO=z16HlFEW!!YAiF?$BP)czn|80{qhpAxP|P!+FavXrG-H5){U z)(ds4?9w1f!A8NJN`V{Q6WR@`ROybKG5utznP2mLfB4@>pf;wZpQX`lG`+f4kk_u- zoXw|5h5Vf75u0Imr!!N;944#TLIfWD4E}>sN^w0VJu5^!>eMO!o_|jxr;^8GP|r=B z^rcPTPG;Ih*6OuQdY+?zXRhdGz=jCigVSNj#zsM!l2u}?fyv~1YMEbo(Lhe*eeHl= znn!5{Oea)|Cl73S6h}HimlY8`Ftgb)vD$-`%Ck3>3<~y0R=)upS`eQ%!fgXy=>ESn zlO5j>*a;NlNPVXDb8*QZ_RQ(h2_rc$oPNzCS(Hw_<&1G>m*-=B3f_d>5nj*AnU;cpKw_ogc4eQT_^6O|c$7r=l=F z__yh|^<1@TQO7d)T$1-XtSzOPZt@>rL?xY(bBM&2;7F8hjI#eSv2|HlDak__96w3n zTySMJR~~#O`Y)Hv#%|6!Jypef{qUv1n_?vSBjv&1&D zSy|Mmq<8dKM>QkEq|wOGTzT?pcbxcMGe$QNqgTTswRFd1`KLqtL{>Ke)u!Dwmc>TlD1hBwxwQFGZs!1>- z@=dp#I@UhDsIQLyP_+ciW4^h;> z!3=c~&C3Xms157K_PPR))zvz4)<4Mz$2{>ROCwgjDbeDl+F`?{ibtTt6PCI%v%k$X zuC;nxUTxby0JsDMtMgzGQ@P`I5d-UU;3IjB% z1r6Z%S*A8)*wdw2#1AI6e>s)f2Eg{wd>yT6)kb+3Dj0e7P(G;_UQpqR73lHh>Sf^J zRkLL}?U0{En;kU~c?A9^45}Ue&U3`M9Jl$x74SXgkIgX?epa?QnM(0!^>PD`>|nhw zhYuDfWjU^2;n9tt_)H?;WTX3BI`kTx@ae=jJ}W`kH0Qe2z0G-|I=-2sQWjJ#uNpHT zbE7z2A*c`&-w>VHzp}tHX@)sQ3_*SxbS>4U^w`_XY*4O^T0s6)Aq<62A1OKOa(gZ| zz+pB}dd-@zpyC}zHr>wY70njJyn*_9@a4L!hQ*YeC;=Xh}Zc!4=k+w7v*Alc@v# zt!s}KwBT)*t~{)L2HTtp<+h}ZH5-{S6?&8}c`nC*4wHDv;@hs^?OiX*ncprhkig)0LAa#!pa<<*Uv`A&VT@ z^c-7fI$GG5goH=U!f@V1+wDSf9F?)z;u{A_SE@z6g zxgxRoszLqMD4c%-nN#Asfq>Mj9mbH!(E4~akBYm zf_o7O`P#o3;^K3C44_wK)h`^6{@UvWU&xh^QOK%jY`rKIXM;0Q29B#|r5fP|=uM7M zl%y!LUpppWP9*;m8l@A390)r6BCCNcncNAPh1!{|$^9a=@#(7&b=i6e!kVG?aC!7i zNaCIM&TG@8ZjFmFZhcK9kMHpu7YwwG9rQ8vUyarPl}tTtjj$Ylhvd|8NM41d)rZ{s zPS>O4&VB1Q2DI))N~uAv*lCb$2Wq`qwRO1b_=YKm_wRvWrBBK2=NvX1_)~hCfuxiC zQ0C|BD8gtd{r3YA;^Q_>TDFDG(c?lNCfopB-XcRELwL1lVytLlL8Qhfwh5b!?>!a> zJTZSClXapFbbh5S`3!K^Jo{sxtf_%f08HK!+z*GD#KEN5uMNK^5Ga;|r*hGmnMvg; zW2W|5%1Y$_(Dp~q*)X!Yr9E&<8KmA?J$m~)Whw$xq+J51#Al54u zu;DzRA8Ygr+h#y5V_XZU!M$wMZ`-yB9Eky{NWmU?;|o{SMPnE^qVgv_lRM z>>R4X(EFhTYTQ%D&z;5ojs12O!=R+v@N%ItYhfR0_FB9El&SBETju?0d}H1Y9J;CQ zKzGe{t(xhR;X7%szf6B0oZNi*rHy`=bO&Rv)(H#R*L_Y8g0pUfO9$ggG^QaPLmWd_ zjC+*ys66b_t^ZuX-wNh$5>i?0+{L%jDk$OymUsK#?GZ_p!a_yjjb(XrWVxQnxsBhy za_NBi1^ug^I=$U)1&BHb&pr@!#_F3qX?)o{%FWB%UP^FKv{IzbwJ z20~gc;>auGM$Vy2nfk0s!fz=ZT=bb3S1UPP06?`QXG$xj)*B}=L!Wr0Z9C6a5w}eB zA0?Wv$L#|(C~68`ON{ZW+{dl^tLi8H1lR7pYgn4#+FWFxWPZ-4&!*wj^r(`Tl4|>t5e|HvO$Ba8vSIXl;IIo2)Af zMjL}WuHIErbqMyxFT}AiSz#GwGcb$dF0O$CyI+3zsM~rtV{p^k6LHV8?+v!$Ib!tR zK9%0nRy+v^i%DjXep`wN*G-1)wm4%u+60#)Hh_&}C)wNmjmrt^K!|}bq}c8qdFk$& zF<@5T89!7jN2)D`km>FO4W?AkwQx;7E!Kg5Sq&<@)N!K6_2m?`>!OkInfyO**g)vk zWn?s??eihoZa>u)jZaU4U&Oit zjy=JTe(u5Dh;z5sGAot4sx(Hd`ea2-Ix{X{!`serF%a^(HVcSEtusIJ!)*!9N&&bLgl2kDM0CANB*)u*+MC`R92+YcP$HcvH| zs$S343Ye%QZC|ozwPe-Qf7P#=vg*U*bujtL^LF#<8W!z)SbcS9oIQ`=iq2up)ZvXa z!E?g)A+PL__##p=cXTs&u(=ewQ`6oI=O~GtQ`ztU-v1mbDNxWPgLbg~Adw>Y!p2c# zE?lk5jxR3N(XR1(owBSRgatN}Yx95EEY}AHH2w=`X8wcrY;usbp;OwZMlsexKEsep zkq)u9_^exR>HRou>@mJCy2I@du}`?^eQbSuajXqC{bO)?x*)DoC}67Ych^U;75=A> zSaEQ$Z4s(4H-s>B+va;M{Ey7EbmtPBoCmnM0N>NtLCW+Mlz)k8B8o|j9aR&N-p`%h zlxB8xw$QZxd-|jUy?`$}lB@KZ$phf~CszU#L}RPg)ogsE(2vx3{jhc1`Sm=;=`Rn>1;_>Nin}6K zn&i881NB@J{vOZdrJ*4QB^=MIgc}$Q;xd)K0eBhxGzv5IEqh}rv;Bge-?5_k-udPL zg2W6S{gGu6Xt#Nqszkvc&qyA5$PQGMhZiTu=jZ>>VA62BVA?<5IJ&hIv{{bc{2kXY zWHYNE#Qjf-m>me}E(V$o7z~RXoCHLI7JEd6C4KZVI{@A&)yG%~U(x@B&y9h_h3=!J z4&Iqvc_dxb)oVL*8dQ991*#=Gu9aJ~CmebmGlN&!6<$G18=HgVuLT(cBXcwG_-yDz zYTp@q{*10LMVw(NQz`sGAhm5qg zjB>k%y>YHGM%v^K_*pwx-1q34J_XHt;g}ZMx6vi;ThUbPi_3q8vI`FfmXGmWft_VN zE@h9~rfAnGN`jSn3G!i79tc*K&^D%cvjqhNT7nx6(R5jSF!TM3D7)=zYQ&c-cbPSU z(xV*_@7QlSoM`GD@DBFllU)8C zUslIU)ry8x?pa{bQue$pQtBhc#UKJ1uhMu_X^wIs#^UtkLVyT2~4m_D3Z1)K{g%CZX*k#+_VfnN?~ zL#}6KJ<*ylLj#tCWM%=Lk6ZU#j~6*VdA?IyP`4jF)M7 zuC2sQ&Pfem6;C}9RJJJ&j8Hr<>@^}F#&Pml{7UkQS})x5!xcwvUHj~j7=YpKnfK%Q z<(Q>m8w(&ixxFLLA0R{fp%bv9p=Pf{-SxikL`7Kc@u;^u-P$RL)N0eSmhHKnAY@`* z4pgvSUMbI+J0Iwk&B+A1YN3&pbD>j>ds*W~GgNG!tFHxrTqnedQ6fjQd7A+TWabxb zXG!({`Pl}Vg-_$JM&`m+6j>LbhrQ}bFjp{Z%oo#x0*-+cVE9 z)h~EIQ1>}N?Q&agTx>UW@G-oP%H+*ewEvq-^o*hR;va5MrYG4!59%W3fxDUXLN6*h zMhiqQ>P-=fy`!)qj`m2kZcC&Zf2OIZpzC?rI&d(`Lt!sWT?C$Ix=1!?mt+F`d1B># z?=VGz=wXG=T=nkjW{Li1EuhDuG{XXt51Qp*G@oloK+lhKG#ejLWsq?QokWrk8i&

U(hRYMrQ_!VQjX(9iUkX&f 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

w| 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

k8_oe?&beoQBwjFM ziYr1l40@QG_did+(vkPtc6H%Gqq*jepcXA^1=tuA>DlxIId?KEZ#3RoN|?Ubmx6z4 z0^?>gxP}*Z-o_WmU0*zR>`ad=p0Ao%q+DdN$qVAxMi{){;B))hLUVE2w(1l(+vV?= z>NUo&(NMN?1&8e3n*jH{+V&IsI=uP3F|#D-X8)2GZkq3*4u!CPSLADezn&*>52xAB z;Yr1Xl-VL7(O~Sx#v0Q$ec+!6(@QUmuh3d9KLOm8plA~FUg*2oqc$IF!K46n-+C6R zoSDqK620>jg-RbtN#M<9#=T*FcP+0lv2QU)!>QA84C-bFz}%DKz^YMzkEXtwBSJkDo^R#kMC>68crZb} z0E4qoEC-Xn`4`t#PPtANtR#_7zS!}U!T?B zM#?khJ5S`P5_l@B6NbX&uSsH(T8&-g#yYuwb!=^fbtfwv( z;?;D9p%j-;gQBZ>yH{R-A|P6>F#*RGYmeD+D$LgvzGi+wex!$-c1TTZk8Aoa$;UtL zzz?3F*A|PE6{0cLlU&7Dhd8a>209dj*%}xycqh3r)PaObqon zdFR}O_1?qnxao*t+aS!26OjNp(}A_Pd3y3++Qd)s4~S~U=V#|{mW)eP)6uWAviUzw zlkB^Rnq4`t*{&+%xG`$j%oF_3lCtH41)yx@(9oF3A!EN#8+xK@-`#SF>1QJCDk9R+I4wpQ z#9G-@?KxoKUVcUl2a0T+9@)(Mb%v_*Zt08pqZkNimkzU8CW z6`_rIdpL;W% zh%~9ijeE09orKO9I9%jG(rH+#qx}fgKQjQVCuy_aebPh(A%z}I4`JVZ$Q~+7d+vTm z87;W90G~a5$a*0zplT*yu*TsjY2(o@>p-BLfwYvQ@F6H9Oe?+~!`@m|H2heZ#m&;D z9c=I(v-Z2my~bf-^Xl~K<^um%I+uv?<42XolAV}5J5`>`mHkiO4EpVTuCF|^w!Fz< z1?%{Lh-%(YrHvDYNu88uCVl^;UAt~F9BUpLYoX>LIMus3^){(*^#-2qJ3^0h*n|G) ziYTnFHt*dMq^WOo2HYsi`J@>j?Ueb6`b_XydAz=i_9vGLrQ}5C7N0TP6~Gn`2ijOU-ex_$o2i|^EdP7L#_RJ#FYUEL<7+kB8YAJk&?lW(|q-M_VOXEcy zC+z*Jy?YDVbG>7Ur7DRW&x2RIWu&oef=F$^B^AH)-?3DLGQKT0>m;;aHZ>1pT4 z&8!9r09jdC{Pv`!f^nJQebVmQ^43oE=@N5GuXP~r_4^i_``Dk&XWKHl6tw~mQNC)> z$5;wkOR|CW;&YjKhgEeH-yP;?N~M?-GgJXw)lffw<3;LMo=Y`2KCxI+x^<6q!;wJo zJB#0U)?BVF3M+ADS-K9;uc&@}|C|+Zn-S{1y~)?ZF`X!Nspj#3V%tFOYv>f6y#w*J zhabldbvugj%|6XCYXbVuK_wY$fTKa0AwRn(Im|>&@iiyHWw=+#?;k&NG3Rf43*HBB z17I2K2@29{`#oF%PGAaP@b}xv;)UT79*lTh3%U?j?_xN(o`2TJN&;&23Vao5P+mec z(yi-t-qq9Lu8JAW18s{QIudR2<2N}L6}1a&YI$SWsl8Qa7!QX&G|#7#Zbs0DXga9` zJZ6-7@v><#x7YiD>63SW4WE9_Dn;tma3{r7xcti<6%>&0VRwVqd|-Z-#Wq-l5<33u zi1rVSG0dR9yULvKxj9Vi`)=Xjox|Y*IPr9Jbo2}#y=8=lCbJ>xEMPhdemxZVNrwuf zCQL9_i61t(SI{k(f+?_>C4C^5r$(a$II z5c@AzJ3->0>^3udIYS}?Q2L0UzlW|bUq;1j07lIy@?sloN8N{%l3J&NdInDeTG zX*@2bumA&gQcwS9zOJCYE!I(y2_5kZo#E!?*=4h#X~s558G5=P>N!WODaF5y z%Iz)i-E0^SN;yn_;TmHinv5UySVfbQE%%~->D(Dly9&9@ax{HK^V@Nsj1P<5(qBPD zi({9!h6BWz=&BnEE6Mt-i% z;W|zCYR3L)8s;*tU?4`>IpMxn~;jwl62gFolrJj4a zOITo(cP?z&QmSYX@VF&kakk9%THXL2jEnM6u+Q>aOFfoqKG+=%z|Kcl#Asw-Wp7-a z0(R=~^YeT8brry4%J<66-Ob_^P72JdWu1eQpl(wZ7-ANh?2IPoq`->t%j#Nl8G~s} z_xdAs2C=t2hhNY8BuaLqv%A4m10MF|n_5W6S)7*Vn&oSFzSCqvJZk+*RE2gzmpUGX zWs=`xxEup}H>f3mB@|RduVQX=F{f%^3_X+IOH_X2$;C^DZtS5NGlu9k)O_&Q4&3=7 zj4`}+xZ^4QSkaVQwt|qtNzsn{j@rOaNDgsLt8y5(5lq6mqxXe*Y!_L^gH|t^ztu>N z?Vss>T_tqw?3E1%JNxOaQr=YgS^6`NaKRkm!)iJ-7MdC|8uTYWc@Kq;1<#)VQQHJK9RQnT+?(10E6=Lb93; zX5`_Yj7pRC-n*5WjdWosGBAe>Z7c}1mGxIP5w$LwpPU!1xB7j3Pmc-|{GX%^Qfij` zDy9VJizoZT7H`$2BNe@yVz{=h-@EIZ5SXG}U}co6SfXOHQQx?)GyDBGPnk-0&}zx6 zX`<@41gDbJw(7L#(bGwHy#rRbT%qaK3xeZ#SS^g?Gg;>Gk?*=8vC^fOXqRdj^d9&N zd16P7(pg#1S~tu(8`Jr5=NFY6=2y?a-aL^M*IT6;C*Pn=lFh&D&bS#t=t+B%4wqeatFM_R|e z)k`K3&W9Pg96|@O=36FVRV5w!CTz<`y6dB2_XRU^cM`6TRg`l63Of_oMNOj&WB9Do zxc!U`wSE|&-XR=cI0VW|hMUQsh`^L!X&HZf7VyyZJJRY)8# z%d5#(@0qO8DI!Y``Zqxc*9;1dP-POt=|S+xM#FlhDGX-b*z5y;P(EjfFGG6U)x}e2 zfwSmVi)ZzZVfP!})2q#I%wHv~Y)$dA*%z%TqgoZ&J9CnXAw#_0A6Jw1+~APu%CQejs5j6K(7%IQN~at1D5X<*09z zxJ*me ze+_;gn=dy)9oePuvt15O0&&v*I6J9T^VC z*dC71{O`dW?Hwu4p>5HzMOhizSOQF z^wlFIL*2}3OnaXi-~Rs`xV@@q#2KKQX_oqI%)abVM_$t#w34pbY0poub#6~abzk%N zf7?9HZJay!M^kGUJ#4O(Ab@?*okwmQql?sqf@-&5TSFktYmZ$2*1=9a})ySL&17=zgbQ|q=7bGhOip$wo zC~ULl_5tn^d~pML|LbOl-1~#Sx|EG?9^Tu?N3gGGFRk1IT8NyQ52gI2?Ec4u1WYWR z{AiAk-wULy(fXs{(Y86ROTNYE*wUFGIA+z_z`I z##)f3lQw_YOA}=E|FscokBHxCZ1*{jmg35L*duRk+Iw)|0#^a!VC1Vh(Aq8A{~lc? zd2+(W_?z9SWR<&TEr_0pG8Yor`t7TpE|adP~fO{|z3i>@^F` z4)ys?(vlworddXi02JU@wVn!#qG9ym+eX|Uu^K1CPa6#IiOtGL(NfOomJ9#u4&Dys zjwZV8sz?o}$OSMNwmvSnJebPiolf9`g;F0k+!+w`Q^8e zyl4?*Yg@S8;|z+QrUq*%3ke~SY{9DUDl`A{On13{NhxeGZOpwBO)l#{x3v-ukW5)- zc(gB#V3=v)LEfNzpxvGuJ=p&n z)kQgSvWEp-ULZ_v+|lr2jl;Pb9_w+pt(19oAHKnHS4heIj}Ad{OBQFvL$Cj9Sh26K z8O`HPhzyek%;tE|YeDShZ#P$e^IU^JrKSO^eQKNCBgy}pjK&p~kjExxqc{XhF!L^& z%U4dD<8Q9N#L)H*>+iOe_^5GG$F-B= zy=%zBb**5Tqt&*92minE6>?2fN>=)N6!m*p@zZ&JmAT1Vahc<}f5J#BE2m37Vyjdh zPjx%~*WUG!Mqv!t+^q$c*UkKAWgzm8m_Dt<#rcVhA-f4)O>-*_pMF3l6uDLNf6Zy8 zyD$CY#)fG36KK1FOqML4jKSK6J^(6Pw3Xv?e9n7B=5;O#BlbVG9Qj;2<$Z1x6{b9A z#LIvWNa0THg;z=J6@R^mV>!_#N&mM>^X4uphb@<^a!OA<%Uo^_C|s8<_3Q4xiz+3V zOUJNg@K;&$V@iHpM>pXqU&lrW+#)@*g4_DQ4h6s7C15}~t6iK2FVp&L%cBpDi0IMj zWhkBvs$c=EF~LE|C$p>8kYmuQ_@G?XUmEql*0=OJ+H7O#-Hp1l(|`_vRx!TDqF<;+ zpH0g7famay%4X?Vfk`H3QOyamC57v0Cl`;85^mc6l*^y_09b8wT2&j9A&ThkG(Z&LpggY#7L7B7=mKB=8GYz-?>sC!Vx%d7? z3|p)7(+|V}6WcWkK%jS6*eKW23<~AH!RqH%)a%w8Y3wQGE&J`)Fv3Bb8c~*oy;24* zGIZk({q>td*#+ig0T>qVZGK4Qogi4dD+;dUiLb>M&(^IUfglp(-38Ka)3j49sfLns z6kj*Xe3S6cC=@*}A}ot=>)mPM9Lu}bZNRhpg)(4a?{`f$d&-_ps4<;qI$6(xfOQ#8 zwgz&$ER<#_(U9Aqf+oL}Y*`cCrTKoOn7|T{j9MU~9803S^!{C~yR~!I6nz#0FPYt< zR7=i><0VMn8lHP0)R>`Ob_pdO%8k(V%m5b(!TFJjgfWDSU$6|L*=#nCPI0^tQ@->h zTc_7pUv@olZl?sCYI5li$xlxh~q>)sU;vzjH zMTI~w<~R7yVl^c{<}ugd`j~Tq?z{GBqhgy4C{ZSGyVUIwdVoU~;pb5u(;lo`h2V`p zd7D5y=ousk!19#maLc27WeRZcn(M;e;ay{uR7SA(MDK{Zboy$!R-!XMu4DY)5sK=p ziSyN!_w|2Q;&+>#nIa4`qPoY>;5b=k?i>8mtHj6iPH$v21c$Vohiv@}f(HTk7HHL6 z_uY>J)NK>1ziJO0GmL7;%F*@xqNEW2nsv`hh7HlvO-g)}>XpvSWly-f!i5o0n>-WA z6o227!-FoMFrEI>S!UoJw1RO=d9?PmHA^X%CxJRKuew*(>cx%M7dAJo`ZU5UpPUTR zA?p_Qjz)yO^X!t*PiInJE<^vElCUic7@WN{Fn^?2(N#r3B86@BV6EG-`dc?j zUn_7EPp4+DTXmY+5z{RiW-;P1$igjTIk;LaOB5} z)K46EvCi8|gp9A>8fNA&+mVGf3lE0{#z8}*FeZ?x-fV#4IA!#;rTZ$_kk(Bf$qR>8;(JmM>2#6 z{@desbyELb_UrRr@7*UjvU8r?mIy9%R2cZM+tIZWj^BEF?9WHAyQF$iUQSN1XZJxm z*PuLhZfAf0JvLl8)>r7_e~Xuy$sEE9xxgO5-6+zv}D1^>-u!pcyHCgSa!OAt)B%c_3OG6A-}(-ab?ZIXC6FTnu36K zM!8U#C#Q$W-_Se^Zm}J=oBtCpHUB*-)RWFZ3&C=B|1y-Ku2Mu#Z|QwDbF~BE?#zI@ zdCvk_dzcfGY~j#a5`)@B_9?4!)zQylveTlO+@Yp~H@#W9`wb%5FOcb@LRj#`f zaN3?BRL*@-Uf4bXst*?uj}P3f;g9k)ACEjgs`6**&*`ae>5RSFs1D*E&?6%VBUvN<$wm3_ge&Kaje+7Byyu)D4Tj%0%EZBmaoh>8r5W3QVG- z9J>#^4D2^7mC6_Ib|vv8GS^r@Kp;Xb)waqG@7AoVRqul0qUAld*_C0htgqrWkRm~s zS>@$wX5+tVHjUAtwefx4h>HBc^ahF<=p<# z#5tJG)sr{_Bc4uo+@l**AQP6xL6#`@qSQTZ=SPjJI_5gUv@7y3ZIqjTpC+b_9y~Pb z9Bx;Y31s!F40ee}@1f|=76`wgWkqno&9+tpFA4PItt7yzM$YfmiWLfQRVYFa-2LRY z{0T~sr0oM=Y2G;m+dLg0E|7iD;10;PX-fm8c!SHxoHD#L&(Q~mQA(DqNx!8QJgqkZ zrc&nEv8#uogSUEHY>#uVDVG+E^zjm5&FaaFka-W<3L=ZfMH(NZYVBE*#K%Q^v|>{Cc964mb-mWXu<&canZn9Bk4+N zGBD^b^e3fyyAnH#rVg5`b+$Yj#t94JuLnM~9}F}xe0LH5piCHv?U40K3wVizNT@y| z|KXBLfm5IdDRi+AQH7jqRNJpf5}{?t50uQNE6q{7=VGCke4j@NCk8gW+G!0X8YsmA zCoFi+$9vqn`24mWZ;NF8Q$zhSBE4R8s{>WkE}GWfZb1iTRRAduZTZ&oDpOBBz6ffarCyPlj?;P6feGqm(W}N(&MD;M zJ9CATZimG@gw`oV(FOqL#~{@O2xC%FWlfD&mlR(}x=$c`4Ne2|4WHvKmQk0SoM^qt zW;zXFC9sbCabEN)Tt0(V82EIEP+j+Acw$7!Mqap5A${q{;+2=tc=aA9mS4vLQd8^A z8DwO_TD^;Rr7(nz_r_1cTG~l4g|A#M2uI(IvGvQO6BG(1dcL#kRjTKE3Ltr7e}4$@ z?y&!CN7d8L@=|@YH@gSJp;l2n3^fU!KB)}S9V;+cjw%?hbNVK%!)BVLW3RI5vo|M` z_4dBhiI>;cgWgo@{Qa6|+pdRM$?KRN=fG9ICMskeex}{;bbqm-6wVeZI;7M>uhUFZ z&=C$_{+)KaS?1UWyV#-kZkHg}zUC0RG-rbIjrc9`Wp{UXaQ|bJ^~^;%+~k zmmwT;w8{o@-ub}y=Lz7`OuAI<3?XOS!dPXh+ftUFq;M4yku5Dp^__&y<#jyYZ1x+42h2@Q zOz_MZ+um59C9Ph7PilC$#RRpW&0xvw9H0yR8`P#XRDssF3e&YHu0#< zy3NHa5&Fve%gdxmgJOR0MM}LGN7PCk!zO#1Ojow(eJ(jVEH7TU);R4|0u|)o(cUjI z4HB4OT@ZaA4Kh|vYXmB+M4d%_KeHpM*{)3KWeU^PBodZoEi83rIG4_#WZ5_ED3LE8 zV}SrbRGCH74DM!#r=1CwYquB#YtEwy#zaR)L(HNPfw)7~lk6p}{?P&**bW-z$@uZJ z&L4}P?mtf-XZ0whcdk1o`Fl3<>$WnB(sBwsnCuI@b~m~FqhqfwRYrzAP&ntF0d z_yHbikklZva~uot16X{;da)cWP!P93`0Y{IMH1!1P52Z?3E0VA5}#;1{QX%4g@t`Y z?7uylJ2pEz+I_ew-_wzzv^5_RW}(#~?_fxqnIarXX(z4z^dRPbY5iM#K1 z^!ybU$JipM@34JRr{oq*u}xuP7E6mTn{B%7fzR>CqvXdO&bk)pWs+*3TUrSC zhfYt={nCrhYL-5^Pvq~UIk8pRj=+CdR#)~AE5s6jK;_CNd;gh%v7X^!wI`)F%*=y4 z)iB!NkD9X`cuxem(#x}-++pmJ74J8)@(%lZZ%r z*QQn@`GAuhOl1WD1LP=pgnu$PDF>^uhmnoD`$fKZpErN;?aEkb)OWlp=5x4|s|o*{ zsFllt_ulK%%2qCLebAOeNC-gJ2cZ_%-sBPkh34iIG$m9#)KFcdi@apTp+9YD(Dk!X zdtYHY7kKbs%(rCi5Y+Y3S`wYo`l8W_B|SJec-05Z$UytdyiO>`H~cf^$7Xqv{o*qB ztKqU1LxlhHuGG*|MTvH5SxI#P+4apCnQ4r>ZJ=tvZ$k|%PI1=k^?Sfmc5F^zX6xjj z-)_RmR6NV*VIZm6a~c~z_UueCBXIX&q-iH+eNedA7pTyA?}hEK^vz?%RMH~=jQ@^e zR>p09o$5(HqIHR8HCt%gN2*4*W#t#oW%cE-5ks9rzWS`!bG|nADMHbxyO_vq*7{obGY{uGReNcQN)@s?es-B6lMw=ct}ThovR}4&NU|Ktoaala;jHdQ)7^NBc!m? zC|mfaWj<|9+@ZwBU-)RJ>Jx{cUcA;1q~G23i&;!_^jx5#U{|3-_a~fkx6v0ZVtQho z6@!)u$VWYYJMWy}B~cD_=DSpxe53jth7zTX22`$^E9*Y!d3g^BM%nqiPOg%ynqyr5 z4SGKI>m4H3b7kI}ha|(DoyPD-bw9Q#9f67jG(Ec!u#iTra9>hN%5;|6*(J@2CDiIM zk6Bp5(_g%62s>2TtF|a+VID+Do&wU;P5qOaySqDMX-SFa^Ukyc?Sf#g&BfZUzf2#v z#mMI$1ll8aX?`>3uj7*5n^wl#n*dMj{<$N|?trbYcZjfdAv{?u5Guk8 zj^d?eZq$A{#)Z7MPcXhWP1SNJ`=Tal38UHkHB)VNZcd*ABPYAoY}s*umVSy%XNO8iFP0kq`~$iZUY60`Uvt!svbsSh|cT^_ENffq+;7PQ@h2mw%>>{(_wkGM>0XFK>xZ{s|T4d zSFLQA5|Ps8q{4=vQ*KAJTz;D$Z0oaswEl7lGO%F5XOeUT2MbI*xsVQl{D0}|;)BgiL-<$RT diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding1.imageset/all_chats_onboarding1@3x.png deleted file mode 100644 index 8cb11ba5deb04b95992ae4a7bc0750cb7c850652..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169678 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%0Dtz|8^8qU8O6=hot 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 diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json deleted file mode 100644 index 99aa89f84..000000000 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "all_chats_onboarding2.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "all_chats_onboarding2@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "all_chats_onboarding2@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding2.imageset/all_chats_onboarding2.png deleted file mode 100644 index 119903ee62c4584ee49a1f23837ba5dd5c74756d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22361 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 Hu0mjf1psA% diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@2x.png deleted file mode 100644 index 6c2ae7bbfc0753af9067794d34abdcd95602b4de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88244 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 diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3@3x.png deleted file mode 100644 index 8bb136ca16740f913e1816b6ae50cd6d21fa12af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154809 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%0DuqHw@~z<0xoOgZv2pzsf>l9v%zj5 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 II;Vst08YNNWB>pF diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index bb52fc5ea..1bff71b29 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2457,9 +2457,7 @@ "all_chats_all_filter" = "Alle"; "all_chats_edit_layout_show_filters" = "Filter anzeigen"; "all_chats_edit_menu_leave_space" = "%@ verlassen"; -"all_chats_onboarding_page_title3" = "Rückmeldung geben"; "room_invites_empty_view_information" = "Hier erscheinen deine Einladungen."; -"all_chats_onboarding_try_it" = "Probiere es aus"; "threads_discourage_information_1" = "Dein Heimserver unterstützt aktuell keine Threads, weshalb diese Funktion unzuverlässig sein könnte. Manche Thread-Nachrichten könnten nicht zuverlässig verfügbar sein. "; "all_chats_nothing_found_placeholder_title" = "Nichts gefunden."; "spaces_create_subspace_title" = "Sub-Space erstellen"; @@ -2475,16 +2473,10 @@ "room_access_settings_screen_private_message" = "Nur sichtbar und betretbar für eingeladene Personen."; "location_sharing_allow_background_location_message" = "Wenn du deinen Echtzeit-Standort freigeben möchtest, benötigt Element den Standortzugriff auch im Hintergrund. Um den Zugriff zu gewähren, tippe auf Einstellungen > Standort und wähle „Immer“"; "space_selector_empty_view_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Erstelle einen Space, um zu beginnen."; -"all_chats_onboarding_title" = "Was ist neu"; -"all_chats_onboarding_page_message3" = "Drücke auf dein Profil um uns Wissen zu lassen, was du denkst."; -"all_chats_onboarding_page_message2" = "Greife auf deine Spaces (unten links) schneller und einfacher denn je zu."; -"all_chats_onboarding_page_title2" = "Auf Spaces zugreifen"; -"all_chats_onboarding_page_message1" = "Um dein Element zu vereinfachen, sind Tabs nun optional. Verwalte sie mit dem Menü oben rechts."; "all_chats_empty_view_information" = "Die Komplettlösung für sichere Kommunikation unter Freunden, in Gruppen oder in Organisationen. Erstelle eine Unterhaltung oder trete einem bestehenden Raum bei, um loszulegen."; "all_chats_empty_space_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Füge einen bestehenden Raum hinzu oder erstelle einen neuen mit der Schaltfläche unten rechts."; "all_chats_edit_layout_sorting_options_title" = "Sortiere deine Nachrichten nach"; "space_detail_nav_title" = "Space-Details"; -"all_chats_onboarding_page_title1" = "Willkommen in einer neuen Übersicht!"; "all_chats_edit_menu_space_settings" = "Space-Einstellungen"; "all_chats_user_menu_settings" = "Nutzereinstellungen"; "room_recents_recently_viewed_section" = "Kürzlich angesehen"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8ddf6ad5a..49b6fb73c 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2277,15 +2277,6 @@ Tap the + to start adding people."; "all_chats_edit_menu_leave_space" = "Leave %@"; "all_chats_edit_menu_space_settings" = "Space settings"; -"all_chats_onboarding_page_title1" = "Welcome to a new view!"; -"all_chats_onboarding_page_message1" = "To simplify your Element, tabs are now optional. Manage them using the top-right menu."; -"all_chats_onboarding_page_title2" = "Access Spaces"; -"all_chats_onboarding_page_message2" = "Access your Spaces (bottom-left) faster and easier than ever before."; -"all_chats_onboarding_page_title3" = "Give Feedback"; -"all_chats_onboarding_page_message3" = "Tap your profile to let us know what you think."; -"all_chats_onboarding_title" = "What's new"; -"all_chats_onboarding_try_it" = "Try it out"; - // MARK: - Room invites "room_invites_empty_view_title" = "Nothing new."; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 71ea69ebf..c64a5133f 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2417,14 +2417,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Uut teavet ei leidu."; -"all_chats_onboarding_try_it" = "Proovi nüüd"; -"all_chats_onboarding_title" = "Mida on meil uut"; -"all_chats_onboarding_page_message3" = "Kui soovid meile teada anda oma arvamustest, siis klõpsi oma profiili ikooni."; -"all_chats_onboarding_page_title3" = "Jaga tagasisidet"; -"all_chats_onboarding_page_message2" = "Kogukonnad leiad alt vasakult kiiremini ja lihtsamini, kui varem."; -"all_chats_onboarding_page_title2" = "Ligipääs kogukondadele"; -"all_chats_onboarding_page_message1" = "Et Element'i kasutamine oleks lihtsam, siis kaardid on nüüd valikulised. Neid saad hallata ülal paremal avanevast menüüst."; -"all_chats_onboarding_page_title1" = "Meie liidesel on nüüd uus vaade!"; "all_chats_nothing_found_placeholder_message" = "Proovi muuta oma otsingut."; "all_chats_nothing_found_placeholder_title" = "Mitte midagi ei leidu."; "all_chats_empty_unreads_placeholder_message" = "Kui sul on lugemata sõnumeid, siis nad on siit leitavad."; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index ed4a3820f..4d9490190 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -2440,7 +2440,6 @@ "room_access_space_chooser_other_spaces_section_info" = "Ce sont probablement des choses auxquelles les autres admins de %@ participent."; "authentication_choose_password_not_verified_message" = "Vérifiez votre boîte de réception"; "authentication_choose_password_not_verified_title" = "Email non vérifié"; -"all_chats_onboarding_page_title3" = "Donner mon avis"; // MARK: User sessions management @@ -2460,13 +2459,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Rien de neuf."; -"all_chats_onboarding_try_it" = "Essayez"; -"all_chats_onboarding_title" = "Quoi de neuf"; -"all_chats_onboarding_page_message3" = "Appuyez sur votre profil pour nous faire vos retours."; -"all_chats_onboarding_page_message2" = "Accédez à vos espaces (en bas à gauche) plus rapidement et facilement qu’avant."; -"all_chats_onboarding_page_title2" = "Accéder aux espaces"; -"all_chats_onboarding_page_message1" = "Pour simplifier Element, les onglets sont désormais facultatifs. Gérez les depuis le menu en haut à droite."; -"all_chats_onboarding_page_title1" = "Bienvenu dans une nouvelle vue !"; "all_chats_edit_menu_space_settings" = "Paramètres de l’espace"; "all_chats_edit_menu_leave_space" = "Quitter %@"; "all_chats_user_menu_settings" = "Paramètres utilisateur"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 6dd053e54..1cfd48147 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2467,14 +2467,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Semmi új."; -"all_chats_onboarding_try_it" = "Próbáld ki"; -"all_chats_onboarding_title" = "Újdonságok"; -"all_chats_onboarding_page_message3" = "Koppints a profilodra és mond el mit gondolsz."; -"all_chats_onboarding_page_title3" = "Visszajelzés adása"; -"all_chats_onboarding_page_message2" = "A terekhez való hozzáférés (balra lent) gyorsabb és egyszerűbb mint valaha."; -"all_chats_onboarding_page_title2" = "Hozzáférés a terekhez"; -"all_chats_onboarding_page_message1" = "Element egyszerűsítéséhez a lapok mostantól választhatók. Beállítani a jobb felső menüből lehet."; -"all_chats_onboarding_page_title1" = "Üdv az új kinézetben!"; "all_chats_nothing_found_placeholder_message" = "Próbáld meg a keresést módosítani."; "all_chats_nothing_found_placeholder_title" = "Nincs találat."; "all_chats_empty_unreads_placeholder_message" = "Ez az a hely ahol az olvasatlan üzeneteid megjelennek, ha lesznek."; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 508d3eb7d..82ef32120 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2672,14 +2672,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Belum ada yang baru."; -"all_chats_onboarding_try_it" = "Coba"; -"all_chats_onboarding_title" = "Apa yang baru"; -"all_chats_onboarding_page_message3" = "Ketuk profil Anda untuk memberi tahu kami bagaimana menurut Anda."; -"all_chats_onboarding_page_title3" = "Berikan Masukan"; -"all_chats_onboarding_page_message2" = "Akses Space Anda (di kiri bawah) dengan lebih cepat dan lebih mudah dari sebelumnya."; -"all_chats_onboarding_page_title2" = "Akses Space"; -"all_chats_onboarding_page_message1" = "Untuk membuat Element Anda lebih sederhana, fitur tab sekarang opsional. Kelola menggunakan menu kanan atas."; -"all_chats_onboarding_page_title1" = "Selamat datang di tampilan yang baru!"; "all_chats_nothing_found_placeholder_message" = "Coba atur pencarian Anda."; "all_chats_nothing_found_placeholder_title" = "Tidak ada yang ditemukan."; "all_chats_empty_unreads_placeholder_message" = "Ini di mana pesan Anda yang belum dibaca akan ditampilkan, ketika Anda menerimanya."; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index 10722ec1b..1a0e5981e 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -2132,7 +2132,6 @@ "user_sessions_overview_title" = "Setur"; "space_selector_create_space" = "Búa til svæði"; -"all_chats_onboarding_try_it" = "Prófaðu það"; "all_chats_edit_menu_space_settings" = "Stillingar svæðis"; "all_chats_edit_menu_leave_space" = "Yfirgefa %@"; "room_recents_recently_viewed_section" = "Nýlega skoðað"; @@ -2234,7 +2233,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Ekkert nýtt."; -"all_chats_onboarding_page_title1" = "Velkomin í nýja sýn!"; "all_chats_nothing_found_placeholder_message" = "Reyndu að aðlaga leitina þína."; "all_chats_edit_layout_alphabetical_order" = "Raða A-Ö"; "all_chats_edit_layout_activity_order" = "Raða eftir virkni"; @@ -2330,9 +2328,6 @@ // Mark: - Space Selector "space_selector_title" = "Svæðin mín"; -"all_chats_onboarding_title" = "Hvað er nýtt"; -"all_chats_onboarding_page_title3" = "Gefðu umsögn"; -"all_chats_onboarding_page_title2" = "Aðgangur að svæðum"; "all_chats_user_menu_settings" = "Notandastillingar"; "all_chats_edit_layout_pin_spaces_title" = "Festu svæðin þín"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 8dbc7f07a..952758a08 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2445,14 +2445,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Niente di nuovo."; -"all_chats_onboarding_try_it" = "Provalo"; -"all_chats_onboarding_title" = "Novità"; -"all_chats_onboarding_page_message3" = "Tocca il tuo profilo per farci sapere cosa ne pensi."; -"all_chats_onboarding_page_title3" = "Invia un feedback"; -"all_chats_onboarding_page_message2" = "Accedi ai tuoi spazi (in basso a sinistra) più velocemente e più facilmente che mai."; -"all_chats_onboarding_page_title2" = "Accedi agli spazi"; -"all_chats_onboarding_page_message1" = "Per semplificare Element, le schede ora sono opzionali. Gestiscile usando il menu in alto a destra."; -"all_chats_onboarding_page_title1" = "Benvenuti ad una nuova panoramica!"; "all_chats_nothing_found_placeholder_message" = "Prova a cambiare la tua ricerca."; "all_chats_nothing_found_placeholder_title" = "Non è stato trovato niente."; "all_chats_empty_unreads_placeholder_message" = "Qui è dove verranno mostrati i messaggi non letti, quando ne avrai qualcuno."; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 9c76e1920..de3ff9574 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1799,5 +1799,4 @@ "service_terms_modal_information_title_identity_server" = "IDサーバー"; "location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; "location_sharing_live_error" = "位置情報(ライブ)のエラー"; -"all_chats_onboarding_page_title3" = "フィードバックを送信"; "all_chats_edit_layout" = "レイアウトの設定"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index ab294231c..a814c281b 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -2605,14 +2605,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Niets nieuws."; -"all_chats_onboarding_try_it" = "Probeer het uit"; -"all_chats_onboarding_title" = "Wat is nieuw"; -"all_chats_onboarding_page_message3" = "Tik op je profiel om ons te laten weten wat je ervan vindt."; -"all_chats_onboarding_page_title3" = "Geef feedback"; -"all_chats_onboarding_page_message2" = "Krijg sneller en gemakkelijker toegang tot je Spaces (linksonder) dan ooit tevoren."; -"all_chats_onboarding_page_title2" = "Toegang tot spaces"; -"all_chats_onboarding_page_message1" = "Om je Element te vereenvoudigen, zijn tabbladen nu optioneel. Beheer ze met behulp van het menu rechtsboven."; -"all_chats_onboarding_page_title1" = "Welkom bij de nieuwe weergave!"; "all_chats_edit_menu_space_settings" = "Space instellingen"; "all_chats_edit_menu_leave_space" = "Verlaat %@"; "all_chats_user_menu_settings" = "Gebruikersinstellingen"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 8532c4b4d..9106d3eea 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -2533,13 +2533,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nic nowego."; -"all_chats_onboarding_try_it" = "Wypróbuj"; -"all_chats_onboarding_title" = "Co nowego"; -"all_chats_onboarding_page_message3" = "Dotknij swojego profilu by poinformować nas, co o tym sądzisz."; -"all_chats_onboarding_page_title3" = "Prześlij opinię"; -"all_chats_onboarding_page_message2" = "Uzyskaj dostęp do twoich przestrzeni (lewy dolny róg) szybciej i prościej niż kiedykolwiek."; -"all_chats_onboarding_page_message1" = "Aby uprościć korzystanie z Element, karty są teraz opcjonalne. Możesz nimi zarządzać w menu w prawym górnym rogu."; -"all_chats_onboarding_page_title1" = "Witaj w nowym widoku!"; "all_chats_edit_menu_space_settings" = "Ustawienia przestrzeni"; "all_chats_edit_menu_leave_space" = "Opuść %@"; "all_chats_user_menu_settings" = "Ustawienia użytkownika"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 582951c39..0d9dbe5d9 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2446,14 +2446,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nada novo."; -"all_chats_onboarding_try_it" = "Experimentar"; -"all_chats_onboarding_title" = "O que tem de novo"; -"all_chats_onboarding_page_message3" = "Toque em seu perfil para nos deixar sabendo do que você acha."; -"all_chats_onboarding_page_title3" = "Dê Feedback"; -"all_chats_onboarding_page_message2" = "Acesse seus Espaços (esquerda fundo) mais rápido e fácil que jamais antes."; -"all_chats_onboarding_page_title2" = "Acesse Espaços"; -"all_chats_onboarding_page_message1" = "Para simplificar seu Element, abas são agora opcionais. Gerencie-as usando o menu direito topo."; -"all_chats_onboarding_page_title1" = "Boas vindas a uma nova visão!"; "all_chats_nothing_found_placeholder_message" = "Tente ajustar sua pesquisa."; "all_chats_nothing_found_placeholder_title" = "Nada encontrado."; "all_chats_empty_unreads_placeholder_message" = "Isto é onde suas mensagens não-lidas vão aparecer, quando você tiver algumas."; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 83b316cf8..6d69e6c98 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2668,14 +2668,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nič nové."; -"all_chats_onboarding_try_it" = "Vyskúšajte si to"; -"all_chats_onboarding_title" = "Čo je nové"; -"all_chats_onboarding_page_message3" = "Ťuknite na svoj profil a dajte nám vedieť, čo si myslíte."; -"all_chats_onboarding_page_title3" = "Poskytnite spätnú väzbu"; -"all_chats_onboarding_page_title2" = "Prístup k priestorom"; -"all_chats_onboarding_page_message2" = "Získajte prístup k svojim priestorom (vľavo dole) rýchlejšie a jednoduchšie ako kedykoľvek predtým."; -"all_chats_onboarding_page_message1" = "Pre zjednodušenie vašej aplikácie Element, sú teraz karty voliteľné. Spravujte ich pomocou ponuky vpravo hore."; -"all_chats_onboarding_page_title1" = "Vitajte v novom zobrazení!"; "all_chats_nothing_found_placeholder_message" = "Skúste upraviť svoje hľadanie."; "all_chats_nothing_found_placeholder_title" = "Nič sa nenašlo."; "all_chats_empty_unreads_placeholder_message" = "Tu sa zobrazia neprečítané správy, ak nejaké máte."; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index b2a297d07..eaa376f3a 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2417,7 +2417,6 @@ // MARK: Authentication "authentication_registration_title" = "Krijoni llogarinë tuaj"; -"all_chats_onboarding_page_message3" = "Prekni profilin tuaj që të na bëni të ditur se ç’mendoni."; "all_chats_edit_layout_add_section_message" = "Fiksoni ndarje te kreu, për hyrje të lehtë në ta"; "room_event_encryption_info_key_authenticity_not_guaranteed" = "S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje."; "deselect_all" = "Shpërzgjidhi Krejt"; @@ -2534,13 +2533,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "S’ka gjë të re."; -"all_chats_onboarding_try_it" = "Provojeni"; -"all_chats_onboarding_title" = "Ç’ka të re"; -"all_chats_onboarding_page_title3" = "Jepni Përshtypje"; -"all_chats_onboarding_page_message2" = "Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë."; -"all_chats_onboarding_page_title2" = "Hyni Në Hapësira"; -"all_chats_onboarding_page_message1" = "Që të thjeshtohet Element-i juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye."; -"all_chats_onboarding_page_title1" = "Mirë se vini te një pamje e re!"; "all_chats_edit_menu_space_settings" = "Rregullime hapësire"; "all_chats_edit_menu_leave_space" = "Braktise %@"; "all_chats_user_menu_settings" = "Rregullime përdoruesi"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 95c51d890..89455745f 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2670,14 +2670,6 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Нічого нового."; -"all_chats_onboarding_try_it" = "Спробувати"; -"all_chats_onboarding_title" = "Що нового"; -"all_chats_onboarding_page_message3" = "Торкніться свого профілю, щоб розповісти нам свою думку."; -"all_chats_onboarding_page_title3" = "Напишіть відгук"; -"all_chats_onboarding_page_message2" = "Отримуйте доступ до своїх просторів (унизу ліворуч) швидше та легше, ніж раніше."; -"all_chats_onboarding_page_title2" = "Доступ до просторів"; -"all_chats_onboarding_page_message1" = "Щоб спростити ваш Element, вкладки тепер необов’язкові. Керуйте ними у верхньому правому меню."; -"all_chats_onboarding_page_title1" = "Вітаємо в новому вигляді!"; "all_chats_nothing_found_placeholder_message" = "Спробуйте налаштувати пошук."; "all_chats_nothing_found_placeholder_title" = "Нічого не знайдено."; "all_chats_empty_unreads_placeholder_message" = "Тут з'являтимуться ваші непрочитані повідомлення, якщо вони є."; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 5cbcab2e2..ccad26c35 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -22,9 +22,6 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image internal class Asset: NSObject { @objcMembers @objc(AssetImages) internal class Images: NSObject { - internal static let allChatsOnboarding1 = ImageAsset(name: "all_chats_onboarding1") - internal static let allChatsOnboarding2 = ImageAsset(name: "all_chats_onboarding2") - internal static let allChatsOnboarding3 = ImageAsset(name: "all_chats_onboarding3") internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark") internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo") internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1b475b9c0..44ea603d0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -211,38 +211,6 @@ public class VectorL10n: NSObject { public static var allChatsNothingFoundPlaceholderTitle: String { return VectorL10n.tr("Vector", "all_chats_nothing_found_placeholder_title") } - /// To simplify your Element, tabs are now optional. Manage them using the top-right menu. - public static var allChatsOnboardingPageMessage1: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message1") - } - /// Access your Spaces (bottom-left) faster and easier than ever before. - public static var allChatsOnboardingPageMessage2: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message2") - } - /// Tap your profile to let us know what you think. - public static var allChatsOnboardingPageMessage3: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_message3") - } - /// Welcome to a new view! - public static var allChatsOnboardingPageTitle1: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title1") - } - /// Access Spaces - public static var allChatsOnboardingPageTitle2: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title2") - } - /// Give Feedback - public static var allChatsOnboardingPageTitle3: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_page_title3") - } - /// What's new - public static var allChatsOnboardingTitle: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_title") - } - /// Try it out - public static var allChatsOnboardingTryIt: String { - return VectorL10n.tr("Vector", "all_chats_onboarding_try_it") - } /// Chats public static var allChatsSectionTitle: String { return VectorL10n.tr("Vector", "all_chats_section_title") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index d9e64a1af..c6617ad10 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -408,11 +408,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults) var lastNumberOfTrackedSpaces: Int? - // MARK: - All Chats Onboarding - - @UserDefault(key: "allChatsOnboardingHasBeenDisplayed", defaultValue: false, storage: defaults) - var allChatsOnboardingHasBeenDisplayed - } // MARK: - RiotSettings notification constants diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 7bf51dd46..a28ee037f 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2216,9 +2216,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Logout all matrix account [[MXKAccountManager sharedManager] logoutWithCompletion:^{ - // We reset allChatsOnboardingHasBeenDisplayed flag on logout - RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = NO; - if (completion) { completion (YES); diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 4b7ff566c..d6bef4e8e 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -70,8 +70,6 @@ class AllChatsViewController: HomeViewController { private var isOnboardingCoordinatorPreparing: Bool = false - private var allChatsOnboardingCoordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter? - private var theme: Theme { ThemeService.shared().theme } @@ -181,10 +179,6 @@ class AllChatsViewController: HomeViewController { } AppDelegate.theDelegate().checkAppVersion() - - if BuildSettings.newAppLayoutEnabled && !RiotSettings.shared.allChatsOnboardingHasBeenDisplayed { - self.showAllChatsOnboardingScreen() - } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -674,20 +668,6 @@ class AllChatsViewController: HomeViewController { self.navigationController?.pushViewController(invitesViewController, animated: true) } - private func showAllChatsOnboardingScreen() { - let allChatsOnboardingCoordinatorBridgePresenter = AllChatsOnboardingCoordinatorBridgePresenter() - allChatsOnboardingCoordinatorBridgePresenter.completion = { [weak self] in - RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = true - - guard let self = self else { return } - self.allChatsOnboardingCoordinatorBridgePresenter?.dismiss(animated: true, completion: { - self.allChatsOnboardingCoordinatorBridgePresenter = nil - }) - } - - allChatsOnboardingCoordinatorBridgePresenter.present(from: self, animated: true) - self.allChatsOnboardingCoordinatorBridgePresenter = allChatsOnboardingCoordinatorBridgePresenter - } } private extension AllChatsViewController { diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift deleted file mode 100644 index 76beb1205..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit - -// MARK: - Coordinator - -// MARK: View model - -enum AllChatsOnboardingViewModelResult { - case cancel -} - -// MARK: View - -struct AllChatsOnboardingPageData: Identifiable { - let id = UUID().uuidString - let image: UIImage - let title: String - let message: String -} - -struct AllChatsOnboardingViewState: BindableState { - let pages: [AllChatsOnboardingPageData] -} - -enum AllChatsOnboardingViewAction { - case cancel -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift deleted file mode 100644 index 0ba19872a..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Combine -import SwiftUI - -typealias AllChatsOnboardingViewModelType = StateStoreViewModel - -class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol { - // MARK: - Properties - - // MARK: Private - - // MARK: Public - - var completion: ((AllChatsOnboardingViewModelResult) -> Void)? - - // MARK: - Setup - - static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol { - AllChatsOnboardingViewModel() - } - - private init() { - super.init(initialViewState: Self.defaultState()) - } - - private static func defaultState() -> AllChatsOnboardingViewState { - AllChatsOnboardingViewState(pages: [ - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding1.image, - title: VectorL10n.allChatsOnboardingPageTitle1, - message: VectorL10n.allChatsOnboardingPageMessage1), - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding2.image, - title: VectorL10n.allChatsOnboardingPageTitle2, - message: VectorL10n.allChatsOnboardingPageMessage2), - AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding3.image, - title: VectorL10n.allChatsOnboardingPageTitle3, - message: VectorL10n.allChatsOnboardingPageMessage3) - ]) - } - - // MARK: - Public - - override func process(viewAction: AllChatsOnboardingViewAction) { - switch viewAction { - case .cancel: - completion?(.cancel) - } - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift deleted file mode 100644 index dd963c407..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -protocol AllChatsOnboardingViewModelProtocol { - var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set } - static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol - var context: AllChatsOnboardingViewModelType.Context { get } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift deleted file mode 100644 index df189c144..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import CommonKit -import SwiftUI - -/// All Chats onboarding screen -final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { - // MARK: - Properties - - // MARK: Private - - private let hostingController: UIViewController - private var viewModel: AllChatsOnboardingViewModelProtocol - - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? - - // MARK: - Setup - - override init() { - let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel() - let view = AllChatsOnboarding(viewModel: viewModel.context) - self.viewModel = viewModel - hostingController = VectorHostingController(rootView: view) - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) - - super.init() - - hostingController.presentationController?.delegate = self - } - - // MARK: - Public - - func start() { - MXLog.debug("[AllChatsOnboardingCoordinator] did start.") - viewModel.completion = { [weak self] result in - guard let self = self else { return } - MXLog.debug("[AllChatsOnboardingCoordinator] AllChatsOnboardingViewModel did complete with result: \(result).") - switch result { - case .cancel: - self.completion?() - } - } - } - - func toPresentable() -> UIViewController { - hostingController - } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - loadingIndicator = nil - } -} - -// MARK: - UIAdaptivePresentationControllerDelegate - -extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - completion?() - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift deleted file mode 100644 index 75977054d..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -@objc protocol AllChatsOnboardingCoordinatorBridgePresenterDelegate { - func allChatsOnboardingCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter) -} - -/// `AllChatsOnboardingCoordinatorBridgePresenter` enables to start `AllChatsOnboardingCoordinator` from a view controller. -/// This bridge is used while waiting for global usage of coordinator pattern. -/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). -/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. -@objcMembers -final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { - // MARK: - Properties - - // MARK: Private - - private var coordinator: AllChatsOnboardingCoordinator? - - // MARK: Public - - var completion: (() -> Void)? - - // MARK: - Public - - func present(from viewController: UIViewController, animated: Bool) { - let coordinator = AllChatsOnboardingCoordinator() - coordinator.completion = { [weak self] in - guard let self = self else { return } - self.completion?() - } - let presentable = coordinator.toPresentable() - viewController.present(presentable, animated: animated, completion: nil) - coordinator.start() - - self.coordinator = coordinator - } - - func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = coordinator else { - return - } - coordinator.toPresentable().dismiss(animated: animated) { - self.coordinator = nil - completion?() - } - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift deleted file mode 100644 index 513cc55a3..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AllChatsOnboarding: View { - // MARK: - Properties - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - @State private var selectedTab = 0 - - // MARK: Public - - @ObservedObject var viewModel: AllChatsOnboardingViewModel.Context - - var body: some View { - VStack { - Text(VectorL10n.allChatsOnboardingTitle) - .font(theme.fonts.title3SB) - .foregroundColor(theme.colors.primaryContent) - .padding() - TabView(selection: $selectedTab) { - ForEach(viewModel.viewState.pages.indices, id: \.self) { index in - let page = viewModel.viewState.pages[index] - AllChatsOnboardingPage(image: page.image, - title: page.title, - message: page.message) - .tag(index) - } - } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) - .indexViewStyle(.page(backgroundDisplayMode: .always)) - - Button { onCallToAction() } label: { - Text(selectedTab == viewModel.viewState.pages.count - 1 ? VectorL10n.allChatsOnboardingTryIt : VectorL10n.next) - .animation(nil) - } - .buttonStyle(PrimaryActionButtonStyle()) - .padding() - } - .background(theme.colors.background.ignoresSafeArea()) - .frame(maxHeight: .infinity) - } - - // MARK: - Private - - private func onCallToAction() { - if selectedTab == viewModel.viewState.pages.count - 1 { - viewModel.send(viewAction: .cancel) - } else { - withAnimation { - selectedTab += 1 - } - } - } -} - -// MARK: - Previews - -struct AllChatsOnboarding_Previews: PreviewProvider { - static var previews: some View { - AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.light).preferredColorScheme(.light) - AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.dark).preferredColorScheme(.dark) - } -} diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift deleted file mode 100644 index c6a5f06fa..000000000 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AllChatsOnboardingPage: View { - // MARK: - Properties - - let image: UIImage - let title: String - let message: String - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - - var body: some View { - VStack { - Spacer() - Image(uiImage: image) - Spacer() - Text(title) - .font(theme.fonts.title2B) - .foregroundColor(theme.colors.primaryContent) - .padding(.bottom, 16) - Text(message) - .multilineTextAlignment(.center) - .font(theme.fonts.callout) - .foregroundColor(theme.colors.primaryContent) - Spacer() - } - .padding(.horizontal) - } -} - -// MARK: - Previews - -struct AllChatsOnboardingPage_Previews: PreviewProvider { - static var previews: some View { - preview.theme(.light).preferredColorScheme(.light) - preview.theme(.dark).preferredColorScheme(.dark) - } - - private static var preview: some View { - AllChatsOnboardingPage(image: Asset.Images.allChatsOnboarding1.image, - title: VectorL10n.allChatsOnboardingPageTitle1, - message: VectorL10n.allChatsOnboardingPageMessage1) - } -} diff --git a/changelog.d/7298.change b/changelog.d/7298.change new file mode 100644 index 000000000..a7f7a74ce --- /dev/null +++ b/changelog.d/7298.change @@ -0,0 +1 @@ +App Layout: Removed the onboarding flow From 9199407f2fda1ef4e8c13198d503da56bddf11fe Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Wed, 25 Jan 2023 10:27:21 +0100 Subject: [PATCH 205/468] added view poll in timeline button --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 4 +++ .../Coordinator/PollHistoryCoordinator.swift | 4 +++ .../PollHistoryDetailCoordinator.swift | 5 +++- .../PollHistoryDetailModels.swift | 2 ++ .../PollHistoryDetailViewModel.swift | 2 ++ .../View/PollHistoryDetail.swift | 30 ++++++++++++++----- 7 files changed, 39 insertions(+), 10 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e181a16c5..d0cff8767 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2311,7 +2311,7 @@ Tap the + to start adding people."; "poll_history_no_past_poll_text" = "There are no past polls in this room"; "poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months"; "poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months"; - +"poll_history_detail_view_in_timeline" = "View poll in timeline"; // MARK: - Polls "poll_edit_form_create_poll" = "Create poll"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4e7089027..456b9177f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4851,6 +4851,10 @@ public class VectorL10n: NSObject { public static var pollHistoryActiveSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_active_segment_title") } + /// View poll in timeline + public static var pollHistoryDetailViewInTimeline: String { + return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline") + } /// Displaying polls public static var pollHistoryLoadingText: String { return VectorL10n.tr("Vector", "poll_history_loading_text") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 2c3f2e13a..42ad84f54 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -69,6 +69,10 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { case .dismiss: self.toPresentable().dismiss(animated: true) self.remove(childCoordinator: coordinator) + case .viewInTimeline: + self.toPresentable().dismiss(animated: true) + self.remove(childCoordinator: coordinator) + // TODO: go back in timeline } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index 2b68d31ae..5f98622a9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -41,6 +41,7 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { init(parameters: PollHistoryDetailCoordinatorParameters) throws { self.parameters = parameters let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.session, room: parameters.room, pollEvent: parameters.event)) + let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails, timelineViewModel: timelinePollCoordinator.viewModel) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel @@ -48,12 +49,14 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { pollHistoryDetailHostingController = VectorHostingController(rootView: view) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryDetailHostingController) - + self.add(childCoordinator: timelinePollCoordinator) viewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .dismiss: self.completion?(.dismiss) + case .viewInTimeline: + self.completion?(.viewInTimeline) } } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 7be630114..afee15af1 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -22,6 +22,7 @@ typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult enum PollHistoryDetailViewModelResult { case dismiss + case viewInTimeline } // MARK: View model @@ -37,4 +38,5 @@ struct PollHistoryDetailViewState: BindableState { enum PollHistoryDetailViewAction { case dismiss + case viewInTimeline } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index 3566cdf87..dfc5f14c7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -39,6 +39,8 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet switch viewAction { case .dismiss: completion?(.dismiss) + case .viewInTimeline: + completion?(.viewInTimeline) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index c50641c6c..982b4dcd7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -47,21 +47,35 @@ struct PollHistoryDetail: View { } private var content: some View { let timelineViewModel = viewModel.viewState.timelineViewModel - return TimelinePollView(viewModel: timelineViewModel.context) - .navigationTitle(navigationTitle) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .navigationBarItems(leading: btnBack) + return VStack { + TimelinePollView(viewModel: timelineViewModel.context) + .navigationTitle(navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: btnBack) + viewInTimeline + } } - private var btnBack : some View { Button(action: { - viewModel.send(viewAction: .dismiss) + private var btnBack : some View { + Button(action: { + viewModel.send(viewAction: .dismiss) }) { - HStack { Image(systemName: "xmark") //"chevron.left" .aspectRatio(contentMode: .fit) .foregroundColor(theme.colors.accent) + } + } + + private var viewInTimeline: some View { + HStack { + Button { + viewModel.send(viewAction: .viewInTimeline) + } label: { + Text(VectorL10n.pollHistoryDetailViewInTimeline) } + .accentColor(theme.colors.accent) + Spacer() } } From 63faaaf19dd494a39b2b8a9d69c702fa953d3257 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Wed, 25 Jan 2023 11:05:54 +0100 Subject: [PATCH 206/468] room set manually unread style without notification count --- Riot/Modules/Common/Recents/Views/RecentTableViewCell.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index 750594020..d1ac3d914 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -69,6 +69,7 @@ self.missedNotifAndUnreadIndicator.hidden = YES; self.missedNotifAndUnreadBadgeBgView.hidden = YES; self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0; + self.missedNotifAndUnreadBadgeLabel.text = @""; roomCellData = (id)cellData; if (roomCellData) @@ -93,10 +94,10 @@ // Notify unreads and bing if (roomCellData.hasUnread) { - self.missedNotifAndUnreadIndicator.hidden = NO; if (0 < roomCellData.notificationCount) { + self.missedNotifAndUnreadIndicator.hidden = NO; self.missedNotifAndUnreadIndicator.backgroundColor = roomCellData.highlightCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor; self.missedNotifAndUnreadBadgeBgView.hidden = NO; @@ -109,7 +110,9 @@ } else { - self.missedNotifAndUnreadIndicator.backgroundColor = ThemeService.shared.theme.unreadRoomIndentColor; + self.missedNotifAndUnreadBadgeBgView.hidden = NO; + self.missedNotifAndUnreadBadgeBgView.backgroundColor = ThemeService.shared.theme.tintColor; + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 20; } // Use bold font for the room title From 215dbf1e441e436beca956bec0d6df168c2e297e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 11:41:16 +0100 Subject: [PATCH 207/468] Fix TimelinePollAnswerOptionButton layout --- .../View/TimelinePollAnswerOptionButton.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 6b0765a49..1488911bd 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -58,17 +58,19 @@ struct TimelinePollAnswerOptionButton: View { .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label") + .frame(maxWidth: .infinity, alignment: .leading) - if poll.closed, answerOption.winner { - Spacer() - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - - if poll.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") + HStack(spacing: 6) { + if poll.closed, answerOption.winner { + Image(uiImage: Asset.Images.pollWinnerIcon.image) + } + + 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") + } } } From 412452952cf1ed6c169dd369944e304775bf6cad Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 12:05:28 +0100 Subject: [PATCH 208/468] Support load more in PollHistoryService --- .../Room/PollHistory/PollHistoryModels.swift | 1 + .../PollHistory/PollHistoryViewModel.swift | 21 +++++++++++++++++++ .../MatrixSDK/PollHistoryService.swift | 20 ++++++++++++++---- .../Room/PollHistory/View/PollHistory.swift | 2 +- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 7331dc3ed..542f78f4a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -49,4 +49,5 @@ struct PollHistoryViewState: BindableState { enum PollHistoryViewAction { case viewAppeared case segmentDidChange + case loadMoreContent } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index eed1e6c6c..995464ed8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -40,6 +40,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel fetchFirstBatch() case .segmentDidChange: updateViewState() + case .loadMoreContent: + fetchMoreContent() } } } @@ -61,6 +63,21 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) } + func fetchMoreContent() { + state.isLoading = true + + pollService + .nextBatch() + .sink { [weak self] _ in + #warning("Handle errors") + self?.state.isLoading = false + } receiveValue: { [weak self] poll in + self?.add(poll: poll) + self?.updateViewState() + } + .store(in: &subcriptions) + } + func setupUpdateSubscriptions() { subcriptions.removeAll() @@ -88,6 +105,10 @@ private extension PollHistoryViewModel { polls?[pollIndex] = poll } + func add(poll: TimelinePollDetails) { + polls?.append(poll) + } + func updateViewState() { let renderedPolls: [TimelinePollDetails]? diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index a57731cdd..2c684a598 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -28,7 +28,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let pollErrorsSubject: PassthroughSubject = .init() private var pollAggregators: [String: PollAggregator] = [:] - private var targetTimestamp: Date + private var targetTimestamp: Date? private var oldestEventDate: Date = .distantFuture private var currentBatchSubject: PassthroughSubject? @@ -44,7 +44,6 @@ final class PollHistoryService: PollHistoryServiceProtocol { self.room = room self.chunkSizeInDays = chunkSizeInDays timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) - targetTimestamp = Date().addingTimeInterval(-TimeInterval(chunkSizeInDays) * Constants.oneDayInSeconds) setup(timeline: timeline) } @@ -56,7 +55,6 @@ final class PollHistoryService: PollHistoryServiceProtocol { private extension PollHistoryService { enum Constants { static let pageSize: UInt = 500 - static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } func setup(timeline: MXEventTimeline) { @@ -74,6 +72,9 @@ private extension PollHistoryService { } func startPagination() -> AnyPublisher { + let startingTimestamp = targetTimestamp ?? .init() + targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) + let batchSubject = PassthroughSubject() currentBatchSubject = batchSubject @@ -125,7 +126,18 @@ private extension PollHistoryService { } var timestampTargetReached: Bool { - oldestEventDate <= targetTimestamp + guard let targetTimestamp = targetTimestamp else { + return true + } + return oldestEventDate <= targetTimestamp + } +} + +private extension Date { + private static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 + + func subtractingDays(_ days: UInt) -> Date { + addingTimeInterval(-TimeInterval(days) * Self.oneDayInSeconds) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 49ce11a5a..163918bfa 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -80,7 +80,7 @@ struct PollHistory: View { } Button { - #warning("handle action in next ticket") + viewModel.send(viewAction: .loadMoreContent) } label: { Text(VectorL10n.pollHistoryLoadMore) .font(theme.fonts.body) From 7582668aa845c0b331c2462c3963a4a5a936111e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 12:35:51 +0100 Subject: [PATCH 209/468] Handle number of batches / last batch --- .../Room/PollHistory/PollHistoryModels.swift | 1 + .../PollHistory/PollHistoryViewModel.swift | 25 +++++++++++++------ .../MatrixSDK/PollHistoryService.swift | 4 +++ .../Service/Mock/MockPollHistoryService.swift | 18 +++++++------ .../Service/PollHistoryServiceProtocol.swift | 4 +++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 542f78f4a..d861959c7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -44,6 +44,7 @@ struct PollHistoryViewState: BindableState { var isLoading = false var canLoadMoreContent = true var polls: [TimelinePollDetails]? + var numberOfFetchedBatches: UInt = 0 } enum PollHistoryViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 995464ed8..91a8201d5 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -29,6 +29,7 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) { self.pollService = pollService super.init(initialViewState: PollHistoryViewState(mode: mode)) + state.canLoadMoreContent = pollService.hasNextBatch } // MARK: - Public @@ -53,9 +54,8 @@ private extension PollHistoryViewModel { pollService .nextBatch() .collect() - .sink { [weak self] _ in - #warning("Handle errors") - self?.state.isLoading = false + .sink { [weak self] completion in + self?.handleBatchEnded(completion: completion) } receiveValue: { [weak self] polls in self?.polls = polls self?.updateViewState() @@ -68,9 +68,8 @@ private extension PollHistoryViewModel { pollService .nextBatch() - .sink { [weak self] _ in - #warning("Handle errors") - self?.state.isLoading = false + .sink { [weak self] completion in + self?.handleBatchEnded(completion: completion) } receiveValue: { [weak self] poll in self?.add(poll: poll) self?.updateViewState() @@ -78,6 +77,18 @@ private extension PollHistoryViewModel { .store(in: &subcriptions) } + func handleBatchEnded(completion: Subscribers.Completion) { + state.isLoading = false + state.canLoadMoreContent = pollService.hasNextBatch + + switch completion { + case .finished: + state.numberOfFetchedBatches += 1 + case .failure(_): + #warning("Handle errors") + } + } + func setupUpdateSubscriptions() { subcriptions.removeAll() @@ -125,7 +136,7 @@ private extension PollHistoryViewModel { extension PollHistoryViewModel.Context { var emptyPollsText: String { - let days = PollHistoryConstants.chunkSizeInDays + let days = PollHistoryConstants.chunkSizeInDays * viewState.numberOfFetchedBatches switch (viewState.bindings.mode, viewState.canLoadMoreContent) { case (.active, true): diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 2c684a598..b3fc094c4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -50,6 +50,10 @@ final class PollHistoryService: PollHistoryServiceProtocol { func nextBatch() -> AnyPublisher { currentBatchSubject?.eraseToAnyPublisher() ?? startPagination() } + + var hasNextBatch: Bool { + timeline.canPaginate(.backwards) + } } private extension PollHistoryService { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index acd9543e3..d6b12c3d2 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -17,6 +17,15 @@ import Combine final class MockPollHistoryService: PollHistoryServiceProtocol { + lazy var nextBatchPublisher: AnyPublisher = (activePollsData + pastPollsData) + .publisher + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + func nextBatch() -> AnyPublisher { + nextBatchPublisher + } + var updatesPublisher: AnyPublisher = Empty().eraseToAnyPublisher() var updates: AnyPublisher { updatesPublisher @@ -27,14 +36,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { pollErrorPublisher } - lazy var nextBatchPublisher: AnyPublisher = (activePollsData + pastPollsData) - .publisher - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - func nextBatch() -> AnyPublisher { - nextBatchPublisher - } + var hasNextBatch: Bool = true } private extension MockPollHistoryService { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 637ba393f..85f0a9137 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -27,4 +27,8 @@ protocol PollHistoryServiceProtocol { /// Publishes errors regarding poll aggregations. /// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly. var pollErrors: AnyPublisher { get } + + /// Returns true every time the service can fetch another batch. + /// There is no guarantee the `nextBatch()` returned publisher will publish something anyway. + var hasNextBatch: Bool { get } } From 245758e6ebe7ffe500270991f5035022fd07727c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 12:56:36 +0100 Subject: [PATCH 210/468] Disable load more button when there is no content --- RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 163918bfa..c0dfd5c12 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -85,7 +85,7 @@ struct PollHistory: View { Text(VectorL10n.pollHistoryLoadMore) .font(theme.fonts.body) } - .disabled(viewModel.viewState.isLoading) + .disabled(viewModel.viewState.isLoading || !viewModel.viewState.canLoadMoreContent) } } From a6aabaf9e2889991229f17ba72ac9a0149628693 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 25 Jan 2023 14:15:00 +0100 Subject: [PATCH 211/468] Fix a deadlock when updating the summary of a room that has a voice broadcast --- Riot/Utils/EventFormatter.m | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 21cd496dd..db526607a 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -567,7 +567,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent - voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent roomState:roomState]; + voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent + roomState:roomState]; } } @@ -624,22 +625,8 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent } else { - dispatch_group_t group = dispatch_group_create(); - dispatch_group_enter(group); - - __block MXEvent *voiceBroadcastInfoStartedEvent; - - [session eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId success:^(MXEvent *resultEvent) { - voiceBroadcastInfoStartedEvent = resultEvent; - dispatch_group_leave(group); - } failure:^(NSError *error) { - MXLogErrorDetails(@"[EventFormatter] Fetch eventWithEventId with error = %@", error.description); - dispatch_group_leave(group); - }]; - - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - - return voiceBroadcastInfoStartedEvent; + // Search for the event only in the store to avoid network calls while updating the room summary (this a synchronous process and we cannot delay it). + return [mxSession.store eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId]; } } From 06c326603ad1c46c614617d6d3eeb0b54bbf8d6e Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 25 Jan 2023 14:29:41 +0100 Subject: [PATCH 212/468] Add Towncrier file. --- changelog.d/pr-7300.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7300.bugfix diff --git a/changelog.d/pr-7300.bugfix b/changelog.d/pr-7300.bugfix new file mode 100644 index 000000000..bd546901f --- /dev/null +++ b/changelog.d/pr-7300.bugfix @@ -0,0 +1 @@ +Fix a deadlock when updating the summary of a room that has a voice broadcast. From 95b09afc5180d0e8901a3ddc52aa50bc314a6905 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 15:12:19 +0100 Subject: [PATCH 213/468] Add live synced days --- .../Room/PollHistory/PollHistoryModels.swift | 6 ++++-- .../PollHistory/PollHistoryViewModel.swift | 18 +++++++++++++----- .../MatrixSDK/PollHistoryService.swift | 19 +++++++++++++++---- .../Service/Mock/MockPollHistoryService.swift | 5 +++++ .../Service/PollHistoryServiceProtocol.swift | 4 ++++ 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index d861959c7..e88d98367 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -17,7 +17,8 @@ // MARK: View model enum PollHistoryConstants { - static let chunkSizeInDays: UInt = 30 + static let chunkSizeInDays: UInt = 10 + static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } enum PollHistoryViewModelResult: Equatable { @@ -44,7 +45,8 @@ struct PollHistoryViewState: BindableState { var isLoading = false var canLoadMoreContent = true var polls: [TimelinePollDetails]? - var numberOfFetchedBatches: UInt = 0 + var syncStartDate: Date = .init() + var syncedUpTo: Date = .distantFuture } enum PollHistoryViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 91a8201d5..ef7599830 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -83,7 +83,7 @@ private extension PollHistoryViewModel { switch completion { case .finished: - state.numberOfFetchedBatches += 1 + break case .failure(_): #warning("Handle errors") } @@ -106,6 +106,11 @@ private extension PollHistoryViewModel { #warning("Handle errors") } .store(in: &subcriptions) + + pollService + .fetchedUpTo + .weakAssign(to: \.state.syncedUpTo, on: self) + .store(in: &subcriptions) } func update(poll: TimelinePollDetails) { @@ -136,17 +141,20 @@ private extension PollHistoryViewModel { extension PollHistoryViewModel.Context { var emptyPollsText: String { - let days = PollHistoryConstants.chunkSizeInDays * viewState.numberOfFetchedBatches - switch (viewState.bindings.mode, viewState.canLoadMoreContent) { case (.active, true): - return VectorL10n.pollHistoryNoActivePollPeriodText("\(days)") + return VectorL10n.pollHistoryNoActivePollPeriodText("\(syncedPastDays)") case (.active, false): return VectorL10n.pollHistoryNoActivePollText case (.past, true): - return VectorL10n.pollHistoryNoPastPollPeriodText("\(days)") + return VectorL10n.pollHistoryNoPastPollPeriodText("\(syncedPastDays)") case (.past, false): return VectorL10n.pollHistoryNoPastPollText } } + + var syncedPastDays: UInt { + let timeDelta = max(viewState.syncStartDate.timeIntervalSince(viewState.syncedUpTo), 0) + return UInt((timeDelta / PollHistoryConstants.oneDayInSeconds).rounded()) + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index b3fc094c4..e6569174a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -29,7 +29,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private var pollAggregators: [String: PollAggregator] = [:] private var targetTimestamp: Date? - private var oldestEventDate: Date = .distantFuture + private var oldestEventDateSubject: CurrentValueSubject = .init(Date.distantFuture) private var currentBatchSubject: PassthroughSubject? var updates: AnyPublisher { @@ -54,6 +54,10 @@ final class PollHistoryService: PollHistoryServiceProtocol { var hasNextBatch: Bool { timeline.canPaginate(.backwards) } + + var fetchedUpTo: AnyPublisher { + oldestEventDateSubject.eraseToAnyPublisher() + } } private extension PollHistoryService { @@ -135,13 +139,20 @@ private extension PollHistoryService { } return oldestEventDate <= targetTimestamp } + + var oldestEventDate: Date { + get { + oldestEventDateSubject.value + } + set { + oldestEventDateSubject.send(newValue) + } + } } private extension Date { - private static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 - func subtractingDays(_ days: UInt) -> Date { - addingTimeInterval(-TimeInterval(days) * Self.oneDayInSeconds) + addingTimeInterval(-TimeInterval(days) * PollHistoryConstants.oneDayInSeconds) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index d6b12c3d2..8b4ab063b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -37,6 +37,11 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { } var hasNextBatch: Bool = true + + var fetchedUpToPublisher: AnyPublisher = Just(.init()).eraseToAnyPublisher() + var fetchedUpTo: AnyPublisher { + fetchedUpToPublisher + } } private extension MockPollHistoryService { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 85f0a9137..661c2dce4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -31,4 +31,8 @@ protocol PollHistoryServiceProtocol { /// Returns true every time the service can fetch another batch. /// There is no guarantee the `nextBatch()` returned publisher will publish something anyway. var hasNextBatch: Bool { get } + + /// Publishes the date up to the service is synced (in the past). + /// This date doesn't need to be related with any poll event. + var fetchedUpTo: AnyPublisher { get } } From b0dab86871567df8c795b89325f598577007ed2c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 16:04:47 +0100 Subject: [PATCH 214/468] Disable load more button if needed --- .../Room/PollHistory/View/PollHistory.swift | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index c0dfd5c12..9289740e0 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -73,19 +73,22 @@ struct PollHistory: View { } } + @ViewBuilder private var loadMoreButton: some View { - HStack(spacing: 8) { - if viewModel.viewState.isLoading { - spinner + if viewModel.viewState.canLoadMoreContent { + HStack(spacing: 8) { + if viewModel.viewState.isLoading { + spinner + } + + Button { + viewModel.send(viewAction: .loadMoreContent) + } label: { + Text(VectorL10n.pollHistoryLoadMore) + .font(theme.fonts.body) + } + .disabled(viewModel.viewState.isLoading) } - - Button { - viewModel.send(viewAction: .loadMoreContent) - } label: { - Text(VectorL10n.pollHistoryLoadMore) - .font(theme.fonts.body) - } - .disabled(viewModel.viewState.isLoading || !viewModel.viewState.canLoadMoreContent) } } From 2e2705ed772ecae6dbac7d53709db1898bbb1f77 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 16:06:16 +0100 Subject: [PATCH 215/468] Restore default constants --- RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift | 2 +- .../Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index e88d98367..a20d37205 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -17,7 +17,7 @@ // MARK: View model enum PollHistoryConstants { - static let chunkSizeInDays: UInt = 10 + static let chunkSizeInDays: UInt = 30 static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index e6569174a..830d799b8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -62,7 +62,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private extension PollHistoryService { enum Constants { - static let pageSize: UInt = 500 + static let pageSize: UInt = 250 } func setup(timeline: MXEventTimeline) { From 08fc35d4fe56cf2b7837edc68d7c323d533dd109 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Wed, 25 Jan 2023 16:06:40 +0100 Subject: [PATCH 216/468] fix strings after merge --- Riot/Generated/Strings.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 96ff9ba79..d3a740965 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4822,6 +4822,7 @@ public class VectorL10n: NSObject { /// View poll in timeline public static var pollHistoryDetailViewInTimeline: String { return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline") + } /// Load more polls public static var pollHistoryLoadMore: String { return VectorL10n.tr("Vector", "poll_history_load_more") From 2bc460f7e5e7f20f49fc4962f0a3d2ba79016d23 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 16:11:36 +0100 Subject: [PATCH 217/468] Remove dynamic poll updates --- .../PollHistory/PollHistoryViewModel.swift | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index ef7599830..3d6ba34fc 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -38,17 +38,17 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel switch viewAction { case .viewAppeared: setupUpdateSubscriptions() - fetchFirstBatch() + fetchContent() case .segmentDidChange: updateViewState() case .loadMoreContent: - fetchMoreContent() + fetchContent() } } } private extension PollHistoryViewModel { - func fetchFirstBatch() { + func fetchContent() { state.isLoading = true pollService @@ -57,21 +57,7 @@ private extension PollHistoryViewModel { .sink { [weak self] completion in self?.handleBatchEnded(completion: completion) } receiveValue: { [weak self] polls in - self?.polls = polls - self?.updateViewState() - } - .store(in: &subcriptions) - } - - func fetchMoreContent() { - state.isLoading = true - - pollService - .nextBatch() - .sink { [weak self] completion in - self?.handleBatchEnded(completion: completion) - } receiveValue: { [weak self] poll in - self?.add(poll: poll) + self?.add(polls: polls) self?.updateViewState() } .store(in: &subcriptions) @@ -121,8 +107,8 @@ private extension PollHistoryViewModel { polls?[pollIndex] = poll } - func add(poll: TimelinePollDetails) { - polls?.append(poll) + func add(polls: [TimelinePollDetails]) { + self.polls = (self.polls ?? []) + polls } func updateViewState() { From bbd1ab7a96f6e8cac33fe93dd9029b6e7a59e844 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 16:23:38 +0100 Subject: [PATCH 218/468] Fix pagination reset --- .../Service/MatrixSDK/PollHistoryService.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 830d799b8..57bcd5e9b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -44,7 +44,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { self.room = room self.chunkSizeInDays = chunkSizeInDays timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) - setup(timeline: timeline) + setupTimeline() } func nextBatch() -> AnyPublisher { @@ -65,7 +65,9 @@ private extension PollHistoryService { static let pageSize: UInt = 250 } - func setup(timeline: MXEventTimeline) { + func setupTimeline() { + timeline.resetPagination() + timelineListener = timeline.listenToEvents { [weak self] event, _, _ in if event.eventType == .pollStart { self?.aggregatePoll(pollStartEvent: event) @@ -90,14 +92,13 @@ private extension PollHistoryService { guard let self = self else { return } - self.timeline.resetPagination() - self.paginate(timeline: self.timeline) + self.paginate() } return batchSubject.eraseToAnyPublisher() } - func paginate(timeline: MXEventTimeline) { + func paginate() { timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in guard let self = self else { return @@ -105,8 +106,8 @@ private extension PollHistoryService { switch response { case .success: - if timeline.canPaginate(.backwards), self.timestampTargetReached == false { - self.paginate(timeline: timeline) + if self.timeline.canPaginate(.backwards), self.timestampTargetReached == false { + self.paginate() } else { self.completeBatch(completion: .finished) } From 13b7f78de73178a963a59a6e62fd4c243f423b46 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 23 Jan 2023 16:17:33 +0100 Subject: [PATCH 219/468] Inform the user about decryption errors during a voice broadcast --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++ .../Utils/EventFormatter/MXKEventFormatter.m | 9 +++- .../VoiceBroadcastAggregator.swift | 36 +++++++++++--- .../VoiceBroadcastPlaybackViewModel.swift | 14 +++++- ...BroadcastPlaybackDecryptionErrorView.swift | 47 +++++++++++++++++++ .../View/VoiceBroadcastPlaybackView.swift | 13 +++-- .../VoiceBroadcastPlaybackModels.swift | 5 ++ .../VoiceBroadcastPlaybackScreenState.swift | 5 +- changelog.d/7189.change | 1 + 10 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift create mode 100644 changelog.d/7189.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 49b6fb73c..e20ba1c41 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2225,6 +2225,7 @@ Tap the + to start adding people."; "voice_broadcast_connection_error_title" = "Connection error"; "voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later."; "voice_broadcast_recorder_connection_error" = "Connection error - Recording paused"; +"voice_broadcast_playback_unable_to_decrypt" = "Unable to decrypt this voice broadcast."; // MARK: - Version check diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 44ea603d0..02f908418 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9235,6 +9235,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLockScreenPlaceholder: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder") } + /// Unable to decrypt this voice broadcast. + public static var voiceBroadcastPlaybackUnableToDecrypt: String { + return VectorL10n.tr("Vector", "voice_broadcast_playback_unable_to_decrypt") + } /// Connection error - Recording paused public static var voiceBroadcastRecorderConnectionError: String { return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error") diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 422e08990..77ba974fd 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1053,8 +1053,13 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode) { - // Make the unknown inbound session id error description more user friendly - errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + // Hide the decryption error for event related to another one (like voicebroadcast chunks) + if ([event.relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { + displayText = nil; + } else { + // Make the unknown inbound session id error description more user friendly + errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + } } else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorDuplicateMessageIndexCode) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index 39264b42c..4522df16c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -35,6 +35,7 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject { func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) } /** @@ -58,6 +59,7 @@ public class VoiceBroadcastAggregator { private var referenceEventsListener: Any? private var events: [MXEvent] = [] + private var undecryptableEvents: Set = [] public private(set) var voiceBroadcast: VoiceBroadcast! { didSet { @@ -84,7 +86,7 @@ public class VoiceBroadcastAggregator { try buildVoiceBroadcastStartContent() } - + private func buildVoiceBroadcastStartContent() throws { guard let event = session.store.event(withEventId: voiceBroadcastStartEventId, inRoom: room.roomId), let eventContent = VoiceBroadcastInfo(fromJSON: event.content), @@ -118,7 +120,11 @@ public class VoiceBroadcastAggregator { @objc private func eventDidDecrypt(sender: Notification) { guard let event = sender.object as? MXEvent else { return } - + + if undecryptableEvents.remove(event) != nil { + delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: undecryptableEvents) + } + self.handleEvent(event: event) } @@ -138,8 +144,19 @@ public class VoiceBroadcastAggregator { private func updateVoiceBroadcast(event: MXEvent) { guard event.sender == self.voiceBroadcastSenderId, let relatedEventId = event.relatesTo?.eventId, - relatedEventId == self.voiceBroadcastStartEventId, - event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { + relatedEventId == self.voiceBroadcastStartEventId else { + return + } + + // Handle decryption errors + if event.decryptionError != nil { + self.undecryptableEvents.insert(event) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + + return + } + + guard event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { return } @@ -192,15 +209,22 @@ public class VoiceBroadcastAggregator { } self.events.removeAll() + self.undecryptableEvents.removeAll() self.voiceBroadcastLastChunkSequence = 0 let filteredChunk = response.chunk.filter { event in event.sender == self.voiceBroadcastSenderId && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil } - self.events.append(contentsOf: filteredChunk) - + + let decryptionFailure = response.chunk.filter { event in + event.sender == self.voiceBroadcastSenderId && + event.decryptionError != nil + } + self.undecryptableEvents.formUnion(decryptionFailure) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage] self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes, onEvent: { [weak self] event, direction, roomState in self?.handleEvent(event: event, direction: direction, roomState: roomState) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index ffe713e73..fd1ca9157 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -111,7 +111,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic broadcastState: voiceBroadcastAggregator.voiceBroadcastState, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false, canMoveForward: false, canMoveBackward: false), - bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) + bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), + decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0)) super.init(initialViewState: viewState) displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -486,6 +487,17 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { handleVoiceBroadcastChunksProcessing() } } + + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) { + state.decryptionState.errorCount = events.count + if events.count > 0 { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] voice broadcast decryption error count: \(events.count)/\(aggregator.voiceBroadcast.chunks.count)") + + if [.playing, .buffering].contains(state.playbackState) { + pause() + } + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift new file mode 100644 index 000000000..598bde5c3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift @@ -0,0 +1,47 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VoiceBroadcastPlaybackDecryptionErrorView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var body: some View { + ZStack { + HStack(spacing: 0) { + Image(uiImage: Asset.Images.errorIcon.image) + .frame(width: 40, height: 40) + Text(VectorL10n.voiceBroadcastPlaybackUnableToDecrypt) + .multilineTextAlignment(.center) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct VoiceBroadcastPlaybackDecryptionErrorView_Previews: PreviewProvider { + static var previews: some View { + VoiceBroadcastPlaybackDecryptionErrorView() + } +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 09ed1ff44..6ac146ce2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -91,7 +91,7 @@ struct VoiceBroadcastPlaybackView: View { } } }.frame(maxWidth: .infinity, alignment: .leading) - + if viewModel.viewState.broadcastState != .stopped { Label { Text(VectorL10n.voiceBroadcastLive) @@ -109,7 +109,12 @@ struct VoiceBroadcastPlaybackView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 4.0, trailing: 0.0)) - if viewModel.viewState.playbackState == .error { + if viewModel.viewState.decryptionState.errorCount > 0 { + VoiceBroadcastPlaybackDecryptionErrorView() + .fixedSize(horizontal: false, vertical: true) + .accessibilityIdentifier("decryptionErrorView") + } + else if viewModel.viewState.playbackState == .error { VoiceBroadcastPlaybackErrorView() } else { HStack (spacing: 34.0) { @@ -156,8 +161,8 @@ struct VoiceBroadcastPlaybackView: View { } VoiceBroadcastSlider(value: $viewModel.progress, - minValue: 0.0, - maxValue: viewModel.viewState.playingState.duration) { didChange in + minValue: 0.0, + maxValue: viewModel.viewState.playingState.duration) { didChange in viewModel.send(viewAction: .sliderChange(didChange: didChange)) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 488b65c1d..aeb1f4f61 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -48,12 +48,17 @@ struct VoiceBroadcastPlayingState { var canMoveBackward: Bool } +struct VoiceBroadcastPlaybackDecryptionState { + var errorCount: Int +} + struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails var broadcastState: VoiceBroadcastInfoState var playbackState: VoiceBroadcastPlaybackState var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings + var decryptionState: VoiceBroadcastPlaybackDecryptionState } struct VoiceBroadcastPlaybackViewStateBindings { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 306a5be8c..84f210d81 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -43,11 +43,12 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0))) return ( [false, viewModel], - AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context)) + AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/changelog.d/7189.change b/changelog.d/7189.change new file mode 100644 index 000000000..a9acc4ba3 --- /dev/null +++ b/changelog.d/7189.change @@ -0,0 +1 @@ +Voice Broadcast: Inform the user about decryption errors during a voice broadcast. From d2c6506ac63c90817149ebb4d878c800793abfcf Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 17:52:39 +0100 Subject: [PATCH 220/468] Handle live polls --- .../PollHistory/PollHistoryViewModel.swift | 8 +++ .../MatrixSDK/PollHistoryService.swift | 56 +++++++++++++++++-- .../Service/Mock/MockPollHistoryService.swift | 5 ++ .../Service/PollHistoryServiceProtocol.swift | 3 + 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 3d6ba34fc..13c4418ea 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -97,6 +97,14 @@ private extension PollHistoryViewModel { .fetchedUpTo .weakAssign(to: \.state.syncedUpTo, on: self) .store(in: &subcriptions) + + pollService + .livePolls + .sink { [weak self] livePoll in + self?.add(polls: [livePoll]) + self?.updateViewState() + } + .store(in: &subcriptions) } func update(poll: TimelinePollDetails) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 57bcd5e9b..bceaed266 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -22,15 +22,26 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let room: MXRoom private let timeline: MXEventTimeline private let chunkSizeInDays: UInt - private var timelineListener: Any? + private var timelineListener: Any? + private var roomListener: Any? + + // polls aggregation + private var pollAggregators: [String: PollAggregator] = [:] + private var livePollsIDs: Set = .init() + private var publishedPollsIDs: Set = .init() + + // polls + private var currentBatchSubject: PassthroughSubject? + private var livePollsSubject: PassthroughSubject = .init() + + // polls updates private let updatesSubject: PassthroughSubject = .init() private let pollErrorsSubject: PassthroughSubject = .init() - - private var pollAggregators: [String: PollAggregator] = [:] + + // timestamps private var targetTimestamp: Date? private var oldestEventDateSubject: CurrentValueSubject = .init(Date.distantFuture) - private var currentBatchSubject: PassthroughSubject? var updates: AnyPublisher { updatesSubject.eraseToAnyPublisher() @@ -45,6 +56,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { self.chunkSizeInDays = chunkSizeInDays timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil) setupTimeline() + setupLiveUpdates() } func nextBatch() -> AnyPublisher { @@ -58,6 +70,17 @@ final class PollHistoryService: PollHistoryServiceProtocol { var fetchedUpTo: AnyPublisher { oldestEventDateSubject.eraseToAnyPublisher() } + + var livePolls: AnyPublisher { + livePollsSubject.eraseToAnyPublisher() + } + + deinit { + guard let roomListener = roomListener else { + return + } + room.removeListener(roomListener) + } } private extension PollHistoryService { @@ -77,6 +100,15 @@ private extension PollHistoryService { } } + func setupLiveUpdates() { + roomListener = room.listen(toEventsOfTypes: [kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381]) { [weak self] event, _, _ in + if event.eventType == .pollStart { + self?.livePollsIDs.insert(event.eventId) + self?.aggregatePoll(pollStartEvent: event) + } + } + } + func updateTimestamp(event: MXEvent) { oldestEventDate = min(event.originServerDate, oldestEventDate) } @@ -169,7 +201,21 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {} func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - currentBatchSubject?.send(.init(poll: aggregator.poll, represent: .started)) + let pollID = aggregator.poll.id + + guard publishedPollsIDs.contains(pollID) == false else { + return + } + + publishedPollsIDs.insert(pollID) + + let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) + + if livePollsIDs.contains(newPoll.id) { + livePollsSubject.send(newPoll) + } else { + currentBatchSubject?.send(newPoll) + } } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 8b4ab063b..89bee9b83 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -42,6 +42,11 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { var fetchedUpTo: AnyPublisher { fetchedUpToPublisher } + + var livePollsPublisher: AnyPublisher = Empty().eraseToAnyPublisher() + var livePolls: AnyPublisher { + livePollsPublisher + } } private extension MockPollHistoryService { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 661c2dce4..6335d5d63 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -28,6 +28,9 @@ protocol PollHistoryServiceProtocol { /// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly. var pollErrors: AnyPublisher { get } + /// Publishes live polls not related with the current batch. + var livePolls: AnyPublisher { get } + /// Returns true every time the service can fetch another batch. /// There is no guarantee the `nextBatch()` returned publisher will publish something anyway. var hasNextBatch: Bool { get } From 9408efebbcbbca45bcce1c116d9fc5cc091a37d4 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 18:19:48 +0100 Subject: [PATCH 221/468] Cleanup code --- .../MatrixSDK/PollHistoryService.swift | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index bceaed266..395972a47 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -27,9 +27,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { private var roomListener: Any? // polls aggregation - private var pollAggregators: [String: PollAggregator] = [:] - private var livePollsIDs: Set = .init() - private var publishedPollsIDs: Set = .init() + private var pollAggregationContexts: [String: PollAggregationContext] = [:] // polls private var currentBatchSubject: PassthroughSubject? @@ -81,6 +79,18 @@ final class PollHistoryService: PollHistoryServiceProtocol { } room.removeListener(roomListener) } + + class PollAggregationContext { + var pollAggregator: PollAggregator? + let isLivePoll: Bool + var published: Bool = false + + init(pollAggregator: PollAggregator? = nil, isLivePoll: Bool, published: Bool = false) { + self.pollAggregator = pollAggregator + self.isLivePoll = isLivePoll + self.published = published + } + } } private extension PollHistoryService { @@ -93,7 +103,7 @@ private extension PollHistoryService { timelineListener = timeline.listenToEvents { [weak self] event, _, _ in if event.eventType == .pollStart { - self?.aggregatePoll(pollStartEvent: event) + self?.aggregatePoll(pollStartEvent: event, isLivePoll: false) } self?.updateTimestamp(event: event) @@ -103,8 +113,7 @@ private extension PollHistoryService { func setupLiveUpdates() { roomListener = room.listen(toEventsOfTypes: [kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381]) { [weak self] event, _, _ in if event.eventType == .pollStart { - self?.livePollsIDs.insert(event.eventId) - self?.aggregatePoll(pollStartEvent: event) + self?.aggregatePoll(pollStartEvent: event, isLivePoll: true) } } } @@ -154,16 +163,21 @@ private extension PollHistoryService { currentBatchSubject = nil } - func aggregatePoll(pollStartEvent: MXEvent) { - guard pollAggregators[pollStartEvent.eventId] == nil else { + func aggregatePoll(pollStartEvent: MXEvent, isLivePoll: Bool) { + let eventId: String = pollStartEvent.eventId + + guard pollAggregationContexts[eventId] == nil else { return } - guard let aggregator = try? PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) else { - return - } + let newContext: PollAggregationContext = .init(isLivePoll: isLivePoll) + pollAggregationContexts[eventId] = newContext - pollAggregators[pollStartEvent.eventId] = aggregator + do { + newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) + } catch { + pollAggregationContexts.removeValue(forKey: eventId) + } } var timestampTargetReached: Bool { @@ -201,17 +215,15 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {} func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - let pollID = aggregator.poll.id - - guard publishedPollsIDs.contains(pollID) == false else { + guard let context = pollAggregationContexts[aggregator.poll.id], !context.published else { return } - publishedPollsIDs.insert(pollID) + context.published = true let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) - if livePollsIDs.contains(newPoll.id) { + if context.isLivePoll { livePollsSubject.send(newPoll) } else { currentBatchSubject?.send(newPoll) From 844da02c7400127a1046d50ddb3d7b27a212d096 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 18:40:09 +0100 Subject: [PATCH 222/468] Add alert on error --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../Coordinator/PollHistoryCoordinator.swift | 17 ++++++++++++++++- .../Room/PollHistory/PollHistoryModels.swift | 2 +- .../Room/PollHistory/PollHistoryViewModel.swift | 11 ++--------- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 49b6fb73c..40cc62e9c 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2303,6 +2303,7 @@ Tap the + to start adding people."; "poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months"; "poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months"; "poll_history_load_more" = "Load more polls"; +"poll_history_fetching_error" = "Error fetching polls."; // MARK: - Polls diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 44ea603d0..ab9d80f24 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4819,6 +4819,10 @@ public class VectorL10n: NSObject { public static var pollHistoryActiveSegmentTitle: String { return VectorL10n.tr("Vector", "poll_history_active_segment_title") } + /// Error fetching polls. + public static var pollHistoryFetchingError: String { + return VectorL10n.tr("Vector", "poll_history_fetching_error") + } /// Load more polls public static var pollHistoryLoadMore: String { return VectorL10n.tr("Vector", "poll_history_load_more") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 0311cda4e..e0ee3f54c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -44,7 +44,10 @@ final class PollHistoryCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[PollHistoryCoordinator] did start.") pollHistoryViewModel.completion = { [weak self] result in - self?.completion?() + switch result { + case .genericError: + self?.showErrorAlert() + } } } @@ -52,3 +55,15 @@ final class PollHistoryCoordinator: Coordinator, Presentable { pollHistoryHostingController } } + +private extension PollHistoryCoordinator { + func showErrorAlert() { + let alert = UIAlertController(title: VectorL10n.pollHistoryFetchingError, + message: nil, + preferredStyle: .alert) + + let cancelAction = UIAlertAction(title: VectorL10n.ok, style: .cancel) + alert.addAction(cancelAction) + pollHistoryHostingController.present(alert, animated: true, completion: nil) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index a20d37205..0b6213787 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -22,7 +22,7 @@ enum PollHistoryConstants { } enum PollHistoryViewModelResult: Equatable { - #warning("e.g. show poll detail") + case genericError } // MARK: View diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 13c4418ea..1687cf2f4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -70,8 +70,8 @@ private extension PollHistoryViewModel { switch completion { case .finished: break - case .failure(_): - #warning("Handle errors") + case .failure: + self.completion?(.genericError) } } @@ -86,13 +86,6 @@ private extension PollHistoryViewModel { } .store(in: &subcriptions) - pollService - .pollErrors - .sink { detail in - #warning("Handle errors") - } - .store(in: &subcriptions) - pollService .fetchedUpTo .weakAssign(to: \.state.syncedUpTo, on: self) From 684a5d03dcb7930d7a38b887dff2d5b01774be2e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 19:21:56 +0100 Subject: [PATCH 223/468] Improve MockPollHistoryScreenState --- .../MockPollHistoryScreenState.swift | 25 +++++++++---------- .../Service/Mock/MockPollHistoryService.swift | 6 ++--- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index d939dab2f..6d15ca987 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -26,8 +26,8 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { // mock that screen. case active case past - case activeEmpty - case pastEmpty + case empty + case emptyNoMoreContent case loading /// The associated screen @@ -37,7 +37,7 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let pollHistoryMode: PollHistoryMode + var pollHistoryMode: PollHistoryMode = .active let pollService = MockPollHistoryService() switch self { @@ -45,21 +45,20 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { pollHistoryMode = .active case .past: pollHistoryMode = .past - case .activeEmpty: + case .empty: pollHistoryMode = .active pollService.nextBatchPublisher = Empty(completeImmediately: true, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() - case .pastEmpty: - pollHistoryMode = .past + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() + case .emptyNoMoreContent: + pollService.hasNextBatch = false pollService.nextBatchPublisher = Empty(completeImmediately: true, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() case .loading: - pollHistoryMode = .active pollService.nextBatchPublisher = Empty(completeImmediately: false, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() + outputType: TimelinePollDetails.self, + failureType: Error.self).eraseToAnyPublisher() } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 89bee9b83..a75195a0f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -36,7 +36,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { pollErrorPublisher } - var hasNextBatch: Bool = true + var hasNextBatch = true var fetchedUpToPublisher: AnyPublisher = Just(.init()).eraseToAnyPublisher() var fetchedUpTo: AnyPublisher { @@ -51,7 +51,7 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { private extension MockPollHistoryService { var activePollsData: [TimelinePollDetails] { - (1...10) + (1...3) .map { index in TimelinePollDetails(id: "a\(index)", question: "Do you like the active poll number \(index)?", @@ -68,7 +68,7 @@ private extension MockPollHistoryService { } var pastPollsData: [TimelinePollDetails] { - (1...10) + (1...3) .map { index in TimelinePollDetails(id: "p\(index)", question: "Do you like the active poll number \(index)?", From 74254315f2921a0e6648288da0101dd6d2819d2c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 19:41:48 +0100 Subject: [PATCH 224/468] Add more MockPollHistoryScreenState cases --- .../MockPollHistoryScreenState.swift | 38 ++++++++++++++----- .../Service/Mock/MockPollHistoryService.swift | 12 +++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 6d15ca987..15e217e4f 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -25,8 +25,11 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { // with specific, minimal associated data that will allow you // mock that screen. case active + case activeNoMoreContent case past + case contentLoading case empty + case emptyLoading case emptyNoMoreContent case loading @@ -43,27 +46,34 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { switch self { case .active: pollHistoryMode = .active + case .activeNoMoreContent: + pollHistoryMode = .active + pollService.hasNextBatch = false case .past: pollHistoryMode = .past + case .contentLoading: + pollService.nextBatchPublishers.append(loadingPolls) case .empty: pollHistoryMode = .active - pollService.nextBatchPublisher = Empty(completeImmediately: true, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() + pollService.nextBatchPublishers = [noPolls] + case .emptyLoading: + pollService.nextBatchPublishers = [noPolls, loadingPolls] case .emptyNoMoreContent: pollService.hasNextBatch = false - pollService.nextBatchPublisher = Empty(completeImmediately: true, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() + pollService.nextBatchPublishers = [noPolls] case .loading: - pollService.nextBatchPublisher = Empty(completeImmediately: false, - outputType: TimelinePollDetails.self, - failureType: Error.self).eraseToAnyPublisher() + pollService.nextBatchPublishers = [loadingPolls] } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) // can simulate service and viewModel actions here if needs be. + switch self { + case .contentLoading, .emptyLoading: + viewModel.process(viewAction: .loadMoreContent) + default: + break + } return ( [pollHistoryMode, viewModel], @@ -72,3 +82,13 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { ) } } + +private extension MockPollHistoryScreenState { + var noPolls: AnyPublisher { + Empty(completeImmediately: true).eraseToAnyPublisher() + } + + var loadingPolls: AnyPublisher { + Empty(completeImmediately: false).eraseToAnyPublisher() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index a75195a0f..277c6a647 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -17,13 +17,15 @@ import Combine final class MockPollHistoryService: PollHistoryServiceProtocol { - lazy var nextBatchPublisher: AnyPublisher = (activePollsData + pastPollsData) - .publisher - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + lazy var nextBatchPublishers: [AnyPublisher] = [ + (activePollsData + pastPollsData) + .publisher + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + ] func nextBatch() -> AnyPublisher { - nextBatchPublisher + nextBatchPublishers.isEmpty ? Empty().eraseToAnyPublisher() : nextBatchPublishers.removeFirst() } var updatesPublisher: AnyPublisher = Empty().eraseToAnyPublisher() From fcdf77e48b513c4aecf87b81ee02ed1888400df3 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 19:58:02 +0100 Subject: [PATCH 225/468] Add ui tests --- .../MockPollHistoryScreenState.swift | 2 +- .../Test/UI/PollHistoryUITests.swift | 68 ++++++++++++++----- .../Room/PollHistory/View/PollHistory.swift | 1 + 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index 15e217e4f..e2e8278ea 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -25,8 +25,8 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { // with specific, minimal associated data that will allow you // mock that screen. case active - case activeNoMoreContent case past + case activeNoMoreContent case contentLoading case empty case emptyLoading diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift index 986fd37bd..ddce4978c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift @@ -24,6 +24,7 @@ final class PollHistoryUITests: MockScreenTestCase { let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle] + let loadMoreButton = app.buttons["PollHistory.loadMore"] let winningOption = app.staticTexts["PollListData.winningOption"] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) @@ -31,6 +32,7 @@ final class PollHistoryUITests: MockScreenTestCase { XCTAssertFalse(emptyText.exists) XCTAssertTrue(selectedSegment.exists) XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertTrue(loadMoreButton.exists) XCTAssertFalse(winningOption.exists) } @@ -40,6 +42,7 @@ final class PollHistoryUITests: MockScreenTestCase { let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] + let loadMoreButton = app.buttons["PollHistory.loadMore"] let winningOption = app.buttons["PollAnswerOption0"] XCTAssertEqual(title, VectorL10n.pollHistoryTitle) @@ -47,33 +50,66 @@ final class PollHistoryUITests: MockScreenTestCase { XCTAssertFalse(emptyText.exists) XCTAssertTrue(selectedSegment.exists) XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) + XCTAssertTrue(loadMoreButton.exists) XCTAssertTrue(winningOption.exists) } - func testPastPollHistoryIsEmpty() { - app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title) + func testActivePollHistoryHasContentAndCantLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeNoMoreContent.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertFalse(loadMoreButton.exists) + } + + func testActivePollHistoryHasContentAndCanLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.contentLoading.title) let title = app.navigationBars.firstMatch.identifier let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] - let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle] - let winningOption = app.staticTexts["PollListData.winningOption"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] - XCTAssertEqual(title, VectorL10n.pollHistoryTitle) - XCTAssertFalse(items.exists) - XCTAssertTrue(emptyText.exists) - XCTAssertTrue(selectedSegment.exists) - XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected) - XCTAssertFalse(winningOption.exists) + XCTAssertTrue(items.exists) + XCTAssertFalse(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertFalse(loadMoreButton.isEnabled) } - func testLoaderIsShowing() { - app.goToScreenWithIdentifier(MockPollHistoryScreenState.loading.title) - let title = app.navigationBars.firstMatch.identifier - let loadingText = app.staticTexts["PollHistory.loadingText"] + func testActivePollHistoryEmptyAndCanLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.empty.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] - XCTAssertEqual(title, VectorL10n.pollHistoryTitle) XCTAssertFalse(items.exists) - XCTAssertTrue(loadingText.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertTrue(loadMoreButton.isEnabled) + } + + func testActivePollHistoryEmptyAndLoading() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyLoading.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertTrue(loadMoreButton.exists) + XCTAssertFalse(loadMoreButton.isEnabled) + } + + func testActivePollHistoryEmptyAndCantLoadMore() { + app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyNoMoreContent.title) + let emptyText = app.staticTexts["PollHistory.emptyText"] + let items = app.staticTexts["PollListItem.title"] + let loadMoreButton = app.buttons["PollHistory.loadMore"] + + XCTAssertFalse(items.exists) + XCTAssertTrue(emptyText.exists) + XCTAssertFalse(loadMoreButton.exists) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 9289740e0..43e1b28e8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -87,6 +87,7 @@ struct PollHistory: View { Text(VectorL10n.pollHistoryLoadMore) .font(theme.fonts.body) } + .accessibilityIdentifier("PollHistory.loadMore") .disabled(viewModel.viewState.isLoading) } } From 214500ab12a93e614fe5f88969232893be38e6c9 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 25 Jan 2023 20:16:24 +0100 Subject: [PATCH 226/468] Add UTs --- .../Test/Unit/PollHistoryViewModelTests.swift | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift index e2eff7475..86487c1d1 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -42,9 +42,11 @@ final class PollHistoryViewModelTests: XCTestCase { func testLoadingStateIsTrueWhileLoading() { XCTAssertFalse(viewModel.state.isLoading) - pollHistoryService.nextBatchPublisher = Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() + pollHistoryService.nextBatchPublishers = [loadingPublisher, emptyPublisher] viewModel.process(viewAction: .viewAppeared) XCTAssertTrue(viewModel.state.isLoading) + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) } func testUpdatesAreHandled() throws { @@ -79,6 +81,14 @@ final class PollHistoryViewModelTests: XCTestCase { let pollDates = try polls.map(\.startDate) XCTAssertEqual(pollDates, pollDates.sorted(by: { $0 > $1 })) } + + func testLivePollsAreHandled() throws { + pollHistoryService.nextBatchPublishers = [emptyPublisher] + pollHistoryService.livePollsPublisher = Just(mockPoll).eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + XCTAssertEqual(viewModel.state.polls?.count, 1) + XCTAssertEqual(viewModel.state.polls?.first?.id, "id") + } } private extension PollHistoryViewModelTests { @@ -87,4 +97,26 @@ private extension PollHistoryViewModelTests { try XCTUnwrap(viewModel.state.polls) } } + + var loadingPublisher: AnyPublisher { + Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() + } + + var emptyPublisher: AnyPublisher { + Empty(completeImmediately: true, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() + } + + var mockPoll: TimelinePollDetails { + .init(id: "id", + question: "Do you like polls?", + answerOptions: [], + closed: false, + startDate: .init(), + totalAnswerCount: 3, + type: .undisclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + } } From c7b4692112017b5d2a1b7248aef24a6c00de97cc Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 10:09:28 +0100 Subject: [PATCH 227/468] Refine timestamp logics --- .../Service/MatrixSDK/PollHistoryService.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 395972a47..35208a954 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -38,8 +38,8 @@ final class PollHistoryService: PollHistoryServiceProtocol { private let pollErrorsSubject: PassthroughSubject = .init() // timestamps - private var targetTimestamp: Date? - private var oldestEventDateSubject: CurrentValueSubject = .init(Date.distantFuture) + private var targetTimestamp: Date = .init() + private var oldestEventDateSubject: CurrentValueSubject = .init(.init()) var updates: AnyPublisher { updatesSubject.eraseToAnyPublisher() @@ -83,7 +83,7 @@ final class PollHistoryService: PollHistoryServiceProtocol { class PollAggregationContext { var pollAggregator: PollAggregator? let isLivePoll: Bool - var published: Bool = false + var published: Bool init(pollAggregator: PollAggregator? = nil, isLivePoll: Bool, published: Bool = false) { self.pollAggregator = pollAggregator @@ -123,7 +123,7 @@ private extension PollHistoryService { } func startPagination() -> AnyPublisher { - let startingTimestamp = targetTimestamp ?? .init() + let startingTimestamp = oldestEventDate targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) let batchSubject = PassthroughSubject() @@ -181,10 +181,7 @@ private extension PollHistoryService { } var timestampTargetReached: Bool { - guard let targetTimestamp = targetTimestamp else { - return true - } - return oldestEventDate <= targetTimestamp + oldestEventDate <= targetTimestamp } var oldestEventDate: Date { @@ -212,7 +209,7 @@ private extension MXEvent { // MARK: - PollAggregatorDelegate extension PollHistoryService: PollAggregatorDelegate { - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {} + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { guard let context = pollAggregationContexts[aggregator.poll.id], !context.published else { From 96aca0b99d9979bb0893ea02dd50e4d36b8f8ec2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 10:09:47 +0100 Subject: [PATCH 228/468] Improve error handling --- .../Modules/Room/PollHistory/PollHistoryViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 1687cf2f4..c73e2ea32 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -58,7 +58,6 @@ private extension PollHistoryViewModel { self?.handleBatchEnded(completion: completion) } receiveValue: { [weak self] polls in self?.add(polls: polls) - self?.updateViewState() } .store(in: &subcriptions) } @@ -71,8 +70,11 @@ private extension PollHistoryViewModel { case .finished: break case .failure: + polls = polls ?? [] self.completion?(.genericError) } + + updateViewState() } func setupUpdateSubscriptions() { From cc728954d9a21d40634aeecfbc6fb387940412e8 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 10:14:26 +0100 Subject: [PATCH 229/468] Cleanup unused code --- .../Service/MatrixSDK/PollHistoryService.swift | 16 ++++++---------- .../Service/Mock/MockPollHistoryService.swift | 5 ----- .../Service/PollHistoryServiceProtocol.swift | 6 +----- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 35208a954..eb73ff732 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -35,7 +35,6 @@ final class PollHistoryService: PollHistoryServiceProtocol { // polls updates private let updatesSubject: PassthroughSubject = .init() - private let pollErrorsSubject: PassthroughSubject = .init() // timestamps private var targetTimestamp: Date = .init() @@ -45,10 +44,6 @@ final class PollHistoryService: PollHistoryServiceProtocol { updatesSubject.eraseToAnyPublisher() } - var pollErrors: AnyPublisher { - pollErrorsSubject.eraseToAnyPublisher() - } - init(room: MXRoom, chunkSizeInDays: UInt) { self.room = room self.chunkSizeInDays = chunkSizeInDays @@ -211,8 +206,10 @@ private extension MXEvent { extension PollHistoryService: PollAggregatorDelegate { func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], !context.published else { + guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else { return } @@ -227,11 +224,10 @@ extension PollHistoryService: PollAggregatorDelegate { } } - func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - pollErrorsSubject.send(didFailWithError) - } - func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { + guard let context = pollAggregationContexts[aggregator.poll.id], context.published else { + return + } updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 277c6a647..c98f4e136 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -33,11 +33,6 @@ final class MockPollHistoryService: PollHistoryServiceProtocol { updatesPublisher } - var pollErrorPublisher: AnyPublisher = Empty().eraseToAnyPublisher() - var pollErrors: AnyPublisher { - pollErrorPublisher - } - var hasNextBatch = true var fetchedUpToPublisher: AnyPublisher = Just(.init()).eraseToAnyPublisher() diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift index 6335d5d63..5132478cc 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift @@ -21,13 +21,9 @@ protocol PollHistoryServiceProtocol { /// Implementations should return the same publisher if `nextBatch()` is called again before the previous publisher completes. func nextBatch() -> AnyPublisher - /// Publishes updates for the polls previously pusblished by the `nextBatch()` publishers. + /// Publishes updates for the polls previously pusblished by the `nextBatch()` or `livePolls` publishers. var updates: AnyPublisher { get } - /// Publishes errors regarding poll aggregations. - /// Note: `nextBatch()` will continue to publish new polls even if some poll isn't being aggregated correctly. - var pollErrors: AnyPublisher { get } - /// Publishes live polls not related with the current batch. var livePolls: AnyPublisher { get } From c98510d7868f14b8131d2ca08edeca5927fbbd04 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 10:24:36 +0100 Subject: [PATCH 230/468] Improve tests --- .../MockPollHistoryScreenState.swift | 20 ++++++---- .../Test/Unit/PollHistoryViewModelTests.swift | 40 ++++++++++++++----- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift index e2e8278ea..a1c12e5f3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift @@ -52,17 +52,17 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { case .past: pollHistoryMode = .past case .contentLoading: - pollService.nextBatchPublishers.append(loadingPolls) + pollService.nextBatchPublishers.append(MockPollPublisher.loadingPolls) case .empty: pollHistoryMode = .active - pollService.nextBatchPublishers = [noPolls] + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls] case .emptyLoading: - pollService.nextBatchPublishers = [noPolls, loadingPolls] + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls, MockPollPublisher.loadingPolls] case .emptyNoMoreContent: pollService.hasNextBatch = false - pollService.nextBatchPublishers = [noPolls] + pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls] case .loading: - pollService.nextBatchPublishers = [loadingPolls] + pollService.nextBatchPublishers = [MockPollPublisher.loadingPolls] } let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService) @@ -83,12 +83,16 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable { } } -private extension MockPollHistoryScreenState { - var noPolls: AnyPublisher { +enum MockPollPublisher { + static var emptyPolls: AnyPublisher { Empty(completeImmediately: true).eraseToAnyPublisher() } - var loadingPolls: AnyPublisher { + static var loadingPolls: AnyPublisher { Empty(completeImmediately: false).eraseToAnyPublisher() } + + static var failure: AnyPublisher { + Fail(error: NSError(domain: "fake", code: 1)).eraseToAnyPublisher() + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift index 86487c1d1..95a6e7e72 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -42,7 +42,7 @@ final class PollHistoryViewModelTests: XCTestCase { func testLoadingStateIsTrueWhileLoading() { XCTAssertFalse(viewModel.state.isLoading) - pollHistoryService.nextBatchPublishers = [loadingPublisher, emptyPublisher] + pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls, MockPollPublisher.emptyPolls] viewModel.process(viewAction: .viewAppeared) XCTAssertTrue(viewModel.state.isLoading) viewModel.process(viewAction: .viewAppeared) @@ -83,12 +83,40 @@ final class PollHistoryViewModelTests: XCTestCase { } func testLivePollsAreHandled() throws { - pollHistoryService.nextBatchPublishers = [emptyPublisher] + pollHistoryService.nextBatchPublishers = [MockPollPublisher.emptyPolls] pollHistoryService.livePollsPublisher = Just(mockPoll).eraseToAnyPublisher() viewModel.process(viewAction: .viewAppeared) XCTAssertEqual(viewModel.state.polls?.count, 1) XCTAssertEqual(viewModel.state.polls?.first?.id, "id") } + + func testLivePollsDontChangeLoadingState() throws { + let livePolls = PassthroughSubject() + pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls] + pollHistoryService.livePollsPublisher = livePolls.eraseToAnyPublisher() + viewModel.process(viewAction: .viewAppeared) + XCTAssertTrue(viewModel.state.isLoading) + XCTAssertNil(viewModel.state.polls) + livePolls.send(mockPoll) + XCTAssertTrue(viewModel.state.isLoading) + XCTAssertNotNil(viewModel.state.polls) + XCTAssertEqual(viewModel.state.polls?.count, 1) + } + + func testAfterFailureCompletionIsCalled() throws { + let expectation = expectation(description: #function) + + pollHistoryService.nextBatchPublishers = [MockPollPublisher.failure] + viewModel.completion = { event in + XCTAssertEqual(event, .genericError) + expectation.fulfill() + } + viewModel.process(viewAction: .viewAppeared) + XCTAssertFalse(viewModel.state.isLoading) + XCTAssertNotNil(viewModel.state.polls) + + wait(for: [expectation], timeout: 1.0) + } } private extension PollHistoryViewModelTests { @@ -98,14 +126,6 @@ private extension PollHistoryViewModelTests { } } - var loadingPublisher: AnyPublisher { - Empty(completeImmediately: false, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() - } - - var emptyPublisher: AnyPublisher { - Empty(completeImmediately: true, outputType: TimelinePollDetails.self, failureType: Error.self).eraseToAnyPublisher() - } - var mockPoll: TimelinePollDetails { .init(id: "id", question: "Do you like polls?", From e6625cda4f4516df90ec403d30136e0788f740ec Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 10:38:41 +0100 Subject: [PATCH 231/468] Add changelog.d file --- changelog.d/pr-7303.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7303.change diff --git a/changelog.d/pr-7303.change b/changelog.d/pr-7303.change new file mode 100644 index 000000000..fc6bbdb3a --- /dev/null +++ b/changelog.d/pr-7303.change @@ -0,0 +1 @@ +Poll: add a feature to load more polls in the poll history. From 946ca1d1be4da76ad5ef9bee6d662f2ec071f7dd Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 26 Jan 2023 11:15:31 +0000 Subject: [PATCH 232/468] Ensure E2EE never tracks UnknownError --- Riot/Modules/Analytics/DecryptionFailure.swift | 4 ---- Riot/Modules/Analytics/DecryptionFailureTracker.m | 7 ++----- .../Analytics/Helpers/MXCallHangupReason+Analytics.swift | 6 ++++++ changelog.d/pr-7304.change | 1 + 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 changelog.d/pr-7304.change diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 2f33f6369..1c991db88 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -16,12 +16,10 @@ import AnalyticsEvents -/// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. @objc enum DecryptionFailureReason: Int { case unspecified case olmKeysNotSent case olmIndexError - case unexpected var errorName: AnalyticsEvent.Error.Name { switch self { @@ -31,8 +29,6 @@ import AnalyticsEvents return .OlmKeysNotSentError case .olmIndexError: return .OlmIndexError - case .unexpected: - return .UnknownError } } } diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m index d175f9fd1..4a749b71a 100644 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.m @@ -105,12 +105,9 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; reason = DecryptionFailureReasonOlmIndexError; break; - case MXDecryptingErrorEncryptionNotEnabledCode: - case MXDecryptingErrorUnableToDecryptCode: - reason = DecryptionFailureReasonUnexpected; - break; - default: + // All other error codes will be tracked as `OlmUnspecifiedError` and will include `context` containing + // the actual error code and localized description reason = DecryptionFailureReasonUnspecified; break; } diff --git a/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift index 4b8911ce8..c60f35446 100644 --- a/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift +++ b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift @@ -21,6 +21,9 @@ extension __MXCallHangupReason { switch self { case .userHangup: return .VoipUserHangup + case .userBusy: + // There is no dedicated analytics event for `userBusy` error + return .UnknownError case .inviteTimeout: return .VoipInviteTimeout case .iceFailed: @@ -32,6 +35,9 @@ extension __MXCallHangupReason { case .unknownError: return .UnknownError default: + MXLog.failure("Unknown or unhandled hangup reason", context: [ + "reason": rawValue + ]) return .UnknownError } } diff --git a/changelog.d/pr-7304.change b/changelog.d/pr-7304.change new file mode 100644 index 000000000..174f32497 --- /dev/null +++ b/changelog.d/pr-7304.change @@ -0,0 +1 @@ +Analytics: Ensure E2EE never tracks UnknownError From 45718510bb021d7861564994ed6bcdde2c2a4a1c Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 26 Jan 2023 11:59:19 +0000 Subject: [PATCH 233/468] Fix avatar loading in SwiftUI. --- .../Common/Avatar/View/AvatarImage.swift | 21 ++++++---- .../Common/Avatar/View/SpaceAvatarImage.swift | 40 +++++++++++-------- .../Avatar/ViewModel/AvatarViewModel.swift | 25 +++++++----- changelog.d/7305.bugfix | 1 + 4 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 changelog.d/7305.bugfix diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index b143d4d30..3ddd4d472 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -26,9 +26,11 @@ struct AvatarImage: View { var displayName: String? var size: AvatarSize + @State private var avatar: AvatarViewState = .empty + var body: some View { Group { - switch viewModel.viewState { + switch avatar { case .empty: ProgressView() case .placeholder(let firstCharacter, let colorIndex): @@ -42,13 +44,16 @@ struct AvatarImage: View { .frame(maxWidth: CGFloat(size.rawValue), maxHeight: CGFloat(size.rawValue)) .clipShape(Circle()) .onAppear { - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: displayName, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) + avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count) + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size ) { newState in + avatar = newState + } } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index 2662831e1..708c26a2b 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -26,9 +26,11 @@ struct SpaceAvatarImage: View { var displayName: String? var size: AvatarSize + @State private var avatar: AvatarViewState = .empty + var body: some View { Group { - switch viewModel.viewState { + switch avatar { case .empty: ProgressView() case .placeholder(let firstCharacter, let colorIndex): @@ -48,23 +50,27 @@ struct SpaceAvatarImage: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } } - .onChange(of: displayName, perform: { value in - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: value, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) - }) + .onChange(of: displayName) { value in + guard case .placeholder = avatar else { return } + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: value, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size) { newState in + avatar = newState + } + } .onAppear { - viewModel.loadAvatar( - mxContentUri: mxContentUri, - matrixItemId: matrixItemId, - displayName: displayName, - colorCount: theme.colors.namesAndAvatars.count, - avatarSize: size - ) + avatar = viewModel.placeholderAvatar(matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count) + viewModel.loadAvatar(mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: displayName, + colorCount: theme.colors.namesAndAvatars.count, + avatarSize: size) { newState in + avatar = newState + } } } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 10055738d..68037f552 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -22,14 +22,22 @@ import Foundation final class AvatarViewModel: ObservableObject { private let avatarService: AvatarServiceProtocol - @Published private(set) var viewState = AvatarViewState.empty - init(avatarService: AvatarServiceProtocol) { self.avatarService = avatarService } private var cancellables = Set() + func placeholderAvatar(matrixItemId: String, + displayName: String?, + colorCount: Int) -> AvatarViewState { + let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, + matrixItemId: matrixItemId, + colorCount: colorCount) + + return .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) + } + /// Load an avatar /// - Parameters: /// - mxContentUri: The matrix content URI of the avatar. @@ -41,14 +49,10 @@ final class AvatarViewModel: ObservableObject { matrixItemId: String, displayName: String?, colorCount: Int, - avatarSize: AvatarSize) { - let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, - matrixItemId: matrixItemId, - colorCount: colorCount) - - viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) - + avatarSize: AvatarSize, + avatarCompletion: @escaping (AvatarViewState) -> Void) { guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else { + avatarCompletion(placeholderAvatar(matrixItemId: matrixItemId, displayName: displayName, colorCount: colorCount)) return } @@ -56,8 +60,9 @@ final class AvatarViewModel: ObservableObject { .sink { completion in guard case let .failure(error) = completion else { return } UILog.error("[AvatarService] Failed to retrieve avatar", context: error) + // No need to call the completion, there's nothing we can do and the error is logged. } receiveValue: { image in - self.viewState = .avatar(image) + avatarCompletion(.avatar(image)) } .store(in: &cancellables) } diff --git a/changelog.d/7305.bugfix b/changelog.d/7305.bugfix new file mode 100644 index 000000000..5f940f946 --- /dev/null +++ b/changelog.d/7305.bugfix @@ -0,0 +1 @@ +Space Switcher: Fix a bug where the avatars would all be the same. From 7a6e6199e43c054d6b1bcdf68d115f8edf0b113c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 14:52:33 +0100 Subject: [PATCH 234/468] Fix alert presentation --- .../Coordinator/PollHistoryCoordinator.swift | 19 ++----------------- .../Room/PollHistory/PollHistoryModels.swift | 3 ++- .../PollHistory/PollHistoryViewModel.swift | 2 +- .../Test/Unit/PollHistoryViewModelTests.swift | 11 ++--------- .../Room/PollHistory/View/PollHistory.swift | 3 +++ 5 files changed, 10 insertions(+), 28 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index e0ee3f54c..13ed8e3d2 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -43,11 +43,8 @@ final class PollHistoryCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[PollHistoryCoordinator] did start.") - pollHistoryViewModel.completion = { [weak self] result in - switch result { - case .genericError: - self?.showErrorAlert() - } + pollHistoryViewModel.completion = { _ in + } } @@ -55,15 +52,3 @@ final class PollHistoryCoordinator: Coordinator, Presentable { pollHistoryHostingController } } - -private extension PollHistoryCoordinator { - func showErrorAlert() { - let alert = UIAlertController(title: VectorL10n.pollHistoryFetchingError, - message: nil, - preferredStyle: .alert) - - let cancelAction = UIAlertAction(title: VectorL10n.ok, style: .cancel) - alert.addAction(cancelAction) - pollHistoryHostingController.present(alert, animated: true, completion: nil) - } -} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index 0b6213787..f342d50af 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -22,7 +22,7 @@ enum PollHistoryConstants { } enum PollHistoryViewModelResult: Equatable { - case genericError + } // MARK: View @@ -34,6 +34,7 @@ enum PollHistoryMode: CaseIterable { struct PollHistoryViewBindings { var mode: PollHistoryMode + var alertInfo: AlertInfo? } struct PollHistoryViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index c73e2ea32..9dbffb7a8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -71,7 +71,7 @@ private extension PollHistoryViewModel { break case .failure: polls = polls ?? [] - self.completion?(.genericError) + state.bindings.alertInfo = .init(id: true, title: VectorL10n.pollHistoryFetchingError) } updateViewState() diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift index 95a6e7e72..efce641d4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/Unit/PollHistoryViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -104,18 +104,11 @@ final class PollHistoryViewModelTests: XCTestCase { } func testAfterFailureCompletionIsCalled() throws { - let expectation = expectation(description: #function) - pollHistoryService.nextBatchPublishers = [MockPollPublisher.failure] - viewModel.completion = { event in - XCTAssertEqual(event, .genericError) - expectation.fulfill() - } viewModel.process(viewAction: .viewAppeared) XCTAssertFalse(viewModel.state.isLoading) XCTAssertNotNil(viewModel.state.polls) - - wait(for: [expectation], timeout: 1.0) + XCTAssertNotNil(viewModel.state.bindings.alertInfo) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index 43e1b28e8..703390ac3 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -44,6 +44,9 @@ struct PollHistory: View { .onChange(of: viewModel.mode) { _ in viewModel.send(viewAction: .segmentDidChange) } + .alert(item: $viewModel.alertInfo) { + $0.alert + } } @ViewBuilder From 1598c56e9f6af61d60021539d84dd82c76655e1f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 26 Jan 2023 15:16:48 +0100 Subject: [PATCH 235/468] Use Calendar to compute target dates --- .../Modules/Room/PollHistory/PollHistoryModels.swift | 1 - .../Modules/Room/PollHistory/PollHistoryViewModel.swift | 8 +++++--- .../Service/MatrixSDK/PollHistoryService.swift | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift index f342d50af..fda7b0b3b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift @@ -18,7 +18,6 @@ enum PollHistoryConstants { static let chunkSizeInDays: UInt = 30 - static let oneDayInSeconds: TimeInterval = 8.6 * 10e3 } enum PollHistoryViewModelResult: Equatable { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift index 9dbffb7a8..4fe1f49f2 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift @@ -142,8 +142,10 @@ extension PollHistoryViewModel.Context { } } - var syncedPastDays: UInt { - let timeDelta = max(viewState.syncStartDate.timeIntervalSince(viewState.syncedUpTo), 0) - return UInt((timeDelta / PollHistoryConstants.oneDayInSeconds).rounded()) + var syncedPastDays: Int { + guard let days = Calendar.current.dateComponents([.day], from: viewState.syncedUpTo, to: viewState.syncStartDate).day else { + return 0 + } + return max(0, days) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index eb73ff732..7f6d8c5f6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -119,7 +119,7 @@ private extension PollHistoryService { func startPagination() -> AnyPublisher { let startingTimestamp = oldestEventDate - targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) + targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) ?? startingTimestamp let batchSubject = PassthroughSubject() currentBatchSubject = batchSubject @@ -190,8 +190,8 @@ private extension PollHistoryService { } private extension Date { - func subtractingDays(_ days: UInt) -> Date { - addingTimeInterval(-TimeInterval(days) * PollHistoryConstants.oneDayInSeconds) + func subtractingDays(_ days: UInt) -> Date? { + Calendar.current.date(byAdding: DateComponents(day: -Int(days)), to: self) } } From a6e32517b7312b70a3815a1223fb11aaa44a30f3 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 26 Jan 2023 16:24:32 +0000 Subject: [PATCH 236/468] version++ --- CHANGES.md | 12 ++++++++++++ Config/AppVersion.xcconfig | 4 ++-- changelog.d/7305.bugfix | 1 - changelog.d/pr-7300.bugfix | 1 - changelog.d/pr-7304.change | 1 - 5 files changed, 14 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/7305.bugfix delete mode 100644 changelog.d/pr-7300.bugfix delete mode 100644 changelog.d/pr-7304.change diff --git a/CHANGES.md b/CHANGES.md index e4d8ef4d8..3484fcf48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +## Changes in 1.9.17 (2023-01-26) + +🙌 Improvements + +- Analytics: Ensure E2EE never tracks UnknownError ([#7304](https://github.com/vector-im/element-ios/pull/7304)) + +🐛 Bugfixes + +- Fix a deadlock when updating the summary of a room that has a voice broadcast. ([#7300](https://github.com/vector-im/element-ios/pull/7300)) +- Space Switcher: Fix a bug where the avatars would all be the same. ([#7305](https://github.com/vector-im/element-ios/issues/7305)) + + ## Changes in 1.9.16 (2023-01-24) ✨ Features diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 7271fabb8..210603b23 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.16 -CURRENT_PROJECT_VERSION = 1.9.16 +MARKETING_VERSION = 1.9.17 +CURRENT_PROJECT_VERSION = 1.9.17 diff --git a/changelog.d/7305.bugfix b/changelog.d/7305.bugfix deleted file mode 100644 index 5f940f946..000000000 --- a/changelog.d/7305.bugfix +++ /dev/null @@ -1 +0,0 @@ -Space Switcher: Fix a bug where the avatars would all be the same. diff --git a/changelog.d/pr-7300.bugfix b/changelog.d/pr-7300.bugfix deleted file mode 100644 index bd546901f..000000000 --- a/changelog.d/pr-7300.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a deadlock when updating the summary of a room that has a voice broadcast. diff --git a/changelog.d/pr-7304.change b/changelog.d/pr-7304.change deleted file mode 100644 index 174f32497..000000000 --- a/changelog.d/pr-7304.change +++ /dev/null @@ -1 +0,0 @@ -Analytics: Ensure E2EE never tracks UnknownError From a768b0c42381b2a8d2a111a591cdc8a09697abc5 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 26 Jan 2023 17:17:12 +0000 Subject: [PATCH 237/468] finish version++ From 4e7ce652d4484f96a38fe4db1a2212830391307f Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 26 Jan 2023 17:17:21 +0000 Subject: [PATCH 238/468] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 210603b23..250f5e373 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.17 -CURRENT_PROJECT_VERSION = 1.9.17 +MARKETING_VERSION = 1.9.18 +CURRENT_PROJECT_VERSION = 1.9.18 From 746fd99ddb5f84877aed7418149d1b4aae1fd65c Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 26 Jan 2023 20:02:31 +0000 Subject: [PATCH 239/468] Generate crypto store key --- Riot/Assets/en.lproj/Vector.strings | 6 ++-- .../MatrixSDKCrypto+LocalizedError.swift | 31 +++++++++++++++++++ Riot/Generated/Strings.swift | 6 ++-- .../EncryptionKeyManager.swift | 12 +++++++ .../Modules/Settings/SettingsViewController.m | 2 +- changelog.d/pr-7310.change | 1 + 6 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 Riot/Categories/MatrixSDKCrypto+LocalizedError.swift create mode 100644 changelog.d/pr-7310.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 40cc62e9c..bff7b6ac3 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -804,9 +804,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "Enable new rust-based Crypto SDK"; -"settings_labs_confirm_crypto_sdk" = "This action cannot be undone"; -"settings_labs_disable_crypto_sdk" = "Crypto SDK is enabled. To disable please reinstall the app"; +"settings_labs_enable_crypto_sdk" = "End-to-end encryption 2.0"; +"settings_labs_confirm_crypto_sdk" = "This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed?"; +"settings_labs_disable_crypto_sdk" = "End-to-end encryption 2.0 (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift new file mode 100644 index 000000000..d802d54ff --- /dev/null +++ b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift @@ -0,0 +1,31 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG + +import MatrixSDKCrypto + +extension CryptoStoreError: LocalizedError { + public var errorDescription: String? { + // We dont really care about the type of error here when showing to the user. + // Details about the error are tracked independently + return VectorL10n.e2eNeedLogInAgain + } +} + +#endif diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ab9d80f24..0327090c1 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7571,7 +7571,7 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// This action cannot be undone + /// This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed? public static var settingsLabsConfirmCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") } @@ -7579,7 +7579,7 @@ public class VectorL10n: NSObject { public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// Crypto SDK is enabled. To disable please reinstall the app + /// End-to-end encryption 2.0 (log out to disable) public static var settingsLabsDisableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") } @@ -7595,7 +7595,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// Enable new rust-based Crypto SDK + /// End-to-end encryption 2.0 public static var settingsLabsEnableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") } diff --git a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift index 5085e9efb..484a63832 100644 --- a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift +++ b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift @@ -31,6 +31,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { private static let cryptoOlmPickleKey: KeyValueStoreKey = "cryptoOlmPickleKey" private static let roomLastMessageIv: KeyValueStoreKey = "roomLastMessageIv" private static let roomLastMessageAesKey: KeyValueStoreKey = "roomLastMessageAesKey" + private static let cryptoSDKStoreKey: KeyValueStoreKey = "cryptoSDKStoreKey" private let keychainStore: KeyValueStore = KeychainStore(withKeychain: Keychain(service: keychainService, accessGroup: BuildSettings.keychainAccessGroup)) @@ -47,6 +48,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { generateKeyIfNotExists(forKey: EncryptionKeyManager.cryptoOlmPickleKey, size: 32) generateIvIfNotExists(forKey: EncryptionKeyManager.roomLastMessageIv) generateAesKeyIfNotExists(forKey: EncryptionKeyManager.roomLastMessageAesKey) + generateKeyIfNotExists(forKey: EncryptionKeyManager.cryptoSDKStoreKey, size: 32) assert(keychainStore.containsObject(forKey: EncryptionKeyManager.contactsIv), "[EncryptionKeyManager] initKeys: Failed to generate IV for acount") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.contactsAesKey), "[EncryptionKeyManager] initKeys: Failed to generate AES Key for acount") @@ -55,6 +57,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { assert(keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoOlmPickleKey), "[EncryptionKeyManager] initKeys: Failed to generate Key for olm pickle key") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageIv), "[EncryptionKeyManager] initKeys: Failed to generate IV for room last message") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageAesKey), "[EncryptionKeyManager] initKeys: Failed to generate AES Key for room last message encryption") + assert(keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoSDKStoreKey), "[EncryptionKeyManager] initKeys: Failed to generate Key for crypto sdk store") } // MARK: - MXKeyProviderDelegate @@ -64,6 +67,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { || dataType == MXKAccountManagerDataType || dataType == MXCryptoOlmPickleKeyDataType || dataType == MXRoomLastMessageDataType + || dataType == MXCryptoSDKStoreKeyDataType } func hasKeyForData(ofType dataType: String) -> Bool { @@ -77,7 +81,10 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { case MXRoomLastMessageDataType: return keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageIv) && keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageAesKey) + case MXCryptoSDKStoreKeyDataType: + return keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoSDKStoreKey) default: + MXLog.warning("[EncryptionKeyManager] hasKeyForData: No key for \(dataType)") return false } } @@ -103,7 +110,12 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { let aesKey = try? keychainStore.data(forKey: EncryptionKeyManager.roomLastMessageAesKey) { return MXAesKeyData(iv: ivKey, key: aesKey) } + case MXCryptoSDKStoreKeyDataType: + if let key = try? keychainStore.data(forKey: EncryptionKeyManager.cryptoSDKStoreKey) { + return MXRawDataKey(key: key) + } default: + MXLog.failure("[EncryptionKeyManager] keyDataForData: Attempting to get data for unknown type", dataType) return nil } return nil diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 90533cfd1..fc10edf0e 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3386,7 +3386,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> MXWeakify(self); [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:nil + UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk message:VectorL10n.settingsLabsConfirmCryptoSdk preferredStyle:UIAlertControllerStyleAlert]; diff --git a/changelog.d/pr-7310.change b/changelog.d/pr-7310.change new file mode 100644 index 000000000..4ba5e9ee1 --- /dev/null +++ b/changelog.d/pr-7310.change @@ -0,0 +1 @@ +CryptoV2: Generate Crypto SDK store key From 21eafa23837ae40ff7bf152ccc5279ea8c0ce042 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Fri, 27 Jan 2023 15:07:32 +0100 Subject: [PATCH 240/468] added view in timeline action, added tests --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 6 ++- .../RoomInfoCoordinatorBridgePresenter.swift | 5 ++ .../RoomInfo/RoomInfoCoordinatorType.swift | 2 + Riot/Modules/Room/RoomViewController.m | 23 +++++++++ .../ExploreRoomCoordinator.swift | 4 ++ .../Modules/Common/Mock/MockAppScreens.swift | 3 +- .../Coordinator/PollHistoryCoordinator.swift | 51 ++++++++++--------- .../PollHistoryDetailCoordinator.swift | 7 +-- .../MockPollHistoryDetailScreenState.swift | 6 +-- .../PollHistoryDetailModels.swift | 7 ++- .../PollHistoryDetailViewModel.swift | 4 +- .../Test/UI/PollHistoryDetailUITests.swift | 24 ++++----- .../PollHistoryDetailViewModelTests.swift | 32 +++++++----- .../View/PollHistoryDetail.swift | 48 ++++++++++------- .../Room/PollHistory/View/PollListItem.swift | 6 +-- 15 files changed, 141 insertions(+), 87 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index bb15c964e..26246eeae 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -176,9 +176,13 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { coordinator.start() push(coordinator: coordinator) case .pollHistory: - let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, session: session, room: room, navigationRouter: navigationRouter)) + let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: room, navigationRouter: navigationRouter)) coordinator.start() push(coordinator: coordinator) + coordinator.completion = { [weak self] event in + guard let self else { return } + self.delegate?.roomInfoCoordinator(self, viewEventInTimeline: event) + } default: guard let tabIndex = target.tabIndex else { fatalError("No settings tab index for this target.") diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index b8db2a66a..39e740bfc 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -17,12 +17,14 @@ */ import Foundation +import MatrixSDK @objc protocol RoomInfoCoordinatorBridgePresenterDelegate { func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember) func roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String) + func roomInfoCoordinatorBridgePresenter(_ coordinator: RoomInfoCoordinatorBridgePresenter, viewEventInTimeline event: MXEvent) } /// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller. @@ -129,6 +131,9 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate { func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) { self.delegate?.roomInfoCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId) } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) { + self.delegate?.roomInfoCoordinatorBridgePresenter(self, viewEventInTimeline: event) + } } // MARK: - UIAdaptivePresentationControllerDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift index 80f696b0b..2122ddf1d 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift @@ -17,12 +17,14 @@ */ import Foundation +import MatrixSDK protocol RoomInfoCoordinatorDelegate: AnyObject { func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType) func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType) func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) } /// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3792fee64..b6d14bf08 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7871,6 +7871,29 @@ static CGSize kThreadListBarButtonItemImageSize; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } } +- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator + viewEventInTimeline:(MXEvent *)event +{ + [self.navigationController popToViewController:self animated:true]; + // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. + MXWeakify(self); + [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:event.eventId + threadId:event.threadId + andMatrixSession:self.mainSession + onComplete:^(id roomDataSource) { + MXStrongifyAndReturnIfNil(self); + + [roomDataSource finalizeInitialization]; + + // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view. + self.centerBubblesTableViewContentOnTheInitialEventBottom = YES; + [self displayRoom:roomDataSource]; + + // Give the data source ownership to the room view controller. + self.hasRoomDataSourceOwnership = YES; + }]; +} #pragma mark - RemoveJitsiWidgetViewDelegate diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index edf5218fb..c6c8a0601 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -17,6 +17,7 @@ */ import UIKit +import MatrixSDK @objcMembers final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { @@ -519,5 +520,8 @@ extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate { self.remove(childCoordinator: coordinator) } } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) { + self.navigationRouter.popToModule(self.toPresentable(), animated: true) + } } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index ba1d91e52..91bf25a51 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -73,6 +73,7 @@ enum MockAppScreens { MockComposerCreateActionListScreenState.self, MockComposerLinkActionScreenState.self, MockVoiceBroadcastPlaybackScreenState.self, - MockPollHistoryScreenState.self + MockPollHistoryScreenState.self, + MockPollHistoryDetailScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 42ad84f54..d68669bb7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -20,7 +20,6 @@ import SwiftUI struct PollHistoryCoordinatorParameters { let mode: PollHistoryMode - let session: MXSession let room: MXRoom let navigationRouter: NavigationRouterType } @@ -33,7 +32,7 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? + var completion: ((MXEvent) -> Void)? init(parameters: PollHistoryCoordinatorParameters) { self.parameters = parameters @@ -58,33 +57,35 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { func showPollDetail(_ poll: TimelinePollDetails) { - parameters.session.event(withEventId: poll.id, inRoom: parameters.room.roomId) { [weak self] response in - guard let self else { return } - if let event = response.value, - let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(pollHistoryDetails: MockPollHistoryDetailScreenState.openUndisclosed.poll, event: event, session: self.parameters.session, room: self.parameters.room)) { - detailCoordinator.toPresentable().presentationController?.delegate = self - detailCoordinator.completion = { [weak self, weak detailCoordinator] result in - guard let self = self, let coordinator = detailCoordinator else { return } - switch result { - case .dismiss: - self.toPresentable().dismiss(animated: true) - self.remove(childCoordinator: coordinator) - case .viewInTimeline: - self.toPresentable().dismiss(animated: true) - self.remove(childCoordinator: coordinator) - // TODO: go back in timeline + if let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: self.parameters.room)) { + detailCoordinator.toPresentable().presentationController?.delegate = self + detailCoordinator.completion = { [weak self, weak detailCoordinator] result in + guard let self = self, let coordinator = detailCoordinator else { return } + switch result { + case .dismiss: + self.toPresentable().dismiss(animated: true) + self.remove(childCoordinator: coordinator) + case .viewInTimeline: + self.toPresentable().dismiss(animated: false) + self.remove(childCoordinator: coordinator) + var event = event + if poll.closed { + let room = self.parameters.room + let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) + let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) + event = pollEndedEvent ?? event } + self.completion?(event) } - - self.add(childCoordinator: detailCoordinator) - detailCoordinator.start() - self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) - } else { - // TODO: manage error } + + self.add(childCoordinator: detailCoordinator) + detailCoordinator.start() + self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) + } else { + // TODO: #1040 manage error } - - } func toPresentable() -> UIViewController { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index 5f98622a9..113948a85 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -20,9 +20,7 @@ import Combine import MatrixSDK struct PollHistoryDetailCoordinatorParameters { - let pollHistoryDetails: TimelinePollDetails let event: MXEvent - let session: MXSession let room: MXRoom } @@ -30,7 +28,6 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { private let parameters: PollHistoryDetailCoordinatorParameters private let pollHistoryDetailHostingController: UIViewController private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol - private var cancellables = Set() private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? @@ -40,9 +37,9 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { init(parameters: PollHistoryDetailCoordinatorParameters) throws { self.parameters = parameters - let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.session, room: parameters.room, pollEvent: parameters.event)) + let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.room.mxSession, room: parameters.room, pollEvent: parameters.event)) - let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails, timelineViewModel: timelinePollCoordinator.viewModel) + let viewModel = PollHistoryDetailViewModel(timelineViewModel: timelinePollCoordinator.viewModel) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index c4e46f0e1..3e9709994 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -25,7 +25,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { case closedPollEnded var screenType: Any.Type { - TimelinePollDetails.self + PollHistoryDetail.self } var poll: TimelinePollDetails { @@ -37,7 +37,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { question: "Question", answerOptions: answerOptions, closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, - startDate: .init(), + startDate: .init(timeIntervalSinceReferenceDate: 0), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, eventType: self == .closedPollEnded ? .ended : .started, @@ -49,7 +49,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { - let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll, timelineViewModel: TimelinePollViewModel(timelinePollDetails: poll)) + let viewModel = PollHistoryDetailViewModel(timelineViewModel: TimelinePollViewModel(timelinePollDetails: poll)) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index afee15af1..44ec2204c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -32,8 +32,13 @@ enum PollHistoryDetailViewModelResult { // MARK: View struct PollHistoryDetailViewState: BindableState { - var poll: TimelinePollDetails var timelineViewModel: TimelinePollViewModelProtocol + var pollStartDate: Date { + timelineViewModel.context.viewState.poll.startDate + } + var isPollClosed: Bool { + timelineViewModel.context.viewState.poll.closed + } } enum PollHistoryDetailViewAction { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index dfc5f14c7..25068201a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -29,8 +29,8 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet // MARK: - Setup - init(pollHistoryDetails: TimelinePollDetails, timelineViewModel: TimelinePollViewModelProtocol) { - super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails, timelineViewModel: timelineViewModel)) + init(timelineViewModel: TimelinePollViewModelProtocol) { + super.init(initialViewState: PollHistoryDetailViewState(timelineViewModel: timelineViewModel)) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift index 27d68342b..6f3468161 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift @@ -18,21 +18,17 @@ import RiotSwiftUI import XCTest class PollHistoryDetailUITests: MockScreenTestCase { - func testPollHistoryDetailPromptRegular() { - let promptType = PollHistoryDetailPromptType.regular - app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title) - - let title = app.staticTexts["title"] - XCTAssert(title.exists) - XCTAssertEqual(title.label, promptType.title) + func testPollHistoryDetailOpenPoll() { + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.openDisclosed.title) + XCTAssert(app.staticTexts["Active polls"].exists) + XCTAssert(app.staticTexts["1/1/01"].exists) + XCTAssert(app.buttons["View poll in timeline"].exists) } - func testPollHistoryDetailPromptUpgrade() { - let promptType = PollHistoryDetailPromptType.upgrade - app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title) - - let title = app.staticTexts["title"] - XCTAssert(title.exists) - XCTAssertEqual(title.label, promptType.title) + func testPollHistoryDetailClosedPoll() { + app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.closedDisclosed.title) + XCTAssert(app.staticTexts["Past polls"].exists) + XCTAssert(app.staticTexts["1/1/01"].exists) + XCTAssert(app.buttons["View poll in timeline"].exists) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift index 0de398a49..d278f5136 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -27,22 +27,28 @@ class PollHistoryDetailViewModelTests: XCTestCase { var context: PollHistoryDetailViewModelType.Context! override func setUpWithError() throws { - viewModel = PollHistoryDetailViewModel(promptType: .regular, initialCount: Constants.counterInitialValue) + + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] + + let timelinePoll = TimelinePollDetails(id: "poll-id", + question: "Question", + answerOptions: answerOptions, + closed: false, + startDate: .init(), + totalAnswerCount: 3, + type: .disclosed, + eventType: .started, + maxAllowedSelections: 1, + hasBeenEdited: false, + hasDecryptionError: false) + + viewModel = PollHistoryDetailViewModel(timelineViewModel: TimelinePollViewModel(timelinePollDetails: timelinePoll)) context = viewModel.context } func testInitialState() { - XCTAssertEqual(context.viewState.count, Constants.counterInitialValue) - } - - func testCounter() throws { - context.send(viewAction: .incrementCount) - XCTAssertEqual(context.viewState.count, 1) - - context.send(viewAction: .incrementCount) - XCTAssertEqual(context.viewState.count, 2) - - context.send(viewAction: .decrementCount) - XCTAssertEqual(context.viewState.count, 1) + XCTAssertFalse(context.viewState.isPollClosed) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index 982b4dcd7..c775ce86c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -47,41 +47,51 @@ struct PollHistoryDetail: View { } private var content: some View { let timelineViewModel = viewModel.viewState.timelineViewModel - return VStack { - TimelinePollView(viewModel: timelineViewModel.context) - .navigationTitle(navigationTitle) - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .navigationBarItems(leading: btnBack) - viewInTimeline + return ScrollView { + VStack(alignment: .leading) { + Text(DateFormatter.pollShortDateFormatter.string(from: viewModel.viewState.pollStartDate)) + .foregroundColor(theme.colors.tertiaryContent) + .font(theme.fonts.caption1) + .padding([.top]) + TimelinePollView(viewModel: timelineViewModel.context) + .navigationTitle(navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: btnBack, trailing: btnDone) + viewInTimeline + } } } - private var btnBack : some View { + private var btnBack: some View { Button(action: { viewModel.send(viewAction: .dismiss) }) { - Image(systemName: "xmark") //"chevron.left" + Image(systemName: "chevron.left") .aspectRatio(contentMode: .fit) .foregroundColor(theme.colors.accent) } } + private var btnDone: some View { + Button { + viewModel.send(viewAction: .dismiss) + } label: { + Text(VectorL10n.done) + } + .accentColor(theme.colors.accent) + } private var viewInTimeline: some View { - HStack { - Button { - viewModel.send(viewAction: .viewInTimeline) - } label: { - Text(VectorL10n.pollHistoryDetailViewInTimeline) - } - .accentColor(theme.colors.accent) - Spacer() + Button { + viewModel.send(viewAction: .viewInTimeline) + } label: { + Text(VectorL10n.pollHistoryDetailViewInTimeline) } + .accentColor(theme.colors.accent) } private var navigationTitle: String { - let poll = viewModel.viewState.poll - if poll.closed { + if viewModel.viewState.isPollClosed { return VectorL10n.pollHistoryPastSegmentTitle } else { return VectorL10n.pollHistoryActiveSegmentTitle diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index df7cb3774..6ee1b0ddf 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -28,7 +28,7 @@ struct PollListItem: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text(DateFormatter.shortDateFormatter.string(from: pollData.startDate)) + Text(DateFormatter.pollShortDateFormatter.string(from: pollData.startDate)) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) @@ -68,8 +68,8 @@ struct PollListItem: View { } } -private extension DateFormatter { - static let shortDateFormatter: DateFormatter = { +extension DateFormatter { + static let pollShortDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.timeStyle = .none formatter.dateStyle = .short From f66170b9cd7c694bcfef79731d52415115a4bb5e Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Fri, 27 Jan 2023 15:23:44 +0100 Subject: [PATCH 241/468] generated String --- Riot/Generated/Strings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 494fa3b89..29d4108d0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4821,7 +4821,7 @@ public class VectorL10n: NSObject { } /// View poll in timeline public static var pollHistoryDetailViewInTimeline: String { - return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline") + return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline") } /// Error fetching polls. public static var pollHistoryFetchingError: String { From 645edd91c5d970d41f19c7f7d9367623c4aea394 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Fri, 27 Jan 2023 15:28:02 +0100 Subject: [PATCH 242/468] function renamed --- Riot/Modules/Common/Recents/RecentsViewController.m | 2 +- .../ContextMenu/Services/RoomContextActionService.swift | 4 ++-- .../Services/RoomContextActionServiceProtocol.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 04ff954d0..7beb563a7 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -2471,7 +2471,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro editedRoomId = nil; } --(void)roomContextActionServiceDidMarkedRoom:(id)service +-(void)roomContextActionServiceDidMarkRoom:(id)service { [self refreshRecentsTable]; } diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift index a9d07c27c..b19698da6 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift @@ -108,11 +108,11 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { func markAsRead() { room.markAllAsRead() - self.delegate?.roomContextActionServiceDidMarkedRoom(self) + self.delegate?.roomContextActionServiceDidMarkRoom(self) } func markAsUnread() { room.markAsUnread() - self.delegate?.roomContextActionServiceDidMarkedRoom(self) + self.delegate?.roomContextActionServiceDidMarkRoom(self) } // MARK: - Private diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift index 4ebd13d66..25c66773f 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift @@ -22,7 +22,7 @@ import Foundation func roomContextActionService(_ service: RoomContextActionServiceProtocol, showRoomNotificationSettingsForRoomWithId roomId: String) func roomContextActionServiceDidJoinRoom(_ service: RoomContextActionServiceProtocol) func roomContextActionServiceDidLeaveRoom(_ service: RoomContextActionServiceProtocol) - func roomContextActionServiceDidMarkedRoom(_ service: RoomContextActionServiceProtocol) + func roomContextActionServiceDidMarkRoom(_ service: RoomContextActionServiceProtocol) } /// `RoomContextActionServiceProtocol` classes are meant to be called by a `RoomActionProviderProtocol` instance so it provides the implementation of the menu actions. From bb384f837e545ce3f94665126be5e93271db929b Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Fri, 27 Jan 2023 17:10:16 +0100 Subject: [PATCH 243/468] added alert to show possible error, improved tests --- Config/BuildSettings.swift | 1 - .../RoomInfoListViewController.swift | 2 +- .../Coordinator/PollHistoryCoordinator.swift | 56 +++++++++---------- .../Test/UI/PollHistoryDetailUITests.swift | 14 +++-- .../PollHistoryDetailViewModelTests.swift | 1 + .../View/PollHistoryDetail.swift | 2 + .../Room/PollHistory/View/PollHistory.swift | 9 +-- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index d4b57871a..2f85f3c13 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -399,7 +399,6 @@ final class BuildSettings: NSObject { // MARK: - Polls static let pollsEnabled = true - static var pollsHistoryEnabled: Bool = false // MARK: - Location Sharing diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index fdb34304d..a879eeed0 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -199,7 +199,7 @@ final class RoomInfoListViewController: UIViewController { } rows.append(rowMembers) - if BuildSettings.pollsHistoryEnabled { + if BuildSettings.pollsEnabled { rows.append(rowPollHistory) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index d68669bb7..d970a444b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -57,35 +57,35 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { func showPollDetail(_ poll: TimelinePollDetails) { - if let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), - let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: self.parameters.room)) { - detailCoordinator.toPresentable().presentationController?.delegate = self - detailCoordinator.completion = { [weak self, weak detailCoordinator] result in - guard let self = self, let coordinator = detailCoordinator else { return } - switch result { - case .dismiss: - self.toPresentable().dismiss(animated: true) - self.remove(childCoordinator: coordinator) - case .viewInTimeline: - self.toPresentable().dismiss(animated: false) - self.remove(childCoordinator: coordinator) - var event = event - if poll.closed { - let room = self.parameters.room - let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) - let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) - event = pollEndedEvent ?? event - } - self.completion?(event) - } - } - - self.add(childCoordinator: detailCoordinator) - detailCoordinator.start() - self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) - } else { - // TODO: #1040 manage error + guard let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: self.parameters.room)) else { + pollHistoryViewModel.context.alertInfo = .init(id: true, title: VectorL10n.settingsDiscoveryErrorMessage) + return } + detailCoordinator.toPresentable().presentationController?.delegate = self + detailCoordinator.completion = { [weak self, weak detailCoordinator] result in + guard let self = self, let coordinator = detailCoordinator else { return } + switch result { + case .dismiss: + self.toPresentable().dismiss(animated: true) + self.remove(childCoordinator: coordinator) + case .viewInTimeline: + self.toPresentable().dismiss(animated: false) + self.remove(childCoordinator: coordinator) + var event = event + if poll.closed { + let room = self.parameters.room + let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) + let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) + event = pollEndedEvent ?? event + } + self.completion?(event) + } + } + + self.add(childCoordinator: detailCoordinator) + detailCoordinator.start() + self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) } func toPresentable() -> UIViewController { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift index 6f3468161..952a271fb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/UI/PollHistoryDetailUITests.swift @@ -20,15 +20,17 @@ import XCTest class PollHistoryDetailUITests: MockScreenTestCase { func testPollHistoryDetailOpenPoll() { app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.openDisclosed.title) - XCTAssert(app.staticTexts["Active polls"].exists) - XCTAssert(app.staticTexts["1/1/01"].exists) - XCTAssert(app.buttons["View poll in timeline"].exists) + let title = app.navigationBars.staticTexts.firstMatch.label + XCTAssertEqual(title, VectorL10n.pollHistoryActiveSegmentTitle) + XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01") + XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline) } func testPollHistoryDetailClosedPoll() { app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.closedDisclosed.title) - XCTAssert(app.staticTexts["Past polls"].exists) - XCTAssert(app.staticTexts["1/1/01"].exists) - XCTAssert(app.buttons["View poll in timeline"].exists) + let title = app.navigationBars.staticTexts.firstMatch.label + XCTAssertEqual(title, VectorL10n.pollHistoryPastSegmentTitle) + XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01") + XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift index d278f5136..4a9821a86 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -51,4 +51,5 @@ class PollHistoryDetailViewModelTests: XCTestCase { func testInitialState() { XCTAssertFalse(context.viewState.isPollClosed) } + } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index c775ce86c..1df5270a9 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -53,6 +53,7 @@ struct PollHistoryDetail: View { .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) .padding([.top]) + .accessibilityIdentifier("PollHistoryDetail.date") TimelinePollView(viewModel: timelineViewModel.context) .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) @@ -88,6 +89,7 @@ struct PollHistoryDetail: View { Text(VectorL10n.pollHistoryDetailViewInTimeline) } .accentColor(theme.colors.accent) + .accessibilityIdentifier("PollHistoryDetail.viewInTimeLineButton") } private var navigationTitle: String { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift index da9ba02da..612f85089 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift @@ -64,10 +64,11 @@ struct PollHistory: View { ScrollView { LazyVStack(spacing: 32) { ForEach(viewModel.viewState.polls ?? []) { pollData in - PollListItem(pollData: pollData) - .onTapGesture { - viewModel.send(viewAction: .showPollDetail(poll: pollData)) - } + Button(action: { + viewModel.send(viewAction: .showPollDetail(poll: pollData)) + }) { + PollListItem(pollData: pollData) + } } .frame(maxWidth: .infinity, alignment: .leading) From 356029f67412cdc4d46fa67736cd1951fd65b2aa Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Mon, 30 Jan 2023 10:31:00 +0100 Subject: [PATCH 244/468] added changelog --- changelog.d/pr-7314.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7314.change diff --git a/changelog.d/pr-7314.change b/changelog.d/pr-7314.change new file mode 100644 index 000000000..fd3dc46e9 --- /dev/null +++ b/changelog.d/pr-7314.change @@ -0,0 +1 @@ +Poll: added poll detail in poll list hisotry with navigation to timeline From a903196e48a6fefac4b2397f40a12645f1378d99 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Mon, 30 Jan 2023 15:38:56 +0100 Subject: [PATCH 245/468] Labs: Rich text editor: enable list items indentation --- Podfile | 2 - .../xcshareddata/swiftpm/Package.resolved | 22 ++++- Riot/Assets/en.lproj/Vector.strings | 4 +- Riot/Generated/Strings.swift | 8 ++ .../Utils/EventFormatter/HTMLFormatter.swift | 4 +- .../WysiwygInputToolbarView.swift | 23 ++++-- Riot/Utils/EventFormatter+DTCoreTextFix.h | 30 ------- Riot/Utils/EventFormatter+DTCoreTextFix.m | 80 ------------------- Riot/Utils/EventFormatter.m | 6 -- Riot/target.yml | 1 + RiotNSE/target.yml | 1 + RiotShareExtension/target.yml | 1 + .../Room/Composer/Model/ComposerModels.swift | 22 +++++ SiriIntents/target.yml | 1 + changelog.d/7316.change | 1 + project.yml | 5 +- 16 files changed, 79 insertions(+), 132 deletions(-) delete mode 100644 Riot/Utils/EventFormatter+DTCoreTextFix.h delete mode 100644 Riot/Utils/EventFormatter+DTCoreTextFix.m create mode 100644 changelog.d/7316.change diff --git a/Podfile b/Podfile index 35ba935b2..420a69dd5 100644 --- a/Podfile +++ b/Podfile @@ -53,8 +53,6 @@ end def import_MatrixKit_pods pod 'libPhoneNumber-iOS', '~> 0.9.13' - pod 'DTCoreText', '~> 1.6.25' - #pod 'DTCoreText/Extension', '~> 1.6.25' pod 'Down', '~> 0.11.0' end diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 34484b0a7..1e8132370 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,24 @@ "version" : "4.7.0" } }, + { + "identity" : "dtcoretext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTCoreText", + "state" : { + "revision" : "9d2d4d2296e5d2d852a7d3c592b817d913a5d020", + "version" : "1.6.27" + } + }, + { + "identity" : "dtfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTFoundation.git", + "state" : { + "revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3", + "version" : "1.7.18" + } + }, { "identity" : "maplibre-gl-native-distribution", "kind" : "remoteSourceControl", @@ -23,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "6927cb878376136c4a03d919b689af8dfbdad080", - "version" : "0.19.0" + "revision" : "3f72aeab7d7e04b52ff3f735ab79a75993f97ef2", + "version" : "0.22.0" } }, { diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bff7b6ac3..03ab2ca29 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2579,8 +2579,8 @@ To enable access, tap Settings> Location and select Always"; "wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list"; "wysiwyg_composer_format_action_code_block" = "Toggle code block"; "wysiwyg_composer_format_action_quote" = "Toggle quote"; - - +"wysiwyg_composer_format_action_indent" = "Increase indentation"; +"wysiwyg_composer_format_action_un_indent" = "Decrease indentation"; // Links "wysiwyg_composer_link_action_text" = "Text"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 0327090c1..550732246 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9403,6 +9403,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionCodeBlock: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_code_block") } + /// Increase indentation + public static var wysiwygComposerFormatActionIndent: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_indent") + } /// Apply inline code format public static var wysiwygComposerFormatActionInlineCode: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code") @@ -9427,6 +9431,10 @@ public class VectorL10n: NSObject { public static var wysiwygComposerFormatActionStrikethrough: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough") } + /// Decrease indentation + public static var wysiwygComposerFormatActionUnIndent: String { + return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_un_indent") + } /// Apply strikethrough format public static var wysiwygComposerFormatActionUnderline: String { return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline") diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift index 6c7e43a90..819eb632f 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift @@ -47,9 +47,7 @@ class HTMLFormatter: NSObject { var options: [AnyHashable: Any] = [ DTUseiOS6Attributes: true, - DTDefaultFontFamily: font.familyName, - DTDefaultFontName: font.fontName, - DTDefaultFontSize: font.pointSize, + DTDefaultFontDescriptor: font.fontDescriptor, DTDefaultLinkDecoration: false, DTDefaultLinkColor: ThemeService.shared().theme.colors.links, DTWillFlushBlockCallBack: sanitizeCallback diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 013602843..d611fb06a 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -17,6 +17,7 @@ import Foundation import Reusable import WysiwygComposer +import HTMLParser import SwiftUI import Combine import UIKit @@ -43,9 +44,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel( - textColor: ThemeService.shared().theme.colors.primaryContent, - linkColor: ThemeService.shared().theme.colors.links, - codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor + parserStyle: HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent, + linkColor: ThemeService.shared().theme.colors.links, + codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + codeBorderColor: ThemeService.shared().theme.textQuinaryColor, + quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + quoteBorderColor: ThemeService.shared().theme.textQuinaryColor, + borderWidth: 1.0, + cornerRadius: 4.0) ) private var viewModel: ComposerViewModelProtocol! @@ -298,9 +304,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background - wysiwygViewModel.textColor = theme.colors.primaryContent - wysiwygViewModel.linkColor = theme.colors.links - wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor + wysiwygViewModel.parserStyle = HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent, + linkColor: ThemeService.shared().theme.colors.links, + codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + codeBorderColor: ThemeService.shared().theme.textQuinaryColor, + quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + quoteBorderColor: ThemeService.shared().theme.textQuinaryColor, + borderWidth: 1.0, + cornerRadius: 4.0) } private func updateTextViewHeight() { diff --git a/Riot/Utils/EventFormatter+DTCoreTextFix.h b/Riot/Utils/EventFormatter+DTCoreTextFix.h deleted file mode 100644 index 90be432c4..000000000 --- a/Riot/Utils/EventFormatter+DTCoreTextFix.h +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -@import Foundation; - -#import "EventFormatter.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface EventFormatter(DTCoreTextFix) - -// Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168) -+ (void)fixDTCoreTextFont; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Riot/Utils/EventFormatter+DTCoreTextFix.m b/Riot/Utils/EventFormatter+DTCoreTextFix.m deleted file mode 100644 index 110d036fc..000000000 --- a/Riot/Utils/EventFormatter+DTCoreTextFix.m +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#import "EventFormatter+DTCoreTextFix.h" - -@import UIKit; -@import CoreText; -@import ObjectiveC; - -#pragma mark - UIFont DTCoreText fix - -@interface UIFont (vc_DTCoreTextFix) - -+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont; - -@end - -@implementation UIFont (vc_DTCoreTextFix) - -+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont { - NSString *fontName = (__bridge_transfer NSString *)CTFontCopyName(ctFont, kCTFontPostScriptNameKey); - - CGFloat fontSize = CTFontGetSize(ctFont); - UIFont *font = [UIFont fontWithName:fontName size:fontSize]; - - // On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI" - // In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits. - if ([font.familyName.lowercaseString containsString:@"times"]) - { - UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont); - - UIFontDescriptor *systemFontDescriptor = [UIFont systemFontOfSize:fontSize].fontDescriptor; - - UIFontDescriptor *finalFontDescriptor = [systemFontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; - font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize]; - } - - return font; -} - -@end - -#pragma mark - Implementation - -@implementation EventFormatter(DTCoreTextFix) - -// DTCoreText iOS 13 fix. See issue and comment here: https://github.com/Cocoanetics/DTCoreText/issues/1168#issuecomment-583541514 -+ (void)fixDTCoreTextFont -{ - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - Class originalClass = object_getClass([UIFont class]); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wundeclared-selector" - SEL originalSelector = @selector(fontWithCTFont:); // DTCoreText method we're overriding - SEL ourSelector = @selector(vc_fixedFontWithCTFont:); // Use custom implementation -#pragma clang diagnostic pop - - Method originalMethod = class_getClassMethod(originalClass, originalSelector); - Method swizzledMethod = class_getClassMethod(originalClass, ourSelector); - - method_exchangeImplementations(originalMethod, swizzledMethod); - }); -} - -@end diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index db526607a..5ea1d5f2d 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -25,7 +25,6 @@ #import "MXDecryptionResult.h" #import "DecryptionFailureTracker.h" -#import "EventFormatter+DTCoreTextFix.h" #import #pragma mark - Constants definitions @@ -50,11 +49,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; @implementation EventFormatter -+ (void)load -{ - [self fixDTCoreTextFont]; -} - - (void)initDateTimeFormatters { [super initDateTimeFormatters]; diff --git a/Riot/target.yml b/Riot/target.yml index 5813e2617..b0fc131fb 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -44,6 +44,7 @@ targets: - package: SwiftOGG - package: WysiwygComposer - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index ae27022c3..21c5f2864 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -33,6 +33,7 @@ targets: dependencies: - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 2d398950a..8601ce85f 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -33,6 +33,7 @@ targets: dependencies: - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 5525e9940..800494c5d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -36,6 +36,8 @@ enum FormatType { case strikethrough case unorderedList case orderedList + case indent + case unIndent case inlineCode case codeBlock case quote @@ -66,6 +68,10 @@ extension FormatItem { return Asset.Images.bulletList.name case .orderedList: return Asset.Images.numberedList.name + case .indent: + return Asset.Images.indentIncrease.name + case .unIndent: + return Asset.Images.indentDecrease.name case .inlineCode: return Asset.Images.code.name case .codeBlock: @@ -91,6 +97,10 @@ extension FormatItem { return "unorderedListButton" case .orderedList: return "orderedListButton" + case .indent: + return "indentListButton" + case .unIndent: + return "unIndentButton" case .inlineCode: return "inlineCodeButton" case .codeBlock: @@ -116,6 +126,10 @@ extension FormatItem { return VectorL10n.wysiwygComposerFormatActionUnorderedList case .orderedList: return VectorL10n.wysiwygComposerFormatActionOrderedList + case .indent: + return VectorL10n.wysiwygComposerFormatActionIndent + case .unIndent: + return VectorL10n.wysiwygComposerFormatActionUnIndent case .inlineCode: return VectorL10n.wysiwygComposerFormatActionInlineCode case .codeBlock: @@ -144,6 +158,10 @@ extension FormatType { return .unorderedList case .orderedList: return .orderedList + case .indent: + return .indent + case .unIndent: + return .unIndent case .inlineCode: return .inlineCode case .codeBlock: @@ -171,6 +189,10 @@ extension FormatType { return .unorderedList case .orderedList: return .orderedList + case .indent: + return .indent + case .unIndent: + return .unIndent case .inlineCode: return .inlineCode case .codeBlock: diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 324497cf3..82f7a89da 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -34,6 +34,7 @@ targets: dependencies: - sdk: Intents.framework - package: DeviceKit + - package: DTCoreText configFiles: Debug: Debug.xcconfig diff --git a/changelog.d/7316.change b/changelog.d/7316.change new file mode 100644 index 000000000..4ef97e1bd --- /dev/null +++ b/changelog.d/7316.change @@ -0,0 +1 @@ +Labs: Rich text editor: enable list items indentation diff --git a/project.yml b/project.yml index fe9af33a8..de099a507 100644 --- a/project.yml +++ b/project.yml @@ -53,7 +53,10 @@ packages: branch: main WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 0.19.0 + version: 0.22.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 + DTCoreText: + url: https://github.com/Cocoanetics/DTCoreText + version: 1.6.27 \ No newline at end of file From 1a3529d09bc1a1dfcbc2810ec482d8ee4f0452f9 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Mon, 30 Jan 2023 16:04:58 +0100 Subject: [PATCH 246/468] function renamed as in sdk --- .../Modules/ContextMenu/Services/RoomContextActionService.swift | 2 +- Riot/Modules/Room/MXKRoomViewController.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift index b19698da6..12c02c938 100644 --- a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift @@ -111,7 +111,7 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { self.delegate?.roomContextActionServiceDidMarkRoom(self) } func markAsUnread() { - room.markAsUnread() + room.setUnread() self.delegate?.roomContextActionServiceDidMarkRoom(self) } diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 06fa9d618..b1b43112c 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -373,7 +373,7 @@ if (!self.isContextPreview) { - [self.roomDataSource.room unmarkAsUnread]; + [self.roomDataSource.room resetUnread]; } } From 0d49c043a7aab2887791c9255a104402b658d13b Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 30 Jan 2023 16:37:58 +0100 Subject: [PATCH 247/468] Hide decryption errors only for voice broadcast chunks. --- .../Utils/EventFormatter/MXKEventFormatter.m | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 77ba974fd..da231c119 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1053,8 +1053,17 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode) { - // Hide the decryption error for event related to another one (like voicebroadcast chunks) + // Hide the decryption error for VoiceBroadcast chunks + BOOL isVoiceBroadcastChunk = NO; if ([event.relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { + MXEvent *startEvent = [mxSession.store eventWithEventId:event.relatesTo.eventId + inRoom:event.roomId]; + + if (startEvent) { + isVoiceBroadcastChunk = (startEvent.eventType == MXEventTypeCustom && [startEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]); + } + } + if (isVoiceBroadcastChunk) { displayText = nil; } else { // Make the unknown inbound session id error description more user friendly From ea6c6b171d477bb69f879560817dccb3442562ec Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 30 Jan 2023 14:45:07 +0000 Subject: [PATCH 248/468] Display backup import progress --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ ...verFromPrivateKeyViewController.storyboard | 26 ++++++++++++------- ...pRecoverFromPrivateKeyViewController.swift | 10 ++++--- ...BackupRecoverFromPrivateKeyViewModel.swift | 15 ++++++++++- ...BackupRecoverFromPrivateKeyViewState.swift | 2 +- changelog.d/pr-7319.change | 1 + 7 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 changelog.d/pr-7319.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index bff7b6ac3..9e94d569b 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1469,6 +1469,7 @@ Tap the + to start adding people."; // Recover from private key "key_backup_recover_from_private_key_info" = "Restoring backup…"; +"key_backup_recover_from_private_key_progress" = "%@%% Complete"; // Recover from passphrase diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 0327090c1..62d12a67f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2723,6 +2723,10 @@ public class VectorL10n: NSObject { public static var keyBackupRecoverFromPrivateKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_info") } + /// %@%% Complete + public static func keyBackupRecoverFromPrivateKeyProgress(_ p1: String) -> String { + return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_progress", p1) + } /// Use your Security Key to unlock your secure message history public static var keyBackupRecoverFromRecoveryKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_recovery_key_info") diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard index 1c8ba341c..42e99205e 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard @@ -1,25 +1,23 @@ - - - - + + - + - + - + - + @@ -40,15 +38,24 @@ + + + + @@ -72,6 +79,7 @@ + @@ -79,10 +87,10 @@ - + diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift index 1aaf96e62..a02fed201 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift @@ -29,6 +29,7 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { @IBOutlet private weak var shieldImageView: UIImageView! @IBOutlet private weak var informationLabel: UILabel! + @IBOutlet private weak var progressLabel: UILabel! // MARK: Private @@ -118,8 +119,8 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { private func render(viewState: KeyBackupRecoverFromPrivateKeyViewState) { switch viewState { - case .loading: - self.renderLoading() + case .loading(let progress): + self.renderLoading(progress: progress) case .loaded: self.renderLoaded() case .error(let error): @@ -127,8 +128,11 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { } } - private func renderLoading() { + private func renderLoading(progress: Double) { self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + + let percent = Int(round(progress * 100)) + self.progressLabel.text = VectorL10n.keyBackupRecoverFromPrivateKeyProgress("\(percent)") } private func renderLoaded() { diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift index cef1d7c0c..04fb48850 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift @@ -27,6 +27,7 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private let keyBackup: MXKeyBackup private var currentHTTPOperation: MXHTTPOperation? private let keyBackupVersion: MXKeyBackupVersion + private var progressUpdateTimer: Timer? // MARK: Public @@ -56,7 +57,14 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private func recoverWithPrivateKey() { - self.update(viewState: .loading) + self.update(viewState: .loading(0)) + + // Update loading progress every second until no longer loading + progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + if let progress = self?.keyBackup.importProgress { + self?.update(viewState: .loading(progress.fractionCompleted)) + } + } self.currentHTTPOperation = keyBackup.restore(usingPrivateKeyKeyBackup: keyBackupVersion, room: nil, session: nil, success: { [weak self] (_, _) in guard let self = self else { @@ -91,6 +99,11 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate } private func update(viewState: KeyBackupRecoverFromPrivateKeyViewState) { + if case .loading = viewState {} else { + progressUpdateTimer?.invalidate() + progressUpdateTimer = nil + } + self.viewDelegate?.keyBackupRecoverFromPrivateKeyViewModel(self, didUpdateViewState: viewState) } } diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift index bdd417853..b4ef05fb9 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift @@ -20,7 +20,7 @@ import Foundation /// KeyBackupRecoverFromPrivateKeyViewController view state enum KeyBackupRecoverFromPrivateKeyViewState { - case loading + case loading(Double) case loaded case error(Error) } diff --git a/changelog.d/pr-7319.change b/changelog.d/pr-7319.change new file mode 100644 index 000000000..187b315b5 --- /dev/null +++ b/changelog.d/pr-7319.change @@ -0,0 +1 @@ +Backup: Display backup import progress From 6027898c26dada0c597e5c929e4b64ed11f17131 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 2 Dec 2022 10:40:52 +0100 Subject: [PATCH 249/468] Add polls rule ids --- .../Model/NotificationPushRuleDefinitions.swift | 4 ++-- .../Notifications/Model/NotificationPushRuleIds.swift | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 77c6e1ef2..5daa05843 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -42,7 +42,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .highlight } - case .oneToOneRoom: + case .oneToOneRoom, .msc3930oneToOnePollStart, .msc3930oneToOnePollEnd: switch index { case .off: return .dontNotify case .silent: return .notify @@ -54,7 +54,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .notifyDefaultSound } - case .allOtherMessages: + case .allOtherMessages, .msc3930pollStart, .msc3930pollEnd: switch index { case .off: return .dontNotify case .silent: return .notify diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index d74968c8b..e47372099 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -29,6 +29,10 @@ enum NotificationPushRuleId: String { case oneToOneRoom = ".m.rule.room_one_to_one" case allOtherMessages = ".m.rule.message" case encrypted = ".m.rule.encrypted" + case msc3930pollStart = ".org.matrix.msc3930.rule.poll_start" + case msc3930oneToOnePollStart = ".org.matrix.msc3930.rule.poll_start_one_to_one" + case msc3930pollEnd = ".org.matrix.msc3930.rule.poll_end" + case msc3930oneToOnePollEnd = ".org.matrix.msc3930.rule.poll_end_one_to_one" case keywords = "_keywords" } @@ -65,6 +69,8 @@ extension NotificationPushRuleId { return VectorL10n.settingsEncryptedGroupMessages case .keywords: return VectorL10n.settingsMessagesContainingKeywords + case .msc3930pollStart, .msc3930oneToOnePollStart, .msc3930pollEnd, .msc3930oneToOnePollEnd: + return "" } } } From d1cc076ab90f31a74e4b49568828b87512143295 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 2 Dec 2022 12:31:26 +0100 Subject: [PATCH 250/468] Cleanup code --- .../NotificationPushRuleDefinitions.swift | 2 +- .../MXNotificationSettingsService.swift | 3 +- .../NotificationSettingsViewModel.swift | 31 ++++++++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 5daa05843..0fc2be7b2 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -22,7 +22,7 @@ extension NotificationPushRuleId { /// It is defined similarly across Web and Android. /// - Parameter index: The notification index for which to get the actions for. /// - Returns: The associated `NotificationStandardActions`. - func standardActions(for index: NotificationIndex) -> NotificationStandardActions? { + func standardActions(for index: NotificationIndex) -> NotificationStandardActions { switch self { case .containDisplayName: switch index { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 375b50ab9..fc4350873 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -59,8 +59,7 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { func add(keyword: String, enabled: Bool) { let index = NotificationIndex.index(when: enabled) - guard let actions = NotificationPushRuleId.keywords.standardActions(for: index)?.actions - else { + guard let actions = NotificationPushRuleId.keywords.standardActions(for: index).actions else { return } session.notificationCenter.addContentRuleWithRuleId(matchingPattern: keyword, notify: actions.notify, sound: actions.sound, highlight: actions.highlight) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 588597572..068e429f2 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -107,19 +107,19 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob func update(ruleID: NotificationPushRuleId, isChecked: Bool) { let index = NotificationIndex.index(when: isChecked) - if ruleID == .keywords { - // Keywords is handled differently to other settings - updateKeywords(isChecked: isChecked) - return - } - // Get the static definition and update the actions and enabled state. - guard let standardActions = ruleID.standardActions(for: index) else { return } + let standardActions = ruleID.standardActions(for: index) let enabled = standardActions != .disabled - notificationSettingsService.updatePushRuleActions( - for: ruleID.rawValue, - enabled: enabled, - actions: standardActions.actions - ) + + switch ruleID { + case .keywords: // Keywords is handled differently to other settings + updateKeywords(isChecked: isChecked) + default: + notificationSettingsService.updatePushRuleActions( + for: ruleID.rawValue, + enabled: enabled, + actions: standardActions.actions + ) + } } private func updateKeywords(isChecked: Bool) { @@ -129,8 +129,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } // Get the static definition and update the actions and enabled state for every keyword. let index = NotificationIndex.index(when: isChecked) - guard let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) else { return } + let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) let enabled = standardActions != .disabled + keywordsOrdered.forEach { keyword in notificationSettingsService.updatePushRuleActions( for: keyword, @@ -175,7 +176,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob /// - Parameter rule: The push rule type to check. /// - Returns: Wether it should be displayed as checked or not checked. private func isChecked(rule: NotificationPushRuleType) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + return false + } let firstIndex = NotificationIndex.allCases.first { nextIndex in rule.matches(standardActions: ruleId.standardActions(for: nextIndex)) From 861cebd9d0cc31e1d1934cfd36edd4fe9eadce96 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 2 Dec 2022 12:41:31 +0100 Subject: [PATCH 251/468] Add updatePushActions(for:enabled:standardActions) method --- .../ViewModel/NotificationSettingsViewModel.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 068e429f2..1105f2974 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -114,8 +114,14 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob case .keywords: // Keywords is handled differently to other settings updateKeywords(isChecked: isChecked) default: + updatePushActions(for: [ruleID], enabled: enabled, standardActions: standardActions) + } + } + + private func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { + for id in ids { notificationSettingsService.updatePushRuleActions( - for: ruleID.rawValue, + for: id.rawValue, enabled: enabled, actions: standardActions.actions ) From ddf2693865885246b976f5d06caf1a6f22d99ee6 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 2 Dec 2022 12:50:40 +0100 Subject: [PATCH 252/468] Add calls to update polls rules --- .../Service/MatrixSDK/MXNotificationSettingsService.swift | 5 ++++- .../ViewModel/NotificationSettingsViewModel.swift | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index fc4350873..4d966636b 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -71,7 +71,10 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { } func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { - guard let rule = session.notificationCenter.rule(byId: ruleId) else { return } + guard let rule = session.notificationCenter.rule(byId: ruleId) else { + return + } + session.notificationCenter.enableRule(rule, isEnabled: enabled) if let actions = actions { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 1105f2974..d5bd17fc1 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -113,6 +113,10 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob switch ruleID { case .keywords: // Keywords is handled differently to other settings updateKeywords(isChecked: isChecked) + case .oneToOneRoom: + updatePushActions(for: [ruleID, .msc3930oneToOnePollStart, .msc3930oneToOnePollEnd], enabled: enabled, standardActions: standardActions) + case .allOtherMessages: + updatePushActions(for: [ruleID, .msc3930pollStart, .msc3930pollEnd], enabled: enabled, standardActions: standardActions) default: updatePushActions(for: [ruleID], enabled: enabled, standardActions: standardActions) } From 10447947b040517112cdd57ac3ef86ab0dd7f877 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 10:41:30 +0100 Subject: [PATCH 253/468] Add comment --- .../Settings/Notifications/Model/NotificationPushRuleIds.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index e47372099..6f30b827e 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -70,6 +70,7 @@ extension NotificationPushRuleId { case .keywords: return VectorL10n.settingsMessagesContainingKeywords case .msc3930pollStart, .msc3930oneToOnePollStart, .msc3930pollEnd, .msc3930oneToOnePollEnd: + // They don't need to be rendered on the UI return "" } } From 30fb00bd337fcd0226d98a3b133e4827aee40685 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 11:33:21 +0100 Subject: [PATCH 254/468] Fix build errors after api change --- Riot/Categories/MXRoom+Riot.m | 2 +- .../Controllers/MXKNotificationSettingsViewController.m | 2 +- .../Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index df47c1674..04538ba80 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -637,7 +637,7 @@ }]; } - [notificationCenter enableRule:rule isEnabled:YES]; + [notificationCenter enableRule:rule isEnabled:YES completion:nil]; } - (void)setNotificationCenterDidFailObserver:(id)anObserver diff --git a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m index 1524eb2d7..7007c12bf 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m @@ -193,7 +193,7 @@ MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID]; if (pushRule) { - [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled]; + [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled completion:nil]; } } } diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m index a65174564..79632d87c 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m @@ -163,7 +163,7 @@ if (sender == _controlButton) { // Swap enable state - [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled]; + [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled completion:nil]; } else if (sender == _deleteButton) { From 0e8c78be075425255d9211270e5541d64ce7fb4b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 11:37:35 +0100 Subject: [PATCH 255/468] Add missing NotificationPushRuleIds --- .../NotificationPushRuleDefinitions.swift | 4 ++-- .../Model/NotificationPushRuleIds.swift | 18 +++++++++++++----- .../NotificationSettingsViewModel.swift | 4 ++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 0fc2be7b2..c30b81fec 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -42,7 +42,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .highlight } - case .oneToOneRoom, .msc3930oneToOnePollStart, .msc3930oneToOnePollEnd: + case .oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd: switch index { case .off: return .dontNotify case .silent: return .notify @@ -54,7 +54,7 @@ extension NotificationPushRuleId { case .silent: return .notify case .noisy: return .notifyDefaultSound } - case .allOtherMessages, .msc3930pollStart, .msc3930pollEnd: + case .allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd: switch index { case .off: return .dontNotify case .silent: return .notify diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index 6f30b827e..27d8b9e41 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -29,11 +29,19 @@ enum NotificationPushRuleId: String { case oneToOneRoom = ".m.rule.room_one_to_one" case allOtherMessages = ".m.rule.message" case encrypted = ".m.rule.encrypted" - case msc3930pollStart = ".org.matrix.msc3930.rule.poll_start" - case msc3930oneToOnePollStart = ".org.matrix.msc3930.rule.poll_start_one_to_one" - case msc3930pollEnd = ".org.matrix.msc3930.rule.poll_end" - case msc3930oneToOnePollEnd = ".org.matrix.msc3930.rule.poll_end_one_to_one" case keywords = "_keywords" + // poll started event + case pollStart = ".m.rule.poll_start" + case msc3930pollStart = ".org.matrix.msc3930.rule.poll_start" + // poll started event (one to one) + case oneToOnePollStart = ".m.rule.poll_start_one_to_one" + case msc3930oneToOnePollStart = ".org.matrix.msc3930.rule.poll_start_one_to_one" + // poll ended event + case pollEnd = ".m.rule.poll_end" + case msc3930pollEnd = ".org.matrix.msc3930.rule.poll_end" + // poll ended event (one to one) + case oneToOnePollEnd = ".m.rule.poll_end_one_to_one" + case msc3930oneToOnePollEnd = ".org.matrix.msc3930.rule.poll_end_one_to_one" } extension NotificationPushRuleId: Identifiable { @@ -69,7 +77,7 @@ extension NotificationPushRuleId { return VectorL10n.settingsEncryptedGroupMessages case .keywords: return VectorL10n.settingsMessagesContainingKeywords - case .msc3930pollStart, .msc3930oneToOnePollStart, .msc3930pollEnd, .msc3930oneToOnePollEnd: + case .pollStart, .msc3930pollStart, .oneToOnePollStart, .msc3930oneToOnePollStart, .pollEnd, .msc3930pollEnd, .oneToOnePollEnd, .msc3930oneToOnePollEnd: // They don't need to be rendered on the UI return "" } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index d5bd17fc1..03243d14f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -114,9 +114,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob case .keywords: // Keywords is handled differently to other settings updateKeywords(isChecked: isChecked) case .oneToOneRoom: - updatePushActions(for: [ruleID, .msc3930oneToOnePollStart, .msc3930oneToOnePollEnd], enabled: enabled, standardActions: standardActions) + updatePushActions(for: [ruleID, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd], enabled: enabled, standardActions: standardActions) case .allOtherMessages: - updatePushActions(for: [ruleID, .msc3930pollStart, .msc3930pollEnd], enabled: enabled, standardActions: standardActions) + updatePushActions(for: [ruleID, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd], enabled: enabled, standardActions: standardActions) default: updatePushActions(for: [ruleID], enabled: enabled, standardActions: standardActions) } From c295cf88bbee7790716f509a03009e059458af99 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 13:27:18 +0100 Subject: [PATCH 256/468] Update NotificationSettingsServiceType --- .../Service/MatrixSDK/MXNotificationSettingsService.swift | 7 +++++-- .../Service/Mock/MockNotificationSettingsService.swift | 3 ++- .../Service/NotificationSettingsServiceType.swift | 3 ++- .../ViewModel/NotificationSettingsViewModel.swift | 6 ++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 4d966636b..969deb14c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -70,12 +70,15 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { session.notificationCenter.removeRule(rule) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { guard let rule = session.notificationCenter.rule(byId: ruleId) else { return } - session.notificationCenter.enableRule(rule, isEnabled: enabled) + session.notificationCenter.enableRule(rule, isEnabled: enabled) { error in + #warning("complete here") + print("*** enable error: \(error)") + } if let actions = actions { session.notificationCenter.updatePushRuleActions(ruleId, diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 44a553f6c..17dc3b90e 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -44,5 +44,6 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { + } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index a5a1671e3..765c58701 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -40,5 +40,6 @@ protocol NotificationSettingsServiceType { /// - ruleId: The id of the rule. /// - enabled: Whether the rule should be enabled or disabled. /// - actions: The actions to update with. - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) + /// - completion: The completion of the operation. + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 03243d14f..736d3bc4f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -127,7 +127,8 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob notificationSettingsService.updatePushRuleActions( for: id.rawValue, enabled: enabled, - actions: standardActions.actions + actions: standardActions.actions, + completion: nil ) } } @@ -146,7 +147,8 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob notificationSettingsService.updatePushRuleActions( for: keyword, enabled: enabled, - actions: standardActions.actions + actions: standardActions.actions, + completion: nil ) } } From 3dfd572205f7d4917701ff6cb61d61b51cdd751b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 15:08:33 +0100 Subject: [PATCH 257/468] Fix push rule updates call order --- .../MXNotificationSettingsService.swift | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 969deb14c..f5e4c457a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -70,22 +70,54 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { session.notificationCenter.removeRule(rule) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { + func updatePushRuleActions(for ruleId: String, + enabled: Bool, + actions: NotificationActions?, + completion: ((Result) -> Void)?) { + guard let rule = session.notificationCenter.rule(byId: ruleId) else { + completion?(.success) return } - session.notificationCenter.enableRule(rule, isEnabled: enabled) { error in - #warning("complete here") - print("*** enable error: \(error)") + guard let actions = actions else { + enableRule(rule: rule, enabled: enabled, completion: completion) + return } - if let actions = actions { - session.notificationCenter.updatePushRuleActions(ruleId, - kind: rule.kind, - notify: actions.notify, - soundName: actions.sound, - highlight: actions.highlight) + // Updating the actions before enabling the rule allows the homeserver to triggers just one sync update + session.notificationCenter.updatePushRuleActions(ruleId, + kind: rule.kind, + notify: actions.notify, + soundName: actions.sound, + highlight: actions.highlight) { [weak self] error in + switch error.result { + case .success: + self?.enableRule(rule: rule, enabled: enabled, completion: completion) + case .failure: + completion?(error.result) + } } } } + +private extension MXNotificationSettingsService { + func enableRule(rule: MXPushRule, enabled: Bool, completion: ((Result) -> Void)?) { + session.notificationCenter.enableRule(rule, isEnabled: enabled) { error in + completion?(error.result) + } + } +} + +private extension Result where Success == Void { + static var success: Self { + .success(()) + } +} + +private extension Optional where Wrapped == Error { + var result: Result { + map { .failure($0) } ?? .success + } +} + From 0b714600612cf1a76657311887c81d928819525e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 18:21:43 +0100 Subject: [PATCH 258/468] Create private extension of NotificationSettingsViewModel --- .../NotificationSettingsViewModel.swift | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 736d3bc4f..5b6414a20 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -122,18 +122,23 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } } - private func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { - for id in ids { - notificationSettingsService.updatePushRuleActions( - for: id.rawValue, - enabled: enabled, - actions: standardActions.actions, - completion: nil - ) + func add(keyword: String) { + if !keywordsOrdered.contains(keyword) { + keywordsOrdered.append(keyword) } + notificationSettingsService.add(keyword: keyword, enabled: true) } - private func updateKeywords(isChecked: Bool) { + func remove(keyword: String) { + keywordsOrdered = keywordsOrdered.filter { $0 != keyword } + notificationSettingsService.remove(keyword: keyword) + } +} + +// MARK: - Private + +private extension NotificationSettingsViewModel { + func updateKeywords(isChecked: Bool) { guard !keywordsOrdered.isEmpty else { viewState.selectionState[.keywords]?.toggle() return @@ -153,21 +158,18 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } } - func add(keyword: String) { - if !keywordsOrdered.contains(keyword) { - keywordsOrdered.append(keyword) + func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { + for id in ids { + notificationSettingsService.updatePushRuleActions( + for: id.rawValue, + enabled: enabled, + actions: standardActions.actions, + completion: nil + ) } - notificationSettingsService.add(keyword: keyword, enabled: true) } - func remove(keyword: String) { - keywordsOrdered = keywordsOrdered.filter { $0 != keyword } - notificationSettingsService.remove(keyword: keyword) - } - - // MARK: - Private - - private func rulesUpdated(newRules: [NotificationPushRuleType]) { + func rulesUpdated(newRules: [NotificationPushRuleType]) { for rule in newRules { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), ruleIds.contains(ruleId) else { continue } @@ -175,7 +177,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } } - private func keywordRuleUpdated(anyEnabled: Bool) { + func keywordRuleUpdated(anyEnabled: Bool) { if !keywordsOrdered.isEmpty { viewState.selectionState[.keywords] = anyEnabled } @@ -187,7 +189,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob /// The same logic is used on android. /// - Parameter rule: The push rule type to check. /// - Returns: Wether it should be displayed as checked or not checked. - private func isChecked(rule: NotificationPushRuleType) -> Bool { + func isChecked(rule: NotificationPushRuleType) -> Bool { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } From 921152e219a42e9942ae0b9b880bf8d2f251976a Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 27 Jan 2023 18:46:22 +0100 Subject: [PATCH 259/468] Start poll push rule sync logic --- .../NotificationSettingsViewModel.swift | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 5b6414a20..bbc7ea01a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -113,12 +113,32 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob switch ruleID { case .keywords: // Keywords is handled differently to other settings updateKeywords(isChecked: isChecked) + case .oneToOneRoom: - updatePushActions(for: [ruleID, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd], enabled: enabled, standardActions: standardActions) + updatePushAction( + id: ruleID, + enabled: enabled, + standardActions: standardActions, + then: [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd], + completion: nil + ) + case .allOtherMessages: - updatePushActions(for: [ruleID, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd], enabled: enabled, standardActions: standardActions) + updatePushAction( + id: ruleID, + enabled: enabled, + standardActions: standardActions, + then: [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd], + completion: nil + ) + default: - updatePushActions(for: [ruleID], enabled: enabled, standardActions: standardActions) + notificationSettingsService.updatePushRuleActions( + for: ruleID.rawValue, + enabled: enabled, + actions: standardActions.actions, + completion: nil + ) } } @@ -157,6 +177,27 @@ private extension NotificationSettingsViewModel { ) } } + + func updatePushAction(id: NotificationPushRuleId, + enabled: Bool, + standardActions: NotificationStandardActions, + then rules: [NotificationPushRuleId], + completion: ((Result) -> Void)?) { + + notificationSettingsService.updatePushRuleActions( + for: id.rawValue, + enabled: enabled, + actions: standardActions.actions) { [weak self] result in + switch result { + case .success: + #warning("TODO: sync the update of these rules with the completion") + self?.updatePushActions(for: rules, enabled: enabled, standardActions: standardActions) + completion?(.success(())) + case .failure(let error): + completion?(.failure(error)) + } + } + } func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { for id in ids { From fff2d6f5e5facc87f2d5005c52c9c09858145ddc Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 11:53:32 +0100 Subject: [PATCH 260/468] Refine poll push rule sync logic --- .../NotificationSettingsServiceType.swift | 10 ++++++++++ .../NotificationSettingsViewModel.swift | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index 765c58701..9e6419d23 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -43,3 +43,13 @@ protocol NotificationSettingsServiceType { /// - completion: The completion of the operation. func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) } + +extension NotificationSettingsServiceType { + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws { + try await withCheckedThrowingContinuation { continuation in + updatePushRuleActions(for: ruleId, enabled: enabled, actions: actions) { result in + continuation.resume(with: result) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index bbc7ea01a..767e95803 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -187,16 +187,17 @@ private extension NotificationSettingsViewModel { notificationSettingsService.updatePushRuleActions( for: id.rawValue, enabled: enabled, - actions: standardActions.actions) { [weak self] result in - switch result { - case .success: - #warning("TODO: sync the update of these rules with the completion") - self?.updatePushActions(for: rules, enabled: enabled, standardActions: standardActions) - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) - } + actions: standardActions.actions + ) { [weak self] result in + switch result { + case .success: + #warning("TODO: sync the update of these rules with the completion") + self?.updatePushActions(for: rules, enabled: enabled, standardActions: standardActions) + completion?(.success(())) + case .failure(let error): + completion?(.failure(error)) } + } } func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { From d08a2fb89094c12d2b55d1b3b65134c8ecbeec55 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 11:59:30 +0100 Subject: [PATCH 261/468] Cleanup --- .../Service/Mock/MockNotificationSettingsService.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 17dc3b90e..28aa9fe10 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -44,6 +44,5 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { - } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { } } From 6f5012a38f1478c9a75f20c0c4449f5a55bdbd1e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 12:06:19 +0100 Subject: [PATCH 262/468] Add Task to update rules --- .../View/NotificationSettings.swift | 1 + .../NotificationSettingsViewModel.swift | 53 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 18be2680d..407be6828 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -40,6 +40,7 @@ struct NotificationSettings: View { bottomSection } .activityIndicator(show: viewModel.viewState.saving) + .disabled(viewModel.viewState.saving) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 767e95803..bfc262c01 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -119,8 +119,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob id: ruleID, enabled: enabled, standardActions: standardActions, - then: [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd], - completion: nil + then: [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] ) case .allOtherMessages: @@ -128,8 +127,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob id: ruleID, enabled: enabled, standardActions: standardActions, - then: [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd], - completion: nil + then: [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] ) default: @@ -181,34 +179,35 @@ private extension NotificationSettingsViewModel { func updatePushAction(id: NotificationPushRuleId, enabled: Bool, standardActions: NotificationStandardActions, - then rules: [NotificationPushRuleId], - completion: ((Result) -> Void)?) { + then rules: [NotificationPushRuleId]) { - notificationSettingsService.updatePushRuleActions( - for: id.rawValue, - enabled: enabled, - actions: standardActions.actions - ) { [weak self] result in - switch result { - case .success: - #warning("TODO: sync the update of these rules with the completion") - self?.updatePushActions(for: rules, enabled: enabled, standardActions: standardActions) - completion?(.success(())) - case .failure(let error): - completion?(.failure(error)) + viewState.saving = true + + Task { + do { + try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions) + + try await withThrowingTaskGroup(of: Void.self) { group in + for ruleId in rules { + group.addTask { + try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions) + } + } + + try await group.waitForAll() + await completeUpdate(error: nil) + } + } + catch { + await completeUpdate(error: error) } } } - func updatePushActions(for ids: [NotificationPushRuleId], enabled: Bool, standardActions: NotificationStandardActions) { - for id in ids { - notificationSettingsService.updatePushRuleActions( - for: id.rawValue, - enabled: enabled, - actions: standardActions.actions, - completion: nil - ) - } + @MainActor + func completeUpdate(error: Error?) { + #warning("Handle error here in the next ticket") + viewState.saving = false } func rulesUpdated(newRules: [NotificationPushRuleType]) { From c708c61d0daadac508813af1f0e15e66e38e60ac Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 12:32:57 +0100 Subject: [PATCH 263/468] Cleanup --- .../Model/NotificationPushRuleIds.swift | 11 ++++++++++ .../NotificationSettingsViewModel.swift | 20 ++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index 27d8b9e41..46cca080e 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -82,4 +82,15 @@ extension NotificationPushRuleId { return "" } } + + var syncedRules: [NotificationPushRuleId] { + switch self { + case .oneToOneRoom: + return [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + case .allOtherMessages: + return [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] + default: + return [] + } + } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index bfc262c01..0dac38eb8 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -114,20 +114,12 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob case .keywords: // Keywords is handled differently to other settings updateKeywords(isChecked: isChecked) - case .oneToOneRoom: + case .oneToOneRoom, .allOtherMessages: updatePushAction( id: ruleID, enabled: enabled, standardActions: standardActions, - then: [.oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] - ) - - case .allOtherMessages: - updatePushAction( - id: ruleID, - enabled: enabled, - standardActions: standardActions, - then: [.pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] + then: ruleID.syncedRules ) default: @@ -212,8 +204,12 @@ private extension NotificationSettingsViewModel { func rulesUpdated(newRules: [NotificationPushRuleType]) { for rule in newRules { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), - ruleIds.contains(ruleId) else { continue } + guard + let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + ruleIds.contains(ruleId) + else { + continue + } viewState.selectionState[ruleId] = isChecked(rule: rule) } } From 52eadbbb7b9a456c8b70611ee2002f45628ac6bb Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 12:57:17 +0100 Subject: [PATCH 264/468] Add loudest option logic --- .../NotificationSettingsViewModel.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 0dac38eb8..24f5b4f97 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -210,7 +210,8 @@ private extension NotificationSettingsViewModel { else { continue } - viewState.selectionState[ruleId] = isChecked(rule: rule) + + viewState.selectionState[ruleId] = isChecked(rule: rule, syncedRules: ruleId.syncedRules(in: newRules)) } } @@ -226,7 +227,7 @@ private extension NotificationSettingsViewModel { /// The same logic is used on android. /// - Parameter rule: The push rule type to check. /// - Returns: Wether it should be displayed as checked or not checked. - func isChecked(rule: NotificationPushRuleType) -> Bool { + func defaultIsChecked(rule: NotificationPushRuleType) -> Bool { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } @@ -241,4 +242,31 @@ private extension NotificationSettingsViewModel { return index.enabled } + + func isChecked(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + return false + } + + switch ruleId { + case .oneToOneRoom, .allOtherMessages: + let ruleIsChecked = defaultIsChecked(rule: rule) + let someSyncedRuleIsChecked = syncedRules.contains(where: { defaultIsChecked(rule: $0) }) + + return ruleIsChecked || someSyncedRuleIsChecked + default: + return defaultIsChecked(rule: rule) + } + } +} + +private extension NotificationPushRuleId { + func syncedRules(in rules: [NotificationPushRuleType]) -> [NotificationPushRuleType] { + rules.filter { + guard let ruleId = NotificationPushRuleId(rawValue: $0.ruleId) else { + return false + } + return syncedRules.contains(ruleId) + } + } } From c8ace35b0a234d14ec4b318a66d0ed0d19ee6ce9 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 16:12:43 +0100 Subject: [PATCH 265/468] Add comment --- .../Notifications/ViewModel/NotificationSettingsViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 24f5b4f97..11b284b55 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -252,7 +252,7 @@ private extension NotificationSettingsViewModel { case .oneToOneRoom, .allOtherMessages: let ruleIsChecked = defaultIsChecked(rule: rule) let someSyncedRuleIsChecked = syncedRules.contains(where: { defaultIsChecked(rule: $0) }) - + // The "loudest" rule will be applied when there is a clash between a rule and its dependent rules. return ruleIsChecked || someSyncedRuleIsChecked default: return defaultIsChecked(rule: rule) From 857aa98430ae7415d2e7bfc11c48fca0f73b3f25 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 17:45:10 +0100 Subject: [PATCH 266/468] Add UTs --- .../Model/Mock/MockNotificationPushRule.swift | 4 +- .../Model/NotificationActions.swift | 2 +- .../MockNotificationSettingsService.swift | 12 +- .../NotificationSettingsViewModelTests.swift | 166 ++++++++++++++++++ .../NotificationSettingsViewModel.swift | 5 +- 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift index 49166c99e..ab7192646 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift @@ -19,7 +19,9 @@ import Foundation struct MockNotificationPushRule: NotificationPushRuleType { var ruleId: String! var enabled: Bool + var actions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions + func matches(standardActions: NotificationStandardActions?) -> Bool { - false + standardActions?.actions == actions } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift index 88b11b3be..98fa34764 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift @@ -17,7 +17,7 @@ import Foundation /// The actions defined on a push rule, used in the static push rule definitions. -struct NotificationActions { +struct NotificationActions: Equatable { let notify: Bool let highlight: Bool let sound: String? diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 28aa9fe10..32cc12c98 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -44,5 +44,15 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { + guard let ruleIndex = rules.firstIndex(where: { $0.ruleId == ruleId }) else { + completion?(.failure(NSError(domain: "fake", code: 0))) + return + } + + rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, + enabled: enabled, + actions: actions) + completion?(.success(())) + } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift new file mode 100644 index 000000000..b80e44c9a --- /dev/null +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -0,0 +1,166 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import RiotSwiftUI +import XCTest + +final class NotificationSettingsViewModelTests: XCTestCase { + private var viewModel: NotificationSettingsViewModel! + private var notificationService: MockNotificationSettingsService! + + override func setUpWithError() throws { + notificationService = .init() + } + + func testAllTheRulesAreChecked() throws { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 4) + XCTAssertTrue(viewModel.viewState.selectionState.values.allSatisfy { $0 }) + } + + func testUpdateRule() throws { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default) + notificationService.rules = [MockNotificationPushRule].default + + viewModel.update(ruleID: .encrypted, isChecked: false) + XCTAssertEqual(viewModel.viewState.selectionState.count, 4) + XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false) + } + + func testUpdateOneToOneRuleAlsoUpdatesPollRules() { + let expectation = expectation(description: #function) + setupWithPollRules() + + viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], false) + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollStart], false) + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollEnd], false) + + // unrelated poll rules stay the same + XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], true) + XCTAssertEqual(self.viewModel.viewState.selectionState[.pollStart], true) + XCTAssertEqual(self.viewModel.viewState.selectionState[.pollEnd], true) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testUpdateMessageRuleAlsoUpdatesPollRules() { + let expectation = expectation(description: #function) + setupWithPollRules() + + viewModel.update(ruleID: .allOtherMessages, isChecked: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], false) + XCTAssertEqual(self.viewModel.viewState.selectionState[.pollStart], false) + XCTAssertEqual(self.viewModel.viewState.selectionState[.pollEnd], false) + + // unrelated poll rules stay the same + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], true) + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollStart], true) + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollEnd], true) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testMismatchingRulesAreHandled() { + let expectation = expectation(description: #function) + setupWithPollRules() + + viewModel.update(ruleID: .allOtherMessages, isChecked: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // simulating a "mismatch" on the poll started rule + self.viewModel.update(ruleID: .pollStart, isChecked: true) + + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) + + // The other messages rule ui flag should match the loudest related poll rule + XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], true) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testMismatchingOneToOneRulesAreHandled() { + let expectation = expectation(description: #function) + setupWithPollRules() + + viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // simulating a "mismatch" on the one to one poll started rule + self.viewModel.update(ruleID: .oneToOnePollStart, isChecked: true) + + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) + + // The one to one room rule ui flag should match the loudest related poll rule + XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], true) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } +} + +private extension NotificationSettingsViewModelTests { + func setupWithPollRules() { + viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default + .polls) + notificationService.rules = [MockNotificationPushRule].default + [MockNotificationPushRule].polls + } +} + +private extension Array where Element == NotificationPushRuleId { + static var `default`: [NotificationPushRuleId] { + [.oneToOneRoom, .allOtherMessages, .oneToOneEncryptedRoom, .encrypted] + } + + static var polls: [NotificationPushRuleId] { + [.pollStart, .pollEnd, .oneToOnePollStart, .oneToOnePollEnd] + } +} + +private extension Array where Element == MockNotificationPushRule { + static var `default`: [MockNotificationPushRule] { + [NotificationPushRuleId] + .default + .map { ruleId in + MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true) + } + } + + static var polls: [MockNotificationPushRule] { + [NotificationPushRuleId] + .polls + .map { ruleId in + MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: true) + } + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 11b284b55..73f11a931 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -172,7 +172,7 @@ private extension NotificationSettingsViewModel { enabled: Bool, standardActions: NotificationStandardActions, then rules: [NotificationPushRuleId]) { - + viewState.saving = true Task { @@ -189,8 +189,7 @@ private extension NotificationSettingsViewModel { try await group.waitForAll() await completeUpdate(error: nil) } - } - catch { + } catch { await completeUpdate(error: error) } } From 3908d168174e79e7a1846e073fb3d238b56ae331 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 17:55:38 +0100 Subject: [PATCH 267/468] Add changelog.d file --- changelog.d/pr-7320.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7320.change diff --git a/changelog.d/pr-7320.change b/changelog.d/pr-7320.change new file mode 100644 index 000000000..3d34c84e2 --- /dev/null +++ b/changelog.d/pr-7320.change @@ -0,0 +1 @@ +Polls: sync push rules with the one of normal messages. From 0497927953e5ff6d3b4e55ff826ad882386fa235 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 30 Jan 2023 18:23:31 +0100 Subject: [PATCH 268/468] Improve tests --- .../MockNotificationSettingsService.swift | 2 +- .../NotificationSettingsViewModelTests.swift | 36 ++++++++++++------- .../NotificationSettingsViewModel.swift | 18 +++++----- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 32cc12c98..7d74f5288 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -46,7 +46,7 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { guard let ruleIndex = rules.firstIndex(where: { $0.ruleId == ruleId }) else { - completion?(.failure(NSError(domain: "fake", code: 0))) + completion?(.success(())) return } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index b80e44c9a..ea698d9e0 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -45,9 +45,12 @@ final class NotificationSettingsViewModelTests: XCTestCase { let expectation = expectation(description: #function) setupWithPollRules() - viewModel.update(ruleID: .oneToOneRoom, isChecked: false) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + viewModel.update(ruleID: .oneToOneRoom, isChecked: false) { result in + guard case .success = result else { + XCTFail() + return + } + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], false) XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollStart], false) @@ -68,9 +71,12 @@ final class NotificationSettingsViewModelTests: XCTestCase { let expectation = expectation(description: #function) setupWithPollRules() - viewModel.update(ruleID: .allOtherMessages, isChecked: false) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + viewModel.update(ruleID: .allOtherMessages, isChecked: false) { result in + guard case .success = result else { + XCTFail() + return + } + XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], false) XCTAssertEqual(self.viewModel.viewState.selectionState[.pollStart], false) @@ -91,9 +97,12 @@ final class NotificationSettingsViewModelTests: XCTestCase { let expectation = expectation(description: #function) setupWithPollRules() - viewModel.update(ruleID: .allOtherMessages, isChecked: false) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + viewModel.update(ruleID: .allOtherMessages, isChecked: false) { result in + guard case .success = result else { + XCTFail() + return + } + // simulating a "mismatch" on the poll started rule self.viewModel.update(ruleID: .pollStart, isChecked: true) @@ -112,9 +121,12 @@ final class NotificationSettingsViewModelTests: XCTestCase { let expectation = expectation(description: #function) setupWithPollRules() - viewModel.update(ruleID: .oneToOneRoom, isChecked: false) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + viewModel.update(ruleID: .oneToOneRoom, isChecked: false) { result in + guard case .success = result else { + XCTFail() + return + } + // simulating a "mismatch" on the one to one poll started rule self.viewModel.update(ruleID: .oneToOnePollStart, isChecked: true) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 73f11a931..208b8d2bb 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -105,7 +105,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // MARK: - Public - func update(ruleID: NotificationPushRuleId, isChecked: Bool) { + func update(ruleID: NotificationPushRuleId, isChecked: Bool, completion: ((Result) -> Void)? = nil) { let index = NotificationIndex.index(when: isChecked) let standardActions = ruleID.standardActions(for: index) let enabled = standardActions != .disabled @@ -119,7 +119,8 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob id: ruleID, enabled: enabled, standardActions: standardActions, - then: ruleID.syncedRules + then: ruleID.syncedRules, + completion: completion ) default: @@ -127,7 +128,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob for: ruleID.rawValue, enabled: enabled, actions: standardActions.actions, - completion: nil + completion: completion ) } } @@ -171,8 +172,8 @@ private extension NotificationSettingsViewModel { func updatePushAction(id: NotificationPushRuleId, enabled: Bool, standardActions: NotificationStandardActions, - then rules: [NotificationPushRuleId]) { - + then rules: [NotificationPushRuleId], + completion: ((Result) -> Void)?) { viewState.saving = true Task { @@ -187,18 +188,19 @@ private extension NotificationSettingsViewModel { } try await group.waitForAll() - await completeUpdate(error: nil) + await completeUpdate(completion: completion, result: .success(())) } } catch { - await completeUpdate(error: error) + await completeUpdate(completion: completion, result: .failure(error)) } } } @MainActor - func completeUpdate(error: Error?) { + func completeUpdate(completion: ((Result) -> Void)?, result: Result) { #warning("Handle error here in the next ticket") viewState.saving = false + completion?(result) } func rulesUpdated(newRules: [NotificationPushRuleType]) { From a1f6813e6947455bb58005fa2ef82048fe80263d Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 31 Jan 2023 10:21:18 +0100 Subject: [PATCH 269/468] Update changelog. --- changelog.d/7189.change | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7189.change b/changelog.d/7189.change index a9acc4ba3..98ffa1e03 100644 --- a/changelog.d/7189.change +++ b/changelog.d/7189.change @@ -1 +1 @@ -Voice Broadcast: Inform the user about decryption errors during a voice broadcast. +Inform the user about decryption errors during a voice broadcast. From 1291648acbfe596c2fd03d4c7953d116fe16a45c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 31 Jan 2023 10:30:25 +0100 Subject: [PATCH 270/468] Empty-Commit From 328f0675a768d18339a444628daa26bc30d0dd56 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 27 Jan 2023 13:01:51 +0100 Subject: [PATCH 271/468] Improve error handling during a voice broadcast playback --- .../VoiceBroadcastPlaybackViewModel.swift | 89 +++++++++++++++---- .../View/VoiceBroadcastPlaybackView.swift | 2 +- .../VoiceBroadcastPlaybackModels.swift | 1 + .../VoiceBroadcastPlaybackScreenState.swift | 2 +- changelog.d/7311.change | 1 + 5 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 changelog.d/7311.change diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index fd1ca9157..af229c35e 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -44,8 +44,17 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private var reloadVoiceBroadcastChunkQueue: Bool = false private var seekToChunkTime: TimeInterval? + /// The last chunk we tried to load + private var lastChunkProcessed: UInt = 0 + /// The last chunk correctly loaded and added to the player's queue private var lastChunkAddedToPlayer: UInt = 0 + private var hasAttachmentErrors: Bool = false { + didSet { + updateErrorState() + } + } + private var isPlayingLastChunk: Bool { // We can't play the last chunk if the brodcast is not stopped guard state.broadcastState == .stopped else { @@ -60,18 +69,19 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration) } - private var playingChunk: VoiceBroadcastChunk? { + /// Current chunk loaded in the audio player + private var currentChunk: VoiceBroadcastChunk? { guard let currentAudioPlayerUrl = audioPlayer?.currentUrl, - let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in + let currentEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in result.url == currentAudioPlayerUrl })?.eventIdentifier else { return nil } - let playingChunk = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in - chunk.attachment.eventId == playingEventId + let currentChunk = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in + chunk.attachment.eventId == currentEventId }) - return playingChunk + return currentChunk } private var isLivePlayback: Bool { @@ -112,7 +122,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), - decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0)) + decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0), + showPlaybackError: false) super.init(initialViewState: viewState) displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -198,8 +209,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // If we known the last chunk sequence, use it to check if we need to stop // Note: it's possible to be in .stopped state and to still have a last chunk sequence at 0 (old versions or a crash during recording). In this case, we use isPlayingLastChunk as a fallback solution if voiceBroadcastAggregator.voiceBroadcastLastChunkSequence > 0 { - // we should stop only if we have already added the last chunk to the player - shouldStop = (lastChunkAddedToPlayer == voiceBroadcastAggregator.voiceBroadcastLastChunkSequence) + // we should stop only if we have already processed the last chunk + shouldStop = (lastChunkProcessed == voiceBroadcastAggregator.voiceBroadcastLastChunkSequence) } else { shouldStop = isPlayingLastChunk } @@ -236,10 +247,12 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func seek(to seekTime: Float) { // Flush the chunks queue and the current audio player playlist + lastChunkProcessed = 0 lastChunkAddedToPlayer = 0 voiceBroadcastChunkQueue = [] reloadVoiceBroadcastChunkQueue = isProcessingVoiceBroadcastChunk audioPlayer?.removeAllPlayerItems() + hasAttachmentErrors = false let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) @@ -326,6 +339,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } + self.lastChunkProcessed = chunk.sequence + switch result { case .success(let result): guard result.eventIdentifier == chunk.attachment.eventId else { @@ -369,19 +384,46 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic audioPlayer.seekToTime(time) self.seekToChunkTime = nil } - + + self.hasAttachmentErrors = false + self.processNextVoiceBroadcastChunk() + case .failure (let error): - MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) - if self.voiceBroadcastChunkQueue.count == 0 { - // No more chunk to try. Go to error - self.state.playbackState = .error + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: ["error": error, "chunk": chunk.sequence]) + self.hasAttachmentErrors = true + // If nothing has been added to the player's queue, exit the buffer state + if self.lastChunkAddedToPlayer == 0 { + self.pause() } } - - self.processNextVoiceBroadcastChunk() } } + private func resetErrorState() { + state.showPlaybackError = false + } + + private func updateErrorState() { + // Show an error if the playback state is .error + var showPlaybackError = state.playbackState == .error + + // Or if there is an attachment error + if hasAttachmentErrors { + // only if the audio player is not playing and has nothing left to play + let audioPlayerIsPlaying = audioPlayer?.isPlaying ?? false + let currentPlayerTime = audioPlayer?.currentTime ?? 0 + let currentPlayerDuration = audioPlayer?.duration ?? 0 + let currentChunkSequence = currentChunk?.sequence ?? 0 + let hasNoMoreChunkToPlay = (currentChunk == nil && lastChunkAddedToPlayer == 0) || (currentChunkSequence == lastChunkAddedToPlayer) + if !audioPlayerIsPlaying && hasNoMoreChunkToPlay && (currentPlayerDuration - currentPlayerTime < 0.2) { + showPlaybackError = true + } + } + + state.showPlaybackError = showPlaybackError + + } + private func updateDuration() { let duration = voiceBroadcastAggregator.voiceBroadcast.duration state.playingState.duration = Float(duration) @@ -404,10 +446,11 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } else { seek(to: state.bindings.progress) } + resetErrorState() } @objc private func handleDisplayLinkTick() { - guard let playingSequence = self.playingChunk?.sequence else { + guard let playingSequence = self.currentChunk?.sequence else { return } @@ -438,7 +481,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic state.playingState.remainingTimeLabel = label state.playingState.canMoveBackward = state.bindings.progress > 0 - state.playingState.canMoveForward = state.bindings.progress < state.playingState.duration + state.playingState.canMoveForward = (state.playingState.duration - state.bindings.progress) > 500 } private func handleVoiceBroadcastChunksProcessing() { @@ -504,6 +547,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { // MARK: - VoiceMessageAudioPlayerDelegate extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) { + updateErrorState() } func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -511,6 +555,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { state.playingState.isLive = isLivePlayback isPlaybackInitialized = true displayLink.isPaused = false + resetErrorState() } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -522,6 +567,9 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying") state.playbackState = .stopped + + updateErrorState() + state.playingState.isLive = false audioPlayer.deregisterDelegate(self) self.mediaServiceProvider.deregisterNowPlayingInfoDelegate(forPlayer: audioPlayer) @@ -531,11 +579,16 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { state.playbackState = .error + self.updateErrorState() } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidFinishPlaying: \(audioPlayer.playerItems.count)") - stopIfVoiceBroadcastOver() + if hasAttachmentErrors { + stop() + } else { + stopIfVoiceBroadcastOver() + } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 6ac146ce2..b4bcaa7aa 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -114,7 +114,7 @@ struct VoiceBroadcastPlaybackView: View { .fixedSize(horizontal: false, vertical: true) .accessibilityIdentifier("decryptionErrorView") } - else if viewModel.viewState.playbackState == .error { + else if viewModel.viewState.showPlaybackError { VoiceBroadcastPlaybackErrorView() } else { HStack (spacing: 34.0) { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index aeb1f4f61..7a810a167 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -59,6 +59,7 @@ struct VoiceBroadcastPlaybackViewState: BindableState { var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings var decryptionState: VoiceBroadcastPlaybackDecryptionState + var showPlaybackError: Bool } struct VoiceBroadcastPlaybackViewStateBindings { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 84f210d81..59b434ca9 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -43,7 +43,7 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0))) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0), showPlaybackError: false)) return ( [false, viewModel], diff --git a/changelog.d/7311.change b/changelog.d/7311.change new file mode 100644 index 000000000..d9b992237 --- /dev/null +++ b/changelog.d/7311.change @@ -0,0 +1 @@ +Improve error handling during a voice broadcast playback. From 7352cd479c0e090fe00b39996f71f0b9e81d5420 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 31 Jan 2023 13:07:16 +0000 Subject: [PATCH 272/468] Reset Crypto SDK on logout --- Config/AppConfiguration.swift | 3 + Config/CommonConfiguration.swift | 6 -- Config/CryptoSDKConfiguration.swift | 55 +++++++++++++++++++ Riot/Modules/Application/LegacyAppDelegate.m | 5 ++ .../Modules/Settings/SettingsViewController.m | 3 +- changelog.d/pr-7323.change | 1 + 6 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 Config/CryptoSDKConfiguration.swift create mode 100644 changelog.d/pr-7323.change diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 70b1d78d5..fe83fba1f 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -24,6 +24,9 @@ class AppConfiguration: CommonConfiguration { override func setupSettings() { super.setupSettings() setupAppSettings() +#if DEBUG + CryptoSDKConfiguration.shared.setup() +#endif } private func setupAppSettings() { diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index f3172a710..fee3796ff 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,12 +91,6 @@ class CommonConfiguration: NSObject, Configurable { MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - - #if DEBUG - if sdkOptions.isCryptoSDKAvailable { - sdkOptions.enableCryptoSDK = RiotSettings.shared.enableCryptoSDK - } - #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift new file mode 100644 index 000000000..6edde7871 --- /dev/null +++ b/Config/CryptoSDKConfiguration.swift @@ -0,0 +1,55 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG + +/// Configuration for enabling / disabling Matrix Crypto SDK +@objcMembers class CryptoSDKConfiguration: NSObject { + static let shared = CryptoSDKConfiguration() + + func setup() { + guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { + return + } + + let isEnabled = RiotSettings.shared.enableCryptoSDK + MXSDKOptions.sharedInstance().enableCryptoSDK = isEnabled + + MXLog.debug("[CryptoSDKConfiguration] setup: Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + } + + func enable() { + guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { + return + } + + RiotSettings.shared.enableCryptoSDK = true + MXSDKOptions.sharedInstance().enableCryptoSDK = true + + MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK") + } + + func disable() { + RiotSettings.shared.enableCryptoSDK = false + MXSDKOptions.sharedInstance().enableCryptoSDK = false + + MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK") + } +} + +#endif diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a28ee037f..05fd5bcec 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2183,6 +2183,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; + // Reset Crypto SDK configuration (labs flag for which crypto module to use) +#if DEBUG + [CryptoSDKConfiguration.shared disable]; +#endif + // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index fc10edf0e..5cf6e933b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3400,8 +3400,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); - RiotSettings.shared.enableCryptoSDK = isEnabled; - MXSDKOptions.sharedInstance.enableCryptoSDK = isEnabled; + [CryptoSDKConfiguration.shared enable]; [[AppDelegate theDelegate] reloadMatrixSessions:YES]; }]]; diff --git a/changelog.d/pr-7323.change b/changelog.d/pr-7323.change new file mode 100644 index 000000000..308cf2813 --- /dev/null +++ b/changelog.d/pr-7323.change @@ -0,0 +1 @@ +CryptoV2: Reset Crypto SDK on logout From 7b4333e9af3f7a7105525ad925439bd36a9339be Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Tue, 31 Jan 2023 16:03:38 +0100 Subject: [PATCH 273/468] code restyle for poll detail --- .../Room/RoomInfo/RoomInfoCoordinator.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 32 +++++-------- .../ExploreRoomCoordinator.swift | 2 +- .../Coordinator/PollHistoryCoordinator.swift | 47 ++++++++++--------- .../PollHistoryDetailCoordinator.swift | 41 ++++------------ .../MockPollHistoryDetailScreenState.swift | 1 - .../PollHistoryDetailModels.swift | 12 ++--- .../PollHistoryDetailViewModel.swift | 11 +---- .../PollHistoryDetailViewModelProtocol.swift | 1 + .../PollHistoryDetailViewModelTests.swift | 18 +++++-- .../View/PollHistoryDetail.swift | 18 +++---- 11 files changed, 80 insertions(+), 105 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 26246eeae..a34507711 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -178,11 +178,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { case .pollHistory: let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: room, navigationRouter: navigationRouter)) coordinator.start() - push(coordinator: coordinator) coordinator.completion = { [weak self] event in guard let self else { return } self.delegate?.roomInfoCoordinator(self, viewEventInTimeline: event) } + push(coordinator: coordinator) default: guard let tabIndex = target.tabIndex else { fatalError("No settings tab index for this target.") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index b6d14bf08..434be83e4 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5257,25 +5257,9 @@ static CGSize kThreadListBarButtonItemImageSize; { // Dismiss potential keyboard. [self dismissKeyboard]; - - // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. - MXWeakify(self); - [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId - initialEventId:self.roomDataSource.room.accountData.readMarkerEventId - threadId:self.roomDataSource.threadId - andMatrixSession:self.mainSession - onComplete:^(id roomDataSource) { - MXStrongifyAndReturnIfNil(self); - - [roomDataSource finalizeInitialization]; - - // Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view. - self.centerBubblesTableViewContentOnTheInitialEventBottom = YES; - [self displayRoom:roomDataSource]; - - // Give the data source ownership to the room view controller. - self.hasRoomDataSourceOwnership = YES; - }]; + NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId; + NSString *threadId = self.roomDataSource.threadId; + [self reloadRoomWihtEventId:eventId threadId:threadId]; } else if (sender == self.resetReadMarkerButton) { @@ -7875,11 +7859,17 @@ static CGSize kThreadListBarButtonItemImageSize; viewEventInTimeline:(MXEvent *)event { [self.navigationController popToViewController:self animated:true]; + [self reloadRoomWihtEventId:event.eventId threadId:event.threadId]; +} + +-(void)reloadRoomWihtEventId:(NSString *)eventId + threadId:(NSString *)threadId +{ // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. MXWeakify(self); [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId - initialEventId:event.eventId - threadId:event.threadId + initialEventId:eventId + threadId:threadId andMatrixSession:self.mainSession onComplete:^(id roomDataSource) { MXStrongifyAndReturnIfNil(self); diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index c6c8a0601..fbda4f49a 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -521,7 +521,7 @@ extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate { } } func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) { - self.navigationRouter.popToModule(self.toPresentable(), animated: true) + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index d970a444b..12bf72e1a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -56,41 +56,44 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { } func showPollDetail(_ poll: TimelinePollDetails) { - guard let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), - let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: self.parameters.room)) else { + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: parameters.room)) else { pollHistoryViewModel.context.alertInfo = .init(id: true, title: VectorL10n.settingsDiscoveryErrorMessage) return } detailCoordinator.toPresentable().presentationController?.delegate = self - detailCoordinator.completion = { [weak self, weak detailCoordinator] result in - guard let self = self, let coordinator = detailCoordinator else { return } - switch result { - case .dismiss: - self.toPresentable().dismiss(animated: true) - self.remove(childCoordinator: coordinator) - case .viewInTimeline: - self.toPresentable().dismiss(animated: false) - self.remove(childCoordinator: coordinator) - var event = event - if poll.closed { - let room = self.parameters.room - let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) - let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) - event = pollEndedEvent ?? event - } - self.completion?(event) - } + detailCoordinator.completion = { [weak self, weak detailCoordinator, weak event] result in + guard let self, let coordinator = detailCoordinator, let event = event else { return } + self.handlePollDetailResult(result, coordinator: coordinator, event: event, poll: poll) } - self.add(childCoordinator: detailCoordinator) + add(childCoordinator: detailCoordinator) detailCoordinator.start() - self.toPresentable().present(detailCoordinator.toPresentable(), animated: true) + toPresentable().present(detailCoordinator.toPresentable(), animated: true) } func toPresentable() -> UIViewController { pollHistoryHostingController } + + private func handlePollDetailResult(_ result: PollHistoryDetailViewModelResult, coordinator: Coordinator, event: MXEvent, poll: TimelinePollDetails) { + switch result { + case .dismiss: + toPresentable().dismiss(animated: true) + remove(childCoordinator: coordinator) + case .viewInTimeline: + toPresentable().dismiss(animated: false) + remove(childCoordinator: coordinator) + var event = event + if poll.closed { + let room = parameters.room + let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference) + let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd }) + event = pollEndedEvent ?? event + } + completion?(event) + } + } } // MARK: UIAdaptivePresentationControllerDelegate diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index 113948a85..61b41a047 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import CommonKit -import SwiftUI import Combine +import CommonKit import MatrixSDK +import SwiftUI struct PollHistoryDetailCoordinatorParameters { let event: MXEvent @@ -28,8 +28,6 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { private let parameters: PollHistoryDetailCoordinatorParameters private let pollHistoryDetailHostingController: UIViewController private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol - private var indicatorPresenter: UserIndicatorTypePresenterProtocol - private var loadingIndicator: UserIndicator? // Must be used only internally var childCoordinators: [Coordinator] = [] @@ -42,12 +40,15 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { let viewModel = PollHistoryDetailViewModel(timelineViewModel: timelinePollCoordinator.viewModel) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel - pollHistoryDetailHostingController = VectorHostingController(rootView: view) - - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryDetailHostingController) - self.add(childCoordinator: timelinePollCoordinator) - viewModel.completion = { [weak self] result in + add(childCoordinator: timelinePollCoordinator) + } + + // MARK: - Public + + func start() { + MXLog.debug("[PollHistoryDetailCoordinator] did start.") + pollHistoryDetailViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .dismiss: @@ -58,29 +59,7 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { } } - // MARK: - Public - - func start() { - MXLog.debug("[PollHistoryDetailCoordinator] did start.") - - } - func toPresentable() -> UIViewController { pollHistoryDetailHostingController } - - // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - loadingIndicator = nil - } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 3e9709994..7043fd314 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -48,7 +48,6 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let viewModel = PollHistoryDetailViewModel(timelineViewModel: TimelinePollViewModel(timelinePollDetails: poll)) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 44ec2204c..788cb3a98 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -25,19 +25,17 @@ enum PollHistoryDetailViewModelResult { case viewInTimeline } -// MARK: View model - - - // MARK: View struct PollHistoryDetailViewState: BindableState { - var timelineViewModel: TimelinePollViewModelProtocol + var timelineViewModel: TimelinePollViewModelType.Context + var pollStartDate: Date { - timelineViewModel.context.viewState.poll.startDate + timelineViewModel.viewState.poll.startDate } + var isPollClosed: Bool { - timelineViewModel.context.viewState.poll.closed + timelineViewModel.viewState.poll.closed } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index 25068201a..ec6687994 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -20,17 +20,14 @@ import SwiftUI typealias PollHistoryDetailViewModelType = StateStoreViewModel class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDetailViewModelProtocol { - // MARK: - Properties - - // MARK: Private - // MARK: Public + var completion: PollHistoryDetailViewModelCallback? // MARK: - Setup init(timelineViewModel: TimelinePollViewModelProtocol) { - super.init(initialViewState: PollHistoryDetailViewState(timelineViewModel: timelineViewModel)) + super.init(initialViewState: PollHistoryDetailViewState(timelineViewModel: timelineViewModel.context)) } // MARK: - Public @@ -43,8 +40,4 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet completion?(.viewInTimeline) } } - - - // MARK: - TimelinePollViewModelProtocol - } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift index 4feadbfb0..0e4abb98a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModelProtocol.swift @@ -17,5 +17,6 @@ import Foundation protocol PollHistoryDetailViewModelProtocol { + var completion: PollHistoryDetailViewModelCallback? { get set } var context: PollHistoryDetailViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift index 4a9821a86..a4ab86104 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -23,11 +23,10 @@ class PollHistoryDetailViewModelTests: XCTestCase { static let counterInitialValue = 0 } - var viewModel: PollHistoryDetailViewModelProtocol! + var viewModel: PollHistoryDetailViewModel! var context: PollHistoryDetailViewModelType.Context! override func setUpWithError() throws { - let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] @@ -51,5 +50,18 @@ class PollHistoryDetailViewModelTests: XCTestCase { func testInitialState() { XCTAssertFalse(context.viewState.isPollClosed) } - + + func testProcessAction() { + viewModel.completion = { result in + XCTAssertEqual(result, .viewInTimeline) + } + viewModel.process(viewAction: .viewInTimeline) + } + + func testProcessDismiss() { + viewModel.completion = { result in + XCTAssertEqual(result, .dismiss) + } + viewModel.process(viewAction: .dismiss) + } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index 1df5270a9..5bf089222 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -17,8 +17,6 @@ import SwiftUI struct PollHistoryDetail: View { - // MARK: - Properties - // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI @@ -29,9 +27,6 @@ struct PollHistoryDetail: View { var body: some View { navigation - .padding([.horizontal], 16) - .padding([.top, .bottom]) - .background(theme.colors.background.ignoresSafeArea()) } private var navigation: some View { @@ -45,6 +40,7 @@ struct PollHistoryDetail: View { } } } + private var content: some View { let timelineViewModel = viewModel.viewState.timelineViewModel return ScrollView { @@ -54,17 +50,20 @@ struct PollHistoryDetail: View { .font(theme.fonts.caption1) .padding([.top]) .accessibilityIdentifier("PollHistoryDetail.date") - TimelinePollView(viewModel: timelineViewModel.context) + TimelinePollView(viewModel: timelineViewModel) .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) - .navigationBarItems(leading: btnBack, trailing: btnDone) + .navigationBarItems(leading: backButton, trailing: doneButton) viewInTimeline } } + .padding([.horizontal], 16) + .padding([.top, .bottom]) + .background(theme.colors.background.ignoresSafeArea()) } - private var btnBack: some View { + private var backButton: some View { Button(action: { viewModel.send(viewAction: .dismiss) }) { @@ -73,7 +72,8 @@ struct PollHistoryDetail: View { .foregroundColor(theme.colors.accent) } } - private var btnDone: some View { + + private var doneButton: some View { Button { viewModel.send(viewAction: .dismiss) } label: { From fae76cf002be0a413adcd848682d0f0ac6ffb146 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Tue, 31 Jan 2023 16:55:59 +0100 Subject: [PATCH 274/468] indentation --- Riot/Modules/Room/RoomViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 434be83e4..d4fdb6bd7 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7863,7 +7863,7 @@ static CGSize kThreadListBarButtonItemImageSize; } -(void)reloadRoomWihtEventId:(NSString *)eventId - threadId:(NSString *)threadId + threadId:(NSString *)threadId { // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. MXWeakify(self); From c2ace3e3507ec701b4b741cec4cf31645989c5fc Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 31 Jan 2023 16:29:17 +0000 Subject: [PATCH 275/468] Fix develop --- Riot/Categories/MXRoom+Riot.m | 2 +- .../Controllers/MXKNotificationSettingsViewController.m | 2 +- .../Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index df47c1674..04538ba80 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -637,7 +637,7 @@ }]; } - [notificationCenter enableRule:rule isEnabled:YES]; + [notificationCenter enableRule:rule isEnabled:YES completion:nil]; } - (void)setNotificationCenterDidFailObserver:(id)anObserver diff --git a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m index 1524eb2d7..7007c12bf 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKNotificationSettingsViewController.m @@ -193,7 +193,7 @@ MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID]; if (pushRule) { - [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled]; + [_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled completion:nil]; } } } diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m index a65174564..79632d87c 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m @@ -163,7 +163,7 @@ if (sender == _controlButton) { // Swap enable state - [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled]; + [_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled completion:nil]; } else if (sender == _deleteButton) { From 47c6a34bb431bbf2880e08d70b54569d2965361a Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 1 Feb 2023 10:14:40 +0100 Subject: [PATCH 276/468] Async-await refactor --- .../MXNotificationSettingsService.swift | 70 +++++----- .../MockNotificationSettingsService.swift | 8 +- .../NotificationSettingsServiceType.swift | 13 +- .../NotificationSettingsViewModelTests.swift | 122 ++++++------------ .../View/NotificationSettings.swift | 4 +- .../NotificationSettingsViewModel.swift | 82 ++++++------ 6 files changed, 127 insertions(+), 172 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index f5e4c457a..6ccd54c1a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -44,7 +44,9 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { // Observe future updates to content rules rulesUpdated - .compactMap { _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] } + .compactMap { [weak self] _ in + self?.session.notificationCenter.rules.global.content as? [MXPushRule] + } .assign(to: &$contentRules) // Set initial value of rules @@ -53,7 +55,9 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { } // Observe future updates to rules rulesUpdated - .compactMap { _ in self.session.notificationCenter.flatRules as? [MXPushRule] } + .compactMap { [weak self] _ in + self?.session.notificationCenter.flatRules as? [MXPushRule] + } .assign(to: &$rules) } @@ -72,52 +76,50 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { func updatePushRuleActions(for ruleId: String, enabled: Bool, - actions: NotificationActions?, - completion: ((Result) -> Void)?) { + actions: NotificationActions?) async throws { guard let rule = session.notificationCenter.rule(byId: ruleId) else { - completion?(.success) return } guard let actions = actions else { - enableRule(rule: rule, enabled: enabled, completion: completion) + try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled) return } // Updating the actions before enabling the rule allows the homeserver to triggers just one sync update - session.notificationCenter.updatePushRuleActions(ruleId, + try await session.notificationCenter.updatePushRuleActions(ruleId, kind: rule.kind, notify: actions.notify, soundName: actions.sound, - highlight: actions.highlight) { [weak self] error in - switch error.result { - case .success: - self?.enableRule(rule: rule, enabled: enabled, completion: completion) - case .failure: - completion?(error.result) + highlight: actions.highlight) + + try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled) + } +} + +private extension MXNotificationCenter { + func enableRule(pushRule: MXPushRule, isEnabled: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + enableRule(pushRule, isEnabled: isEnabled) { error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume() + } + } + } + } + + func updatePushRuleActions(ruleId: String, kind: __MXPushRuleKind, notify: Bool, soundName: String, highlight: Bool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + updatePushRuleActions(ruleId, kind: kind, notify: notify, soundName: soundName, highlight: highlight) { error in + if let error = error { + continuation.resume(with: .failure(error)) + } else { + continuation.resume() + } } } } } - -private extension MXNotificationSettingsService { - func enableRule(rule: MXPushRule, enabled: Bool, completion: ((Result) -> Void)?) { - session.notificationCenter.enableRule(rule, isEnabled: enabled) { error in - completion?(error.result) - } - } -} - -private extension Result where Success == Void { - static var success: Self { - .success(()) - } -} - -private extension Optional where Wrapped == Error { - var result: Result { - map { .failure($0) } ?? .success - } -} - diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index 7d74f5288..ea4bd640c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -44,15 +44,11 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) { + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws { guard let ruleIndex = rules.firstIndex(where: { $0.ruleId == ruleId }) else { - completion?(.success(())) return } - rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, - enabled: enabled, - actions: actions) - completion?(.success(())) + rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, enabled: enabled, actions: actions) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index 9e6419d23..5b06dfb6d 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -40,16 +40,5 @@ protocol NotificationSettingsServiceType { /// - ruleId: The id of the rule. /// - enabled: Whether the rule should be enabled or disabled. /// - actions: The actions to update with. - /// - completion: The completion of the operation. - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?, completion: ((Result) -> Void)?) -} - -extension NotificationSettingsServiceType { - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws { - try await withCheckedThrowingContinuation { continuation in - updatePushRuleActions(for: ruleId, enabled: enabled, actions: actions) { result in - continuation.resume(with: result) - } - } - } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) async throws } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index ea698d9e0..a24e28733 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -32,113 +32,71 @@ final class NotificationSettingsViewModelTests: XCTestCase { XCTAssertTrue(viewModel.viewState.selectionState.values.allSatisfy { $0 }) } - func testUpdateRule() throws { + func testUpdateRule() async { viewModel = .init(notificationSettingsService: notificationService, ruleIds: .default) notificationService.rules = [MockNotificationPushRule].default - viewModel.update(ruleID: .encrypted, isChecked: false) + await viewModel.update(ruleID: .encrypted, isChecked: false) XCTAssertEqual(viewModel.viewState.selectionState.count, 4) XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false) } - func testUpdateOneToOneRuleAlsoUpdatesPollRules() { - let expectation = expectation(description: #function) + func testUpdateOneToOneRuleAlsoUpdatesPollRules() async { setupWithPollRules() - viewModel.update(ruleID: .oneToOneRoom, isChecked: false) { result in - guard case .success = result else { - XCTFail() - return - } - - XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], false) - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollStart], false) - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], true) - XCTAssertEqual(self.viewModel.viewState.selectionState[.pollStart], true) - XCTAssertEqual(self.viewModel.viewState.selectionState[.pollEnd], true) - - expectation.fulfill() - } + await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], false) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], false) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], false) - waitForExpectations(timeout: 1.0) + // unrelated poll rules stay the same + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) + XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], true) + XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], true) } - func testUpdateMessageRuleAlsoUpdatesPollRules() { - let expectation = expectation(description: #function) + func testUpdateMessageRuleAlsoUpdatesPollRules() async { setupWithPollRules() - viewModel.update(ruleID: .allOtherMessages, isChecked: false) { result in - guard case .success = result else { - XCTFail() - return - } - - XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], false) - XCTAssertEqual(self.viewModel.viewState.selectionState[.pollStart], false) - XCTAssertEqual(self.viewModel.viewState.selectionState[.pollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], true) - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollStart], true) - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOnePollEnd], true) - - expectation.fulfill() - } + await viewModel.update(ruleID: .allOtherMessages, isChecked: false) + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], false) + XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], false) + XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], false) - waitForExpectations(timeout: 1.0) + // unrelated poll rules stay the same + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], true) + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], true) } - func testMismatchingRulesAreHandled() { - let expectation = expectation(description: #function) + func testMismatchingRulesAreHandled() async { setupWithPollRules() - viewModel.update(ruleID: .allOtherMessages, isChecked: false) { result in - guard case .success = result else { - XCTFail() - return - } - - // simulating a "mismatch" on the poll started rule - self.viewModel.update(ruleID: .pollStart, isChecked: true) - - XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) - - // The other messages rule ui flag should match the loudest related poll rule - XCTAssertEqual(self.viewModel.viewState.selectionState[.allOtherMessages], true) - - expectation.fulfill() - } + await viewModel.update(ruleID: .allOtherMessages, isChecked: false) - waitForExpectations(timeout: 1.0) + // simulating a "mismatch" on the poll started rule + await viewModel.update(ruleID: .pollStart, isChecked: true) + + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + + // The other messages rule ui flag should match the loudest related poll rule + XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) } - func testMismatchingOneToOneRulesAreHandled() { - let expectation = expectation(description: #function) + func testMismatchingOneToOneRulesAreHandled() async { setupWithPollRules() - viewModel.update(ruleID: .oneToOneRoom, isChecked: false) { result in - guard case .success = result else { - XCTFail() - return - } - - // simulating a "mismatch" on the one to one poll started rule - self.viewModel.update(ruleID: .oneToOnePollStart, isChecked: true) - - XCTAssertEqual(self.viewModel.viewState.selectionState.count, 8) - - // The one to one room rule ui flag should match the loudest related poll rule - XCTAssertEqual(self.viewModel.viewState.selectionState[.oneToOneRoom], true) - - expectation.fulfill() - } + await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) + // simulating a "mismatch" on the one to one poll started rule + await viewModel.update(ruleID: .oneToOnePollStart, isChecked: true) - waitForExpectations(timeout: 1.0) + XCTAssertEqual(viewModel.viewState.selectionState.count, 8) + + // The one to one room rule ui flag should match the loudest related poll rule + XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 407be6828..4d1d3ee04 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -33,7 +33,9 @@ struct NotificationSettings: View { ForEach(viewModel.viewState.ruleIds) { ruleId in let checked = viewModel.viewState.selectionState[ruleId] ?? false FormPickerItem(title: ruleId.title, selected: checked) { - viewModel.update(ruleID: ruleId, isChecked: !checked) + Task { + await viewModel.update(ruleID: ruleId, isChecked: !checked) + } } } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 208b8d2bb..8d60ee163 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -49,7 +49,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // Observe when the rules are updated, to subsequently update the state of the settings. notificationSettingsService.rulesPublisher - .sink(receiveValue: rulesUpdated(newRules:)) + .sink { [weak self] newRules in + self?.rulesUpdated(newRules: newRules) + } .store(in: &cancellables) // Only observe keywords if the current settings view displays it. @@ -88,7 +90,9 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // Keyword rules were updates, check if we need to update the setting. keywordsRules .map { $0.contains { $0.enabled } } - .sink(receiveValue: keywordRuleUpdated(anyEnabled:)) + .sink { [weak self] in + self?.keywordRuleUpdated(anyEnabled: $0) + } .store(in: &cancellables) // Update the viewState with the final keywords to be displayed. @@ -105,30 +109,29 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // MARK: - Public - func update(ruleID: NotificationPushRuleId, isChecked: Bool, completion: ((Result) -> Void)? = nil) { + @MainActor + func update(ruleID: NotificationPushRuleId, isChecked: Bool) async { let index = NotificationIndex.index(when: isChecked) let standardActions = ruleID.standardActions(for: index) let enabled = standardActions != .disabled switch ruleID { case .keywords: // Keywords is handled differently to other settings - updateKeywords(isChecked: isChecked) + await updateKeywords(isChecked: isChecked) case .oneToOneRoom, .allOtherMessages: - updatePushAction( + await updatePushAction( id: ruleID, enabled: enabled, standardActions: standardActions, - then: ruleID.syncedRules, - completion: completion + then: ruleID.syncedRules ) default: - notificationSettingsService.updatePushRuleActions( + try? await notificationSettingsService.updatePushRuleActions( for: ruleID.rawValue, enabled: enabled, - actions: standardActions.actions, - completion: completion + actions: standardActions.actions ) } } @@ -149,58 +152,63 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob // MARK: - Private private extension NotificationSettingsViewModel { - func updateKeywords(isChecked: Bool) { + @MainActor + func updateKeywords(isChecked: Bool) async { guard !keywordsOrdered.isEmpty else { viewState.selectionState[.keywords]?.toggle() return } + // Get the static definition and update the actions and enabled state for every keyword. let index = NotificationIndex.index(when: isChecked) let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) let enabled = standardActions != .disabled + let keywordsToUpdate = keywordsOrdered - keywordsOrdered.forEach { keyword in - notificationSettingsService.updatePushRuleActions( - for: keyword, - enabled: enabled, - actions: standardActions.actions, - completion: nil - ) + await withThrowingTaskGroup(of: Void.self) { group in + for keyword in keywordsToUpdate { + group.addTask { + try await self.notificationSettingsService.updatePushRuleActions( + for: keyword, + enabled: enabled, + actions: standardActions.actions + ) + } + } } } func updatePushAction(id: NotificationPushRuleId, enabled: Bool, standardActions: NotificationStandardActions, - then rules: [NotificationPushRuleId], - completion: ((Result) -> Void)?) { - viewState.saving = true + then rules: [NotificationPushRuleId]) async { + await MainActor.run { + viewState.saving = true + } - Task { - do { - try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions) - - try await withThrowingTaskGroup(of: Void.self) { group in - for ruleId in rules { - group.addTask { - try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions) - } + do { + // update the 'parent rule' first + try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions) + + // synchronize all the 'children rules' with the parent rule + try await withThrowingTaskGroup(of: Void.self) { group in + for ruleId in rules { + group.addTask { + try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions) } - - try await group.waitForAll() - await completeUpdate(completion: completion, result: .success(())) } - } catch { - await completeUpdate(completion: completion, result: .failure(error)) + try await group.waitForAll() } + await completeUpdate() + } catch { + await completeUpdate() } } @MainActor - func completeUpdate(completion: ((Result) -> Void)?, result: Result) { + func completeUpdate() { #warning("Handle error here in the next ticket") viewState.saving = false - completion?(result) } func rulesUpdated(newRules: [NotificationPushRuleType]) { From 5b7bfc5642b1377fb9c84eb8895f583e487ec989 Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Wed, 1 Feb 2023 11:15:22 +0100 Subject: [PATCH 277/468] fix embedding pattern --- .../PollHistory/Coordinator/PollHistoryCoordinator.swift | 2 +- .../Coordinator/PollHistoryDetailCoordinator.swift | 3 ++- .../MockPollHistoryDetailScreenState.swift | 3 ++- .../PollHistoryDetail/PollHistoryDetailModels.swift | 9 +++++---- .../PollHistoryDetail/PollHistoryDetailViewModel.swift | 4 ++-- .../PollHistoryDetail/View/PollHistoryDetail.swift | 5 ++--- .../Coordinator/TimelinePollCoordinator.swift | 4 ++++ 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift index 12bf72e1a..faa293abf 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift @@ -57,7 +57,7 @@ final class PollHistoryCoordinator: NSObject, Coordinator, Presentable { func showPollDetail(_ poll: TimelinePollDetails) { guard let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId), - let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, room: parameters.room)) else { + let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, poll: poll, room: parameters.room)) else { pollHistoryViewModel.context.alertInfo = .init(id: true, title: VectorL10n.settingsDiscoveryErrorMessage) return } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index 61b41a047..c27d3151c 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -21,6 +21,7 @@ import SwiftUI struct PollHistoryDetailCoordinatorParameters { let event: MXEvent + let poll: TimelinePollDetails let room: MXRoom } @@ -37,7 +38,7 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { self.parameters = parameters let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.room.mxSession, room: parameters.room, pollEvent: parameters.event)) - let viewModel = PollHistoryDetailViewModel(timelineViewModel: timelinePollCoordinator.viewModel) + let viewModel = PollHistoryDetailViewModel(timelinePollView: timelinePollCoordinator.toView(), poll: parameters.poll) let view = PollHistoryDetail(viewModel: viewModel.context) pollHistoryDetailViewModel = viewModel pollHistoryDetailHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 7043fd314..cdced783b 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -48,7 +48,8 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let viewModel = PollHistoryDetailViewModel(timelineViewModel: TimelinePollViewModel(timelinePollDetails: poll)) + let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) + let viewModel = PollHistoryDetailViewModel(timelinePollView: TimelinePollView(viewModel: timelineViewModel.context), poll: poll) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 788cb3a98..7f77be536 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -15,6 +15,7 @@ // import Foundation +import SwiftUI // MARK: - Coordinator @@ -28,14 +29,14 @@ enum PollHistoryDetailViewModelResult { // MARK: View struct PollHistoryDetailViewState: BindableState { - var timelineViewModel: TimelinePollViewModelType.Context - + var timelinePollView: any View + var poll: TimelinePollDetails var pollStartDate: Date { - timelineViewModel.viewState.poll.startDate + poll.startDate } var isPollClosed: Bool { - timelineViewModel.viewState.poll.closed + poll.closed } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index ec6687994..505b7f3e6 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -26,8 +26,8 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet // MARK: - Setup - init(timelineViewModel: TimelinePollViewModelProtocol) { - super.init(initialViewState: PollHistoryDetailViewState(timelineViewModel: timelineViewModel.context)) + init(timelinePollView: any View, poll: TimelinePollDetails) { + super.init(initialViewState: PollHistoryDetailViewState(timelinePollView: timelinePollView, poll: poll)) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index 5bf089222..203811236 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -42,15 +42,14 @@ struct PollHistoryDetail: View { } private var content: some View { - let timelineViewModel = viewModel.viewState.timelineViewModel - return ScrollView { + ScrollView { VStack(alignment: .leading) { Text(DateFormatter.pollShortDateFormatter.string(from: viewModel.viewState.pollStartDate)) .foregroundColor(theme.colors.tertiaryContent) .font(theme.fonts.caption1) .padding([.top]) .accessibilityIdentifier("PollHistoryDetail.date") - TimelinePollView(viewModel: timelineViewModel) + AnyView(viewModel.viewState.timelinePollView) .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 77b0e29ba..3214fae65 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -86,6 +86,10 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func toPresentable() -> UIViewController { VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context)) } + + func toView() -> any View { + TimelinePollView(viewModel: viewModel.context) + } func canEndPoll() -> Bool { pollAggregator.poll.isClosed == false From 4637562c090e70806ac30660200deb07dc7b7f6b Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 1 Feb 2023 11:28:48 +0100 Subject: [PATCH 278/468] Cleanup --- .../ViewModel/NotificationSettingsViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 8d60ee163..7a00940af 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -191,13 +191,12 @@ private extension NotificationSettingsViewModel { try await notificationSettingsService.updatePushRuleActions(for: id.rawValue, enabled: enabled, actions: standardActions.actions) // synchronize all the 'children rules' with the parent rule - try await withThrowingTaskGroup(of: Void.self) { group in + await withThrowingTaskGroup(of: Void.self) { group in for ruleId in rules { group.addTask { try await self.notificationSettingsService.updatePushRuleActions(for: ruleId.rawValue, enabled: enabled, actions: standardActions.actions) } } - try await group.waitForAll() } await completeUpdate() } catch { From 3f5beece27ba9a5ca9383e95a7300631116be045 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 31 Jan 2023 12:24:55 +0100 Subject: [PATCH 279/468] Add error handling for push sync --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ .../View/NotificationSettings.swift | 17 ++++++++--- .../NotificationSettingsViewModel.swift | 30 +++++++++++++++++-- .../NotificationSettingsViewState.swift | 1 + 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1e584740b..73d395faf 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -759,6 +759,7 @@ Tap the + to start adding people."; "settings_your_keywords" = "Your Keywords"; "settings_new_keyword" = "Add new Keyword"; "settings_mentions_and_keywords_encryption_notice" = "You won’t get notifications for mentions & keywords in encrypted rooms on mobile."; +"settings_push_rules_error" = "An error occurred when updating your notification preferences. Please try to toggle your option again."; "settings_enable_callkit" = "Integrated calling"; "settings_callkit_info" = "Receive incoming calls on your lock screen. See your %@ calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d722d24f9..fd8eee3b6 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7767,6 +7767,10 @@ public class VectorL10n: NSObject { public static var settingsProfilePicture: String { return VectorL10n.tr("Vector", "settings_profile_picture") } + /// An error occurred when updating your notification preferences. Please try to toggle your option again. + public static var settingsPushRulesError: String { + return VectorL10n.tr("Vector", "settings_push_rules_error") + } /// Are you sure you want to remove the email address %@? public static func settingsRemoveEmailPromptMsg(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_remove_email_prompt_msg", p1) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 4d1d3ee04..4a8c7df2f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -21,6 +21,7 @@ import SwiftUI /// Also renders an optional bottom section. /// Used in the case of keywords, for the keyword chips and input. struct NotificationSettings: View { + @Environment(\.theme) var theme: ThemeSwiftUI @ObservedObject var viewModel: NotificationSettingsViewModel var bottomSection: BottomSection? @@ -31,10 +32,18 @@ struct NotificationSettings: View { header: FormSectionHeader(text: VectorL10n.settingsNotifyMeFor) ) { ForEach(viewModel.viewState.ruleIds) { ruleId in - let checked = viewModel.viewState.selectionState[ruleId] ?? false - FormPickerItem(title: ruleId.title, selected: checked) { - Task { - await viewModel.update(ruleID: ruleId, isChecked: !checked) + VStack(alignment: .leading, spacing: 4) { + let checked = viewModel.viewState.selectionState[ruleId] ?? false + FormPickerItem(title: ruleId.title, selected: checked) { + viewModel.update(ruleID: ruleId, isChecked: !checked) + } + + if viewModel.isRuleOutOfSync(ruleId) { + Text(VectorL10n.settingsPushRulesError) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + .padding(.horizontal) + .padding(.bottom, 16) } } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 7a00940af..f782901b1 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -147,6 +147,10 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob keywordsOrdered = keywordsOrdered.filter { $0 != keyword } notificationSettingsService.remove(keyword: keyword) } + + func isRuleOutOfSync(_ ruleId: NotificationPushRuleId) -> Bool { + viewState.outOfSyncRules.contains(ruleId) && viewState.saving == false + } } // MARK: - Private @@ -206,11 +210,12 @@ private extension NotificationSettingsViewModel { @MainActor func completeUpdate() { - #warning("Handle error here in the next ticket") viewState.saving = false } func rulesUpdated(newRules: [NotificationPushRuleType]) { + var outOfSyncRules: Set = .init() + for rule in newRules { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), @@ -219,8 +224,15 @@ private extension NotificationSettingsViewModel { continue } - viewState.selectionState[ruleId] = isChecked(rule: rule, syncedRules: ruleId.syncedRules(in: newRules)) + let relatedSyncedRules = ruleId.syncedRules(in: newRules) + viewState.selectionState[ruleId] = isChecked(rule: rule, syncedRules: relatedSyncedRules) + + if isOutOfSync(rule: rule, syncedRules: relatedSyncedRules) { + outOfSyncRules.insert(ruleId) + } } + + viewState.outOfSyncRules = outOfSyncRules } func keywordRuleUpdated(anyEnabled: Bool) { @@ -266,6 +278,20 @@ private extension NotificationSettingsViewModel { return defaultIsChecked(rule: rule) } } + + func isOutOfSync(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + return false + } + + switch ruleId { + case .oneToOneRoom, .allOtherMessages: + let ruleIsChecked = defaultIsChecked(rule: rule) + return syncedRules.contains(where: { defaultIsChecked(rule: $0) != ruleIsChecked }) + default: + return false + } + } } private extension NotificationPushRuleId { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift index 22bb4fed8..a732f56b1 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift @@ -22,5 +22,6 @@ struct NotificationSettingsViewState { var saving: Bool var ruleIds: [NotificationPushRuleId] var selectionState: [NotificationPushRuleId: Bool] + var outOfSyncRules: Set = .init() var keywords = [String]() } From 29391c89d55ad31a323cdce98ce8eb01582b87ad Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 31 Jan 2023 15:25:45 +0100 Subject: [PATCH 280/468] Improve UT --- .../Test/Unit/NotificationSettingsViewModelTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index a24e28733..1593b47ac 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -97,6 +97,10 @@ final class NotificationSettingsViewModelTests: XCTestCase { // The one to one room rule ui flag should match the loudest related poll rule XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) + + // the oneToOneRoom rule should be flagged as "out of sync" + XCTAssertTrue(self.viewModel.isRuleOutOfSync(.oneToOneRoom)) + XCTAssertFalse(self.viewModel.isRuleOutOfSync(.allOtherMessages)) } } From 8e5cffef8abe82bf3cfddd5ade45054316db0349 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Tue, 31 Jan 2023 15:31:06 +0100 Subject: [PATCH 281/468] Add changelog.d file --- changelog.d/pr-7324.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7324.change diff --git a/changelog.d/pr-7324.change b/changelog.d/pr-7324.change new file mode 100644 index 000000000..1d7133eb8 --- /dev/null +++ b/changelog.d/pr-7324.change @@ -0,0 +1 @@ +Polls: add error handling when syncing push rules with the ones of normal messages. From 5dc108e4ae4b23f5b5974d6aac9c44a4d47e48e1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 1 Feb 2023 11:41:59 +0100 Subject: [PATCH 282/468] Fix rebase issues --- .../Test/Unit/NotificationSettingsViewModelTests.swift | 4 ++-- .../Settings/Notifications/View/NotificationSettings.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index 1593b47ac..95b5e08fa 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -99,8 +99,8 @@ final class NotificationSettingsViewModelTests: XCTestCase { XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) // the oneToOneRoom rule should be flagged as "out of sync" - XCTAssertTrue(self.viewModel.isRuleOutOfSync(.oneToOneRoom)) - XCTAssertFalse(self.viewModel.isRuleOutOfSync(.allOtherMessages)) + XCTAssertTrue(viewModel.isRuleOutOfSync(.oneToOneRoom)) + XCTAssertFalse(viewModel.isRuleOutOfSync(.allOtherMessages)) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 4a8c7df2f..62cdf247a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -35,7 +35,9 @@ struct NotificationSettings: View { VStack(alignment: .leading, spacing: 4) { let checked = viewModel.viewState.selectionState[ruleId] ?? false FormPickerItem(title: ruleId.title, selected: checked) { - viewModel.update(ruleID: ruleId, isChecked: !checked) + Task { + await viewModel.update(ruleID: ruleId, isChecked: !checked) + } } if viewModel.isRuleOutOfSync(ruleId) { From 14c96a78b0ee10d7b4d22bb6bb44511829024dbc Mon Sep 17 00:00:00 2001 From: Flavio Alescio Date: Wed, 1 Feb 2023 12:41:05 +0100 Subject: [PATCH 283/468] removed view from viewModel --- .../Coordinator/PollHistoryDetailCoordinator.swift | 4 ++-- .../PollHistoryDetail/MockPollHistoryDetailScreenState.swift | 4 ++-- .../PollHistoryDetail/PollHistoryDetailModels.swift | 1 - .../PollHistoryDetail/PollHistoryDetailViewModel.swift | 4 ++-- .../Test/Unit/PollHistoryDetailViewModelTests.swift | 2 +- .../PollHistoryDetail/View/PollHistoryDetail.swift | 3 ++- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift index c27d3151c..4b166b3aa 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Coordinator/PollHistoryDetailCoordinator.swift @@ -38,8 +38,8 @@ final class PollHistoryDetailCoordinator: Coordinator, Presentable { self.parameters = parameters let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.room.mxSession, room: parameters.room, pollEvent: parameters.event)) - let viewModel = PollHistoryDetailViewModel(timelinePollView: timelinePollCoordinator.toView(), poll: parameters.poll) - let view = PollHistoryDetail(viewModel: viewModel.context) + let viewModel = PollHistoryDetailViewModel(poll: parameters.poll) + let view = PollHistoryDetail(viewModel: viewModel.context, contentPoll: timelinePollCoordinator.toView()) pollHistoryDetailViewModel = viewModel pollHistoryDetailHostingController = VectorHostingController(rootView: view) add(childCoordinator: timelinePollCoordinator) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index cdced783b..09a8fb3c7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -49,8 +49,8 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) - let viewModel = PollHistoryDetailViewModel(timelinePollView: TimelinePollView(viewModel: timelineViewModel.context), poll: poll) + let viewModel = PollHistoryDetailViewModel(poll: poll) - return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context))) + return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context)))) } } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift index 7f77be536..7d4fd6365 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailModels.swift @@ -29,7 +29,6 @@ enum PollHistoryDetailViewModelResult { // MARK: View struct PollHistoryDetailViewState: BindableState { - var timelinePollView: any View var poll: TimelinePollDetails var pollStartDate: Date { poll.startDate diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift index 505b7f3e6..58a18441a 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/PollHistoryDetailViewModel.swift @@ -26,8 +26,8 @@ class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDet // MARK: - Setup - init(timelinePollView: any View, poll: TimelinePollDetails) { - super.init(initialViewState: PollHistoryDetailViewState(timelinePollView: timelinePollView, poll: poll)) + init(poll: TimelinePollDetails) { + super.init(initialViewState: PollHistoryDetailViewState(poll: poll)) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift index a4ab86104..a501cbeb0 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/Test/Unit/PollHistoryDetailViewModelTests.swift @@ -43,7 +43,7 @@ class PollHistoryDetailViewModelTests: XCTestCase { hasBeenEdited: false, hasDecryptionError: false) - viewModel = PollHistoryDetailViewModel(timelineViewModel: TimelinePollViewModel(timelinePollDetails: timelinePoll)) + viewModel = PollHistoryDetailViewModel(poll: timelinePoll) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift index 203811236..54b327936 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/View/PollHistoryDetail.swift @@ -24,6 +24,7 @@ struct PollHistoryDetail: View { // MARK: Public @ObservedObject var viewModel: PollHistoryDetailViewModel.Context + var contentPoll: any View var body: some View { navigation @@ -49,7 +50,7 @@ struct PollHistoryDetail: View { .font(theme.fonts.caption1) .padding([.top]) .accessibilityIdentifier("PollHistoryDetail.date") - AnyView(viewModel.viewState.timelinePollView) + AnyView(contentPoll) .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) From 8254ca8b99bded88cb20a729dd75c67ab8e9a062 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 1 Feb 2023 11:49:16 +0000 Subject: [PATCH 284/468] Fix crypto v2 config --- Config/AppConfiguration.swift | 3 --- Config/CommonConfiguration.swift | 10 ++++++++++ Config/CryptoSDKConfiguration.swift | 11 ----------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index fe83fba1f..70b1d78d5 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -24,9 +24,6 @@ class AppConfiguration: CommonConfiguration { override func setupSettings() { super.setupSettings() setupAppSettings() -#if DEBUG - CryptoSDKConfiguration.shared.setup() -#endif } private func setupAppSettings() { diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index fee3796ff..b98195e6d 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,6 +91,16 @@ class CommonConfiguration: NSObject, Configurable { MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature + + #if DEBUG + if sdkOptions.isCryptoSDKAvailable { + let isEnabled = RiotSettings.shared.enableCryptoSDK + MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + sdkOptions.enableCryptoSDK = isEnabled + } else { + MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") + } + #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift index 6edde7871..935988ba9 100644 --- a/Config/CryptoSDKConfiguration.swift +++ b/Config/CryptoSDKConfiguration.swift @@ -22,17 +22,6 @@ import Foundation @objcMembers class CryptoSDKConfiguration: NSObject { static let shared = CryptoSDKConfiguration() - func setup() { - guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { - return - } - - let isEnabled = RiotSettings.shared.enableCryptoSDK - MXSDKOptions.sharedInstance().enableCryptoSDK = isEnabled - - MXLog.debug("[CryptoSDKConfiguration] setup: Crypto SDK is \(isEnabled ? "enabled" : "disabled")") - } - func enable() { guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { return From bea7421c5ec3f777419fc53dd34d50aecff665a4 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 1 Feb 2023 14:43:38 +0100 Subject: [PATCH 285/468] Cleanup --- .../MatrixSDK/VoiceBroadcastPlaybackViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index af229c35e..35933ffce 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -579,7 +579,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) { state.playbackState = .error - self.updateErrorState() + updateErrorState() } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { From b2d77e3455ad6185b919a17302f2833cb60082ff Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 1 Feb 2023 19:11:00 +0100 Subject: [PATCH 286/468] Start PushRulesUpdater --- .../PushRulesUpdater/PushRulesUpdater.swift | 70 +++++++++++++++++++ Riot/Modules/Application/AppCoordinator.swift | 27 ++++++- 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift new file mode 100644 index 000000000..220b16100 --- /dev/null +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -0,0 +1,70 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +final class PushRulesUpdater { + private var cancellables: Set = .init() + private var rules: [MXPushRule] = [] + + init(checkSignal: AnyPublisher, rulesProvider: AnyPublisher<[MXPushRule], Never>) { + rulesProvider + .weakAssign(to: \.rules, on: self) + .store(in: &cancellables) + + checkSignal + .sink { [weak self] _ in + self?.updateRulesIfNeeded() + } + .store(in: &cancellables) + } +} + +private extension PushRulesUpdater { + func updateRulesIfNeeded() { + for rule in rules { + syncRelatedRulesIfNeeded(for: rule) + } + } + + func syncRelatedRulesIfNeeded(for rule: MXPushRule) { + let relatedRules = rule.syncedRules(in: rules) + + for relatedRule in relatedRules { + guard relatedRule == rule else { + MXLog.debug("*** mismatch not found. rule: \(relatedRule.ruleId)") + continue + } + + MXLog.debug("*** mismatch found. rule: \(relatedRule.ruleId)") + } + } +} + +private extension MXPushRule { + func syncedRules(in rules: [MXPushRule]) -> [MXPushRule] { + guard let ruleId = NotificationPushRuleId(rawValue: ruleId) else { + return [] + } + + return rules.filter { + guard let someRuleId = NotificationPushRuleId(rawValue: $0.ruleId) else { + return false + } + return ruleId.syncedRules.contains(someRuleId) + } + } +} diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index c356d0b86..0b44632e5 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -53,6 +53,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { fileprivate weak var sideMenuCoordinator: SideMenuCoordinatorType? private let userSessionsService: UserSessionsService + private var pushRulesUpdater: PushRulesUpdater? /// Main user Matrix session private var mainMatrixSession: MXSession? { @@ -81,9 +82,10 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // MARK: - Public methods func start() { - self.setupLogger() - self.setupTheme() - self.excludeAllItemsFromBackup() + setupLogger() + setupTheme() + excludeAllItemsFromBackup() + setupPushRuleSync() // Setup navigation router store _ = NavigationRouterStore.shared @@ -259,6 +261,25 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Reload split view with selected space id self.splitViewCoordinator?.start(with: spaceId) } + + private func setupPushRuleSync() { + let needsRulesCheck = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) + .map { _ in () } + .eraseToAnyPublisher() + + #warning("Doesn't include the initial state of rules") + let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) + .compactMap { notification -> [MXPushRule]? in + guard let center = notification.object as? MXNotificationCenter else { + return nil + } + + return center.flatRules as? [MXPushRule] + } + .eraseToAnyPublisher() + + pushRulesUpdater = .init(checkSignal: needsRulesCheck, rulesProvider: rulesUpdated) + } } // MARK: - LegacyAppDelegateDelegate From 4400b7522517984b2fc6f15aa9ba69e556d82a84 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 09:58:29 +0100 Subject: [PATCH 287/468] Continue PushRulesUpdater logics --- Riot/Categories/Publisher+Riot.swift | 6 ++++ .../PushRulesUpdater/PushRulesUpdater.swift | 30 +++++++++++++++++-- Riot/Modules/Application/AppCoordinator.swift | 21 ++++++++----- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Riot/Categories/Publisher+Riot.swift b/Riot/Categories/Publisher+Riot.swift index 98bb522b3..7c70404c6 100644 --- a/Riot/Categories/Publisher+Riot.swift +++ b/Riot/Categories/Publisher+Riot.swift @@ -33,4 +33,10 @@ extension Publisher { Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler) } } + + func eraseOutput() -> AnyPublisher { + self + .map { _ in () } + .eraseToAnyPublisher() + } } diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 220b16100..8b45d3035 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -44,12 +44,12 @@ private extension PushRulesUpdater { let relatedRules = rule.syncedRules(in: rules) for relatedRule in relatedRules { - guard relatedRule == rule else { - MXLog.debug("*** mismatch not found. rule: \(relatedRule.ruleId)") + guard MXPushRule.haveSameContent(relatedRule, rule) == false else { + MXLog.debug("*** mismatch -> rule: \(relatedRule.ruleId)") continue } - MXLog.debug("*** mismatch found. rule: \(relatedRule.ruleId)") + MXLog.debug("*** OK -> rule: \(relatedRule.ruleId)") } } } @@ -67,4 +67,28 @@ private extension MXPushRule { return ruleId.syncedRules.contains(someRuleId) } } + + static func haveSameContent(_ firstRule: MXPushRule, _ secondRule: MXPushRule) -> Bool { + guard + firstRule.enabled == secondRule.enabled, + let firstActions = firstRule.mxActions, + let secondActions = secondRule.mxActions, + firstActions.count == secondActions.count + else { + return false + } + + return firstActions.indices.allSatisfy { index in + let action1 = firstActions[index] + let action2 = secondActions[index] + #warning("compare @property (nonatomic) NSDictionary *parameters") + return action1.actionType == action2.actionType + } + } +} + +private extension MXPushRule { + var mxActions: [MXPushRuleAction]? { + actions as? [MXPushRuleAction] + } } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 0b44632e5..389467ded 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -14,6 +14,7 @@ limitations under the License. */ +import Combine import Foundation import Intents import MatrixSDK @@ -263,14 +264,15 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } private func setupPushRuleSync() { - let needsRulesCheck = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) - .map { _ in () } - .eraseToAnyPublisher() + let firstSyncEndend = NotificationCenter.default.publisher(for: .mxSessionDidSync) + .first() + .eraseOutput() + + let rulesDidChange = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() - #warning("Doesn't include the initial state of rules") - let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) - .compactMap { notification -> [MXPushRule]? in - guard let center = notification.object as? MXNotificationCenter else { + let rules = Publishers.Merge(rulesDidChange, firstSyncEndend) + .compactMap { [weak self] _ -> [MXPushRule]? in + guard let center = self?.mainMatrixSession?.notificationCenter else { return nil } @@ -278,7 +280,10 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } .eraseToAnyPublisher() - pushRulesUpdater = .init(checkSignal: needsRulesCheck, rulesProvider: rulesUpdated) + let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() + let needsRulesCheck = Publishers.Merge(firstSyncEndend, applicationDidBecomeActive).eraseOutput() + + pushRulesUpdater = .init(checkSignal: needsRulesCheck, rulesProvider: rules) } } From 1114177a165304989e5d6fdfba687bd311e5df8a Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 11:04:28 +0100 Subject: [PATCH 288/468] Move logic in AppConfiguration --- Config/AppConfiguration.swift | 34 +++++++++++++++++-- .../PushRulesUpdater/PushRulesUpdater.swift | 22 +++++++++--- Riot/Modules/Application/AppCoordinator.swift | 25 -------------- .../MXNotificationSettingsService.swift | 4 +-- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 70b1d78d5..3e70316b4 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import Foundation /// AppConfiguration is CommonConfiguration plus configurations dedicated to the app @@ -54,12 +55,17 @@ class AppConfiguration: CommonConfiguration { // MARK: - Per matrix session settings + private var pushRulesUpdater: PushRulesUpdater? + override func setupSettings(for matrixSession: MXSession) { super.setupSettings(for: matrixSession) setupWidgetReadReceipts(for: matrixSession) + setupPushRuleSync(for: matrixSession) } - - private func setupWidgetReadReceipts(for matrixSession: MXSession) { +} + +private extension AppConfiguration { + func setupWidgetReadReceipts(for matrixSession: MXSession) { var acknowledgableEventTypes = matrixSession.acknowledgableEventTypes ?? [] acknowledgableEventTypes.append(kWidgetMatrixEventTypeString) acknowledgableEventTypes.append(kWidgetModularEventTypeString) @@ -67,4 +73,28 @@ class AppConfiguration: CommonConfiguration { matrixSession.acknowledgableEventTypes = acknowledgableEventTypes } + func setupPushRuleSync(for matrixSession: MXSession) { + let firstSyncEnded = NotificationCenter.default.publisher(for: .mxSessionDidSync) + .first() + .eraseOutput() + + let rulesDidChange = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() + + let rules = Publishers.Merge(rulesDidChange, firstSyncEnded) + .compactMap { _ -> [MXPushRule]? in + guard let center = matrixSession.notificationCenter else { + return nil + } + + return center.flatRules as? [MXPushRule] + } + .eraseToAnyPublisher() + + let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() + let needsRulesCheck = Publishers.Merge(firstSyncEnded, applicationDidBecomeActive).eraseOutput() + + pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: matrixSession), + rules: rules, + needsCheck: needsRulesCheck) + } } diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 8b45d3035..b479febb1 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -19,13 +19,16 @@ import Combine final class PushRulesUpdater { private var cancellables: Set = .init() private var rules: [MXPushRule] = [] + private let notificationSettingsService: NotificationSettingsServiceType - init(checkSignal: AnyPublisher, rulesProvider: AnyPublisher<[MXPushRule], Never>) { - rulesProvider + init(notificationSettingsService: NotificationSettingsServiceType, rules: AnyPublisher<[MXPushRule], Never>, needsCheck: AnyPublisher) { + self.notificationSettingsService = notificationSettingsService + + rules .weakAssign(to: \.rules, on: self) .store(in: &cancellables) - checkSignal + needsCheck .sink { [weak self] _ in self?.updateRulesIfNeeded() } @@ -45,11 +48,20 @@ private extension PushRulesUpdater { for relatedRule in relatedRules { guard MXPushRule.haveSameContent(relatedRule, rule) == false else { - MXLog.debug("*** mismatch -> rule: \(relatedRule.ruleId)") + MXLog.debug("*** OK -> rule: \(relatedRule.ruleId)") continue } - MXLog.debug("*** OK -> rule: \(relatedRule.ruleId)") + let index = NotificationIndex.index(when: rule.enabled) + #warning("Fix me") + let standardActions = NotificationPushRuleId(rawValue: rule.ruleId)!.standardActions(for: index) + + MXLog.debug("*** mismatch -> rule: \(relatedRule.ruleId)") + Task { + try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, + enabled: rule.enabled, + actions: standardActions.actions) + } } } } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 389467ded..802f49e16 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -54,7 +54,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { fileprivate weak var sideMenuCoordinator: SideMenuCoordinatorType? private let userSessionsService: UserSessionsService - private var pushRulesUpdater: PushRulesUpdater? /// Main user Matrix session private var mainMatrixSession: MXSession? { @@ -86,7 +85,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { setupLogger() setupTheme() excludeAllItemsFromBackup() - setupPushRuleSync() // Setup navigation router store _ = NavigationRouterStore.shared @@ -262,29 +260,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Reload split view with selected space id self.splitViewCoordinator?.start(with: spaceId) } - - private func setupPushRuleSync() { - let firstSyncEndend = NotificationCenter.default.publisher(for: .mxSessionDidSync) - .first() - .eraseOutput() - - let rulesDidChange = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() - - let rules = Publishers.Merge(rulesDidChange, firstSyncEndend) - .compactMap { [weak self] _ -> [MXPushRule]? in - guard let center = self?.mainMatrixSession?.notificationCenter else { - return nil - } - - return center.flatRules as? [MXPushRule] - } - .eraseToAnyPublisher() - - let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() - let needsRulesCheck = Publishers.Merge(firstSyncEndend, applicationDidBecomeActive).eraseOutput() - - pushRulesUpdater = .init(checkSignal: needsRulesCheck, rulesProvider: rules) - } } // MARK: - LegacyAppDelegateDelegate diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 6ccd54c1a..84f25c106 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -38,14 +38,14 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) // Set initial value of the content rules - if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] { + if let contentRules = session.notificationCenter.rules?.global.content as? [MXPushRule] { self.contentRules = contentRules } // Observe future updates to content rules rulesUpdated .compactMap { [weak self] _ in - self?.session.notificationCenter.rules.global.content as? [MXPushRule] + self?.session.notificationCenter.rules?.global.content as? [MXPushRule] } .assign(to: &$contentRules) From 5503e5db21d95964fef86939b047aac63551d2fc Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 2 Feb 2023 10:03:33 +0000 Subject: [PATCH 289/468] Refresh notification service on crypto change --- RiotNSE/NotificationService.swift | 18 +++++++++++++++++- changelog.d/pr-7332.change | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr-7332.change diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index f9779641f..aca892844 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,6 +41,9 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? + #if DEBUG + private var isCryptoSDKEnabled = false + #endif /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -195,7 +198,7 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in @@ -214,6 +217,19 @@ class NotificationService: UNNotificationServiceExtension { } } + /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require + /// rebuilding `MXBackgroundSyncService` + private func hasChangedCryptoSDK() -> Bool { + #if DEBUG + if isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK { + isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK + return true + } + #endif + + return false + } + /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content diff --git a/changelog.d/pr-7332.change b/changelog.d/pr-7332.change new file mode 100644 index 000000000..94a5bdc89 --- /dev/null +++ b/changelog.d/pr-7332.change @@ -0,0 +1 @@ +CryptoV2: Refresh notification service on crypto change From d52aa20c9b64eae8059da85aee067543b024d036 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 12:04:40 +0100 Subject: [PATCH 290/468] Refactor PushRulesUpdater --- Config/AppConfiguration.swift | 24 ++----- .../PushRulesUpdater/PushRulesUpdater.swift | 67 ++++++++----------- .../MXNotificationSettingsService.swift | 18 ++++- .../NotificationSettingsViewModel.swift | 2 +- 4 files changed, 50 insertions(+), 61 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 3e70316b4..ce2ad7e77 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -74,27 +74,15 @@ private extension AppConfiguration { } func setupPushRuleSync(for matrixSession: MXSession) { - let firstSyncEnded = NotificationCenter.default.publisher(for: .mxSessionDidSync) - .first() - .eraseOutput() - - let rulesDidChange = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() - - let rules = Publishers.Merge(rulesDidChange, firstSyncEnded) - .compactMap { _ -> [MXPushRule]? in - guard let center = matrixSession.notificationCenter else { - return nil - } - - return center.flatRules as? [MXPushRule] + let sessionIsReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .first { _ in + matrixSession.state >= .running } - .eraseToAnyPublisher() + .eraseOutput() let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() - let needsRulesCheck = Publishers.Merge(firstSyncEnded, applicationDidBecomeActive).eraseOutput() + let needsRulesCheck = Publishers.Merge(sessionIsReady, applicationDidBecomeActive).eraseToAnyPublisher() - pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: matrixSession), - rules: rules, - needsCheck: needsRulesCheck) + pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: matrixSession), needsCheck: needsRulesCheck) } } diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index b479febb1..e7ad1462e 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -18,13 +18,14 @@ import Combine final class PushRulesUpdater { private var cancellables: Set = .init() - private var rules: [MXPushRule] = [] + private var rules: [NotificationPushRuleType] = [] private let notificationSettingsService: NotificationSettingsServiceType - init(notificationSettingsService: NotificationSettingsServiceType, rules: AnyPublisher<[MXPushRule], Never>, needsCheck: AnyPublisher) { + init(notificationSettingsService: NotificationSettingsServiceType, needsCheck: AnyPublisher) { self.notificationSettingsService = notificationSettingsService - rules + notificationSettingsService + .rulesPublisher .weakAssign(to: \.rules, on: self) .store(in: &cancellables) @@ -38,64 +39,50 @@ final class PushRulesUpdater { private extension PushRulesUpdater { func updateRulesIfNeeded() { + print("*** check started: \(rules.count)") for rule in rules { syncRelatedRulesIfNeeded(for: rule) } } - func syncRelatedRulesIfNeeded(for rule: MXPushRule) { - let relatedRules = rule.syncedRules(in: rules) + func syncRelatedRulesIfNeeded(for rule: NotificationPushRuleType) { + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + return + } + + let relatedRules = ruleId.syncedRules(in: rules) for relatedRule in relatedRules { - guard MXPushRule.haveSameContent(relatedRule, rule) == false else { - MXLog.debug("*** OK -> rule: \(relatedRule.ruleId)") + guard rule.hasSameContent(relatedRule) == false else { + print("*** OK -> rule: \(relatedRule.ruleId)") continue } - let index = NotificationIndex.index(when: rule.enabled) - #warning("Fix me") - let standardActions = NotificationPushRuleId(rawValue: rule.ruleId)!.standardActions(for: index) + let notificationOption = NotificationIndex.index(when: rule.enabled) - MXLog.debug("*** mismatch -> rule: \(relatedRule.ruleId)") + guard + let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + let expectedActions = ruleId.standardActions(for: notificationOption).actions + else { + return + } + + print("*** mismatch -> rule: \(relatedRule.ruleId)") Task { - try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, - enabled: rule.enabled, - actions: standardActions.actions) + try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, enabled: rule.enabled, actions: expectedActions) } } } } -private extension MXPushRule { - func syncedRules(in rules: [MXPushRule]) -> [MXPushRule] { +extension NotificationPushRuleType { + func hasSameContent(_ otherRule: NotificationPushRuleType) -> Bool { guard let ruleId = NotificationPushRuleId(rawValue: ruleId) else { - return [] - } - - return rules.filter { - guard let someRuleId = NotificationPushRuleId(rawValue: $0.ruleId) else { - return false - } - return ruleId.syncedRules.contains(someRuleId) - } - } - - static func haveSameContent(_ firstRule: MXPushRule, _ secondRule: MXPushRule) -> Bool { - guard - firstRule.enabled == secondRule.enabled, - let firstActions = firstRule.mxActions, - let secondActions = secondRule.mxActions, - firstActions.count == secondActions.count - else { return false } - return firstActions.indices.allSatisfy { index in - let action1 = firstActions[index] - let action2 = secondActions[index] - #warning("compare @property (nonatomic) NSDictionary *parameters") - return action1.actionType == action2.actionType - } + let notificationOption = NotificationIndex.index(when: enabled) + return otherRule.matches(standardActions: ruleId.standardActions(for: notificationOption)) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 84f25c106..d85c21e3a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -17,7 +17,7 @@ import Combine import Foundation -class MXNotificationSettingsService: NotificationSettingsServiceType { +final class MXNotificationSettingsService: NotificationSettingsServiceType { private let session: MXSession private var cancellables = Set() @@ -34,8 +34,22 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { init(session: MXSession) { self.session = session + // Publisher of all rule updates - let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) + let rulesUpdated: AnyPublisher + let didUpdateRules = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() + + if session.state >= .running { + rulesUpdated = didUpdateRules + } else { + let sessionIsReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .first { _ in + session.state >= .running + } + .eraseOutput() + + rulesUpdated = Publishers.Merge(sessionIsReady, didUpdateRules).eraseToAnyPublisher() + } // Set initial value of the content rules if let contentRules = session.notificationCenter.rules?.global.content as? [MXPushRule] { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index f782901b1..bbf76577f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -294,7 +294,7 @@ private extension NotificationSettingsViewModel { } } -private extension NotificationPushRuleId { +extension NotificationPushRuleId { func syncedRules(in rules: [NotificationPushRuleType]) -> [NotificationPushRuleType] { rules.filter { guard let ruleId = NotificationPushRuleId(rawValue: $0.ruleId) else { From 3e5a0dd47cc90f1e30b1351497a00a0f6ca1bbb2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 12:20:33 +0100 Subject: [PATCH 291/468] Cleanup --- Config/AppConfiguration.swift | 2 +- .../PushRulesUpdater/PushRulesUpdater.swift | 44 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index ce2ad7e77..64d16eafc 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -81,7 +81,7 @@ private extension AppConfiguration { .eraseOutput() let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() - let needsRulesCheck = Publishers.Merge(sessionIsReady, applicationDidBecomeActive).eraseToAnyPublisher() + let needsRulesCheck = Publishers.CombineLatest(sessionIsReady, applicationDidBecomeActive).eraseOutput() pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: matrixSession), needsCheck: needsRulesCheck) } diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index e7ad1462e..4a6ea5d64 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -31,14 +31,14 @@ final class PushRulesUpdater { needsCheck .sink { [weak self] _ in - self?.updateRulesIfNeeded() + self?.syncRulesIfNeeded() } .store(in: &cancellables) } } private extension PushRulesUpdater { - func updateRulesIfNeeded() { + func syncRulesIfNeeded() { print("*** check started: \(rules.count)") for rule in rules { syncRelatedRulesIfNeeded(for: rule) @@ -53,30 +53,34 @@ private extension PushRulesUpdater { let relatedRules = ruleId.syncedRules(in: rules) for relatedRule in relatedRules { - guard rule.hasSameContent(relatedRule) == false else { + guard rule.hasSameContentOf(relatedRule) == false else { print("*** OK -> rule: \(relatedRule.ruleId)") continue } - let notificationOption = NotificationIndex.index(when: rule.enabled) - - guard - let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), - let expectedActions = ruleId.standardActions(for: notificationOption).actions - else { - return - } - print("*** mismatch -> rule: \(relatedRule.ruleId)") - Task { - try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, enabled: rule.enabled, actions: expectedActions) - } + sync(relatedRuleId: relatedRule.ruleId, with: rule) + } + } + + func sync(relatedRuleId: String, with rule: NotificationPushRuleType) { + let notificationOption = NotificationIndex.index(when: rule.enabled) + + guard + let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + let expectedActions = ruleId.standardActions(for: notificationOption).actions + else { + return + } + + Task { + try? await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: expectedActions) } } } -extension NotificationPushRuleType { - func hasSameContent(_ otherRule: NotificationPushRuleType) -> Bool { +private extension NotificationPushRuleType { + func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool { guard let ruleId = NotificationPushRuleId(rawValue: ruleId) else { return false } @@ -85,9 +89,3 @@ extension NotificationPushRuleType { return otherRule.matches(standardActions: ruleId.standardActions(for: notificationOption)) } } - -private extension MXPushRule { - var mxActions: [MXPushRuleAction]? { - actions as? [MXPushRuleAction] - } -} From ba367d149d5d1af9873f5cf7ac79d93ae4177cd4 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 31 Oct 2022 12:23:52 +0000 Subject: [PATCH 292/468] Enable Crypto SDK for production --- Config/CommonConfiguration.swift | 3 +-- Config/CryptoSDKConfiguration.swift | 6 ++---- .../MatrixSDKCrypto+LocalizedError.swift | 5 ----- Riot/Managers/Settings/RiotSettings.swift | 2 -- Riot/Modules/Application/LegacyAppDelegate.m | 2 -- .../Home/AllChats/AllChatsViewController.swift | 10 ++++++---- Riot/Modules/Settings/SettingsViewController.m | 6 ------ RiotNSE/NotificationService.swift | 14 +++++--------- changelog.d/pr-7333.change | 1 + 9 files changed, 15 insertions(+), 34 deletions(-) create mode 100644 changelog.d/pr-7333.change diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index b98195e6d..a8b420b20 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -92,15 +92,14 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - #if DEBUG if sdkOptions.isCryptoSDKAvailable { let isEnabled = RiotSettings.shared.enableCryptoSDK MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") sdkOptions.enableCryptoSDK = isEnabled + sdkOptions.enableStartupProgress = isEnabled } else { MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") } - #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift index 935988ba9..3c922e547 100644 --- a/Config/CryptoSDKConfiguration.swift +++ b/Config/CryptoSDKConfiguration.swift @@ -16,8 +16,6 @@ import Foundation -#if DEBUG - /// Configuration for enabling / disabling Matrix Crypto SDK @objcMembers class CryptoSDKConfiguration: NSObject { static let shared = CryptoSDKConfiguration() @@ -29,6 +27,7 @@ import Foundation RiotSettings.shared.enableCryptoSDK = true MXSDKOptions.sharedInstance().enableCryptoSDK = true + MXSDKOptions.sharedInstance().enableStartupProgress = true MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK") } @@ -36,9 +35,8 @@ import Foundation func disable() { RiotSettings.shared.enableCryptoSDK = false MXSDKOptions.sharedInstance().enableCryptoSDK = false + MXSDKOptions.sharedInstance().enableStartupProgress = false MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK") } } - -#endif diff --git a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift index d802d54ff..4b314a0c1 100644 --- a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift +++ b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift @@ -15,9 +15,6 @@ // import Foundation - -#if DEBUG - import MatrixSDKCrypto extension CryptoStoreError: LocalizedError { @@ -27,5 +24,3 @@ extension CryptoStoreError: LocalizedError { return VectorL10n.e2eNeedLogInAgain } } - -#endif diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index c6617ad10..efb9a8e1c 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -192,11 +192,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults) var enableVoiceBroadcast - #if DEBUG /// Flag indicating if we are using rust-based `MatrixCryptoSDK` instead of `MatrixSDK`'s internal crypto module @UserDefault(key: "enableCryptoSDK", defaultValue: false, storage: defaults) var enableCryptoSDK - #endif // MARK: Calls diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 05fd5bcec..5944cccdb 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2184,9 +2184,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self clearCache]; // Reset Crypto SDK configuration (labs flag for which crypto module to use) -#if DEBUG [CryptoSDKConfiguration.shared disable]; -#endif // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index d6bef4e8e..a424c8346 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -865,10 +865,12 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { return } - let devices = mainSession.crypto.devices(forUser: mainSession.myUserId).values - let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) - if userHasOneUnverifiedDevice { - presentReviewUnverifiedSessionsAlert(with: session) + if let userId = mainSession.myUserId, let crypto = mainSession.crypto { + let devices = crypto.devices(forUser: userId).values + let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) + if userHasOneUnverifiedDevice { + presentReviewUnverifiedSessionsAlert(with: session) + } } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 5cf6e933b..93fffc9e8 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -588,12 +588,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - #if DEBUG if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable) { [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; } - #endif [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; @@ -2593,7 +2591,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } else { - #if DEBUG if (row == LABS_ENABLE_CRYPTO_SDK) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -2606,7 +2603,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = labelAndSwitchCell; } - #endif } } else if (section == SECTION_TAG_SECURITY) @@ -3379,7 +3375,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -#if DEBUG - (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender { BOOL isEnabled = sender.isOn; @@ -3407,7 +3402,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self presentViewController:confirmationAlert animated:YES completion:nil]; currentAlert = confirmationAlert; } -#endif - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index aca892844..6560290ab 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,9 +41,7 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? - #if DEBUG private var isCryptoSDKEnabled = false - #endif /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -201,6 +199,7 @@ class NotificationService: UNNotificationServiceExtension { if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() + NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in @@ -220,14 +219,11 @@ class NotificationService: UNNotificationServiceExtension { /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require /// rebuilding `MXBackgroundSyncService` private func hasChangedCryptoSDK() -> Bool { - #if DEBUG - if isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK { - isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK - return true + guard isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK else { + return false } - #endif - - return false + isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK + return true } /// Attempts to preprocess payload and attach room display name to the best attempt content diff --git a/changelog.d/pr-7333.change b/changelog.d/pr-7333.change new file mode 100644 index 000000000..fbf81e873 --- /dev/null +++ b/changelog.d/pr-7333.change @@ -0,0 +1 @@ +CryptoV2: Enable Crypto SDK for production From 5c1dca8fe46f2588d398af31e931596fff7d581f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 15:02:13 +0100 Subject: [PATCH 293/468] Put logic back in AppCoordinator --- Config/AppConfiguration.swift | 22 ++---------- Riot/Modules/Application/AppCoordinator.swift | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 64d16eafc..70b1d78d5 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -14,7 +14,6 @@ // limitations under the License. // -import Combine import Foundation /// AppConfiguration is CommonConfiguration plus configurations dedicated to the app @@ -55,17 +54,12 @@ class AppConfiguration: CommonConfiguration { // MARK: - Per matrix session settings - private var pushRulesUpdater: PushRulesUpdater? - override func setupSettings(for matrixSession: MXSession) { super.setupSettings(for: matrixSession) setupWidgetReadReceipts(for: matrixSession) - setupPushRuleSync(for: matrixSession) } -} - -private extension AppConfiguration { - func setupWidgetReadReceipts(for matrixSession: MXSession) { + + private func setupWidgetReadReceipts(for matrixSession: MXSession) { var acknowledgableEventTypes = matrixSession.acknowledgableEventTypes ?? [] acknowledgableEventTypes.append(kWidgetMatrixEventTypeString) acknowledgableEventTypes.append(kWidgetModularEventTypeString) @@ -73,16 +67,4 @@ private extension AppConfiguration { matrixSession.acknowledgableEventTypes = acknowledgableEventTypes } - func setupPushRuleSync(for matrixSession: MXSession) { - let sessionIsReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) - .first { _ in - matrixSession.state >= .running - } - .eraseOutput() - - let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() - let needsRulesCheck = Publishers.CombineLatest(sessionIsReady, applicationDidBecomeActive).eraseOutput() - - pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: matrixSession), needsCheck: needsRulesCheck) - } } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 802f49e16..6c3e55275 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -61,6 +61,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } private var currentSpaceId: String? + private var cancellables: Set = .init() + private var pushRulesUpdater: PushRulesUpdater? // MARK: Public @@ -85,6 +87,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { setupLogger() setupTheme() excludeAllItemsFromBackup() + setupPushRulesSync() // Setup navigation router store _ = NavigationRouterStore.shared @@ -260,6 +263,37 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // Reload split view with selected space id self.splitViewCoordinator?.start(with: spaceId) } + + private func setupPushRulesSync() { + let sessionReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .compactMap { $0.object as? MXSession } + .filter { $0.state == .running } + .removeDuplicates { session1, session2 in + session1 == session2 + } + + sessionReady + .print("*** ready") + .sink { [weak self] session in + let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() + let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher() + + self?.pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session), needsCheck: needsCheckPublisher) + } + .store(in: &cancellables) + + + let sessionClosed = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) + .compactMap { $0.object as? MXSession } + .filter { $0.state == .closed } + + sessionClosed + .print("*** closed") + .sink { [weak self] _ in + self?.pushRulesUpdater = nil + } + .store(in: &cancellables) + } } // MARK: - LegacyAppDelegateDelegate From d429388c98bb41b8a9e2380723704e97392b82c1 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 15:03:40 +0100 Subject: [PATCH 294/468] Add empty PushRulesUpdaterTests --- RiotTests/PushRulesUpdaterTests.swift | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 RiotTests/PushRulesUpdaterTests.swift diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift new file mode 100644 index 000000000..80a81e508 --- /dev/null +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -0,0 +1,34 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest +@testable import Element + +final class PushRulesUpdaterTests: XCTestCase { + private var notificationService: MockNotificationSettingsService! + private var pushRulesUpdater: PushRulesUpdater! + private var needsCheckPublisher: PassthroughSubject = .init() + + override func setUpWithError() throws { + notificationService = .init() + pushRulesUpdater = .init(notificationSettingsService: notificationService, needsCheck: needsCheckPublisher.eraseOutput()) + } + + func testExample() throws { + + } +} From fa6ea2d4a6f0b9b7055f5e2fa0fb9434c32ce9c6 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 15:07:00 +0100 Subject: [PATCH 295/468] Restore MXNotificationSettingsService --- .../MXNotificationSettingsService.swift | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index d85c21e3a..6ccd54c1a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -17,7 +17,7 @@ import Combine import Foundation -final class MXNotificationSettingsService: NotificationSettingsServiceType { +class MXNotificationSettingsService: NotificationSettingsServiceType { private let session: MXSession private var cancellables = Set() @@ -34,32 +34,18 @@ final class MXNotificationSettingsService: NotificationSettingsServiceType { init(session: MXSession) { self.session = session - // Publisher of all rule updates - let rulesUpdated: AnyPublisher - let didUpdateRules = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)).eraseOutput() - - if session.state >= .running { - rulesUpdated = didUpdateRules - } else { - let sessionIsReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) - .first { _ in - session.state >= .running - } - .eraseOutput() - - rulesUpdated = Publishers.Merge(sessionIsReady, didUpdateRules).eraseToAnyPublisher() - } + let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) // Set initial value of the content rules - if let contentRules = session.notificationCenter.rules?.global.content as? [MXPushRule] { + if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] { self.contentRules = contentRules } // Observe future updates to content rules rulesUpdated .compactMap { [weak self] _ in - self?.session.notificationCenter.rules?.global.content as? [MXPushRule] + self?.session.notificationCenter.rules.global.content as? [MXPushRule] } .assign(to: &$contentRules) From d269fb726d4bab13a407af44d95e2534716a4f66 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 15:18:52 +0100 Subject: [PATCH 296/468] Add NotificationPushRuleType.pushRuleId --- Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift | 6 +++--- .../Notifications/Model/NotificationPushRuleType.swift | 6 ++++++ .../ViewModel/NotificationSettingsViewModel.swift | 10 +++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 4a6ea5d64..e32841104 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -46,7 +46,7 @@ private extension PushRulesUpdater { } func syncRelatedRulesIfNeeded(for rule: NotificationPushRuleType) { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + guard let ruleId = rule.pushRuleId else { return } @@ -67,7 +67,7 @@ private extension PushRulesUpdater { let notificationOption = NotificationIndex.index(when: rule.enabled) guard - let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + let ruleId = rule.pushRuleId, let expectedActions = ruleId.standardActions(for: notificationOption).actions else { return @@ -81,7 +81,7 @@ private extension PushRulesUpdater { private extension NotificationPushRuleType { func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: ruleId) else { + guard let ruleId = pushRuleId else { return false } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift index 1f98242c7..928b3993d 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift @@ -21,3 +21,9 @@ protocol NotificationPushRuleType { var enabled: Bool { get } func matches(standardActions: NotificationStandardActions?) -> Bool } + +extension NotificationPushRuleType { + var pushRuleId: NotificationPushRuleId? { + ruleId.flatMap(NotificationPushRuleId.init(rawValue:)) + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index bbf76577f..154f926ce 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -218,7 +218,7 @@ private extension NotificationSettingsViewModel { for rule in newRules { guard - let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + let ruleId = rule.pushRuleId, ruleIds.contains(ruleId) else { continue @@ -248,7 +248,7 @@ private extension NotificationSettingsViewModel { /// - Parameter rule: The push rule type to check. /// - Returns: Wether it should be displayed as checked or not checked. func defaultIsChecked(rule: NotificationPushRuleType) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + guard let ruleId = rule.pushRuleId else { return false } @@ -264,7 +264,7 @@ private extension NotificationSettingsViewModel { } func isChecked(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + guard let ruleId = rule.pushRuleId else { return false } @@ -280,7 +280,7 @@ private extension NotificationSettingsViewModel { } func isOutOfSync(rule: NotificationPushRuleType, syncedRules: [NotificationPushRuleType]) -> Bool { - guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { + guard let ruleId = rule.pushRuleId else { return false } @@ -297,7 +297,7 @@ private extension NotificationSettingsViewModel { extension NotificationPushRuleId { func syncedRules(in rules: [NotificationPushRuleType]) -> [NotificationPushRuleType] { rules.filter { - guard let ruleId = NotificationPushRuleId(rawValue: $0.ruleId) else { + guard let ruleId = $0.pushRuleId else { return false } return syncedRules.contains(ruleId) From beb0da9d35835dff0237ac74d625b7802a152814 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 15:33:20 +0100 Subject: [PATCH 297/468] Improve NotificationPushRuleType protocol --- Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift | 9 ++------- .../Model/MatrixSDK/MXNotificationPushRule.swift | 4 ++++ .../Model/Mock/MockNotificationPushRule.swift | 4 ++-- .../Notifications/Model/NotificationPushRuleType.swift | 2 ++ .../Service/Mock/MockNotificationSettingsService.swift | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index e32841104..19a03b509 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -80,12 +80,7 @@ private extension PushRulesUpdater { } private extension NotificationPushRuleType { - func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool { - guard let ruleId = pushRuleId else { - return false - } - - let notificationOption = NotificationIndex.index(when: enabled) - return otherRule.matches(standardActions: ruleId.standardActions(for: notificationOption)) + func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool? { + enabled == otherRule.enabled && ruleActions == otherRule.ruleActions } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift index 2c4d16d8b..337ed4516 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift @@ -41,6 +41,10 @@ extension MXPushRule: NotificationPushRuleType { return false } + var ruleActions: NotificationActions? { + .init(notify: notify, highlight: highlight, sound: sound) + } + private func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? { guard let actions = actions as? [MXPushRuleAction] else { return nil diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift index ab7192646..8f8185535 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift @@ -19,9 +19,9 @@ import Foundation struct MockNotificationPushRule: NotificationPushRuleType { var ruleId: String! var enabled: Bool - var actions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions + var ruleActions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions func matches(standardActions: NotificationStandardActions?) -> Bool { - standardActions?.actions == actions + standardActions?.actions == ruleActions } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift index 928b3993d..14ed88e69 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift @@ -19,6 +19,8 @@ import Foundation protocol NotificationPushRuleType { var ruleId: String! { get } var enabled: Bool { get } + var ruleActions: NotificationActions? { get } + func matches(standardActions: NotificationStandardActions?) -> Bool } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index ea4bd640c..0bff31370 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -49,6 +49,6 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab return } - rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, enabled: enabled, actions: actions) + rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId, enabled: enabled, ruleActions: actions) } } From 8fc47d5e697616cbd3403aeb0689bba9d79199f8 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 16:36:50 +0100 Subject: [PATCH 298/468] Add UTs --- .../PushRulesUpdater/PushRulesUpdater.swift | 11 +-- .../Model/Mock/MockNotificationPushRule.swift | 2 +- RiotTests/PushRulesUpdaterTests.swift | 95 ++++++++++++++++++- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 19a03b509..2b810801a 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -64,17 +64,8 @@ private extension PushRulesUpdater { } func sync(relatedRuleId: String, with rule: NotificationPushRuleType) { - let notificationOption = NotificationIndex.index(when: rule.enabled) - - guard - let ruleId = rule.pushRuleId, - let expectedActions = ruleId.standardActions(for: notificationOption).actions - else { - return - } - Task { - try? await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: expectedActions) + try? await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: rule.ruleActions) } } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift index 8f8185535..64aff0388 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift @@ -16,7 +16,7 @@ import Foundation -struct MockNotificationPushRule: NotificationPushRuleType { +struct MockNotificationPushRule: NotificationPushRuleType, Equatable { var ruleId: String! var enabled: Bool var ruleActions: NotificationActions? = NotificationStandardActions.notifyDefaultSound.actions diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift index 80a81e508..c83cf233d 100644 --- a/RiotTests/PushRulesUpdaterTests.swift +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -25,10 +25,103 @@ final class PushRulesUpdaterTests: XCTestCase { override func setUpWithError() throws { notificationService = .init() + notificationService.rules = [MockNotificationPushRule].default pushRulesUpdater = .init(notificationSettingsService: notificationService, needsCheck: needsCheckPublisher.eraseOutput()) } - func testExample() throws { + func testNoRuleIsUpdated() throws { + needsCheckPublisher.send() + XCTAssertEqual(notificationService.rules as? [MockNotificationPushRule], [MockNotificationPushRule].default) + } + + func testSingleRuleAffected() throws { + let expectation = expectation(description: #function) + let targetActions: NotificationActions = .init(notify: true, sound: "default") + let targetRuleIndex = try mockRule(ruleId: .pollStart, enabled: false, actions: targetActions) + + needsCheckPublisher.send(()) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) + expectation.fulfill() + } + waitForExpectations(timeout: 1.0) + } + + func testAffectedRulesAreUpdated() throws { + let expectation = expectation(description: #function) + + let targetActions: NotificationActions = .init(notify: true, sound: "abc") + try mockRule(ruleId: .allOtherMessages, enabled: true, actions: targetActions) + let affectedRules: [NotificationPushRuleId] = [.allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] + + needsCheckPublisher.send(()) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testAffectedOneToOneRulesAreUpdated() throws { + let expectation = expectation(description: #function) + + let targetActions: NotificationActions = .init(notify: true, sound: "abc") + try mockRule(ruleId: .oneToOneRoom, enabled: true, actions: targetActions) + let affectedRules: [NotificationPushRuleId] = [.oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + + needsCheckPublisher.send(()) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } +} + +private extension PushRulesUpdaterTests { + @discardableResult + func mockRule(ruleId: NotificationPushRuleId, enabled: Bool, actions: NotificationActions) throws -> Int { + guard let ruleIndex = notificationService.rules.firstIndex(where: { $0.pushRuleId == ruleId }) else { + throw NSError(domain: "no ruleIndex found", code: 0) + } + notificationService.rules[ruleIndex] = MockNotificationPushRule(ruleId: ruleId.rawValue, enabled: enabled, ruleActions: actions) + return ruleIndex + } +} + +private extension Array where Element == MockNotificationPushRule { + static var `default`: [MockNotificationPushRule] { + let ids: [NotificationPushRuleId] = [.oneToOneRoom, .allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] + + return ids.map { + MockNotificationPushRule(ruleId: $0.rawValue, enabled: true) + } } } From e31819061a31f97e005f3c12aaf0e2b4d2963fd2 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 17:01:24 +0100 Subject: [PATCH 299/468] Remove debug prints --- Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift | 3 --- Riot/Modules/Application/AppCoordinator.swift | 2 -- 2 files changed, 5 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 2b810801a..8e984f3b2 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -39,7 +39,6 @@ final class PushRulesUpdater { private extension PushRulesUpdater { func syncRulesIfNeeded() { - print("*** check started: \(rules.count)") for rule in rules { syncRelatedRulesIfNeeded(for: rule) } @@ -54,11 +53,9 @@ private extension PushRulesUpdater { for relatedRule in relatedRules { guard rule.hasSameContentOf(relatedRule) == false else { - print("*** OK -> rule: \(relatedRule.ruleId)") continue } - print("*** mismatch -> rule: \(relatedRule.ruleId)") sync(relatedRuleId: relatedRule.ruleId, with: rule) } } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 6c3e55275..a53807db5 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -273,7 +273,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } sessionReady - .print("*** ready") .sink { [weak self] session in let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher() @@ -288,7 +287,6 @@ final class AppCoordinator: NSObject, AppCoordinatorType { .filter { $0.state == .closed } sessionClosed - .print("*** closed") .sink { [weak self] _ in self?.pushRulesUpdater = nil } From d4ab4d391096cad4ea8483b3b21d78ff8cf76a2c Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 17:02:31 +0100 Subject: [PATCH 300/468] Add changelog.d file --- changelog.d/pr-7335.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7335.change diff --git a/changelog.d/pr-7335.change b/changelog.d/pr-7335.change new file mode 100644 index 000000000..62512e930 --- /dev/null +++ b/changelog.d/pr-7335.change @@ -0,0 +1 @@ +Polls: add automatic synchronization logic for poll push rules. From 9d80b9e341b47154671afe98793a4f4583f9740c Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 26 Jan 2023 20:02:31 +0000 Subject: [PATCH 301/468] Generate crypto store key --- Riot/Assets/en.lproj/Vector.strings | 6 ++-- .../MatrixSDKCrypto+LocalizedError.swift | 31 +++++++++++++++++++ Riot/Generated/Strings.swift | 6 ++-- .../EncryptionKeyManager.swift | 12 +++++++ .../Modules/Settings/SettingsViewController.m | 2 +- changelog.d/pr-7310.change | 1 + 6 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 Riot/Categories/MatrixSDKCrypto+LocalizedError.swift create mode 100644 changelog.d/pr-7310.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 5d7ac9e4a..2c555f6fc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -804,9 +804,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "Enable new rust-based Crypto SDK"; -"settings_labs_confirm_crypto_sdk" = "This action cannot be undone"; -"settings_labs_disable_crypto_sdk" = "Crypto SDK is enabled. To disable please reinstall the app"; +"settings_labs_enable_crypto_sdk" = "End-to-end encryption 2.0"; +"settings_labs_confirm_crypto_sdk" = "This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed?"; +"settings_labs_disable_crypto_sdk" = "End-to-end encryption 2.0 (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift new file mode 100644 index 000000000..d802d54ff --- /dev/null +++ b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift @@ -0,0 +1,31 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG + +import MatrixSDKCrypto + +extension CryptoStoreError: LocalizedError { + public var errorDescription: String? { + // We dont really care about the type of error here when showing to the user. + // Details about the error are tracked independently + return VectorL10n.e2eNeedLogInAgain + } +} + +#endif diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d42977875..5df732af1 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7583,7 +7583,7 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// This action cannot be undone + /// This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed? public static var settingsLabsConfirmCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") } @@ -7591,7 +7591,7 @@ public class VectorL10n: NSObject { public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// Crypto SDK is enabled. To disable please reinstall the app + /// End-to-end encryption 2.0 (log out to disable) public static var settingsLabsDisableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") } @@ -7607,7 +7607,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// Enable new rust-based Crypto SDK + /// End-to-end encryption 2.0 public static var settingsLabsEnableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") } diff --git a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift index 5085e9efb..484a63832 100644 --- a/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift +++ b/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift @@ -31,6 +31,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { private static let cryptoOlmPickleKey: KeyValueStoreKey = "cryptoOlmPickleKey" private static let roomLastMessageIv: KeyValueStoreKey = "roomLastMessageIv" private static let roomLastMessageAesKey: KeyValueStoreKey = "roomLastMessageAesKey" + private static let cryptoSDKStoreKey: KeyValueStoreKey = "cryptoSDKStoreKey" private let keychainStore: KeyValueStore = KeychainStore(withKeychain: Keychain(service: keychainService, accessGroup: BuildSettings.keychainAccessGroup)) @@ -47,6 +48,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { generateKeyIfNotExists(forKey: EncryptionKeyManager.cryptoOlmPickleKey, size: 32) generateIvIfNotExists(forKey: EncryptionKeyManager.roomLastMessageIv) generateAesKeyIfNotExists(forKey: EncryptionKeyManager.roomLastMessageAesKey) + generateKeyIfNotExists(forKey: EncryptionKeyManager.cryptoSDKStoreKey, size: 32) assert(keychainStore.containsObject(forKey: EncryptionKeyManager.contactsIv), "[EncryptionKeyManager] initKeys: Failed to generate IV for acount") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.contactsAesKey), "[EncryptionKeyManager] initKeys: Failed to generate AES Key for acount") @@ -55,6 +57,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { assert(keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoOlmPickleKey), "[EncryptionKeyManager] initKeys: Failed to generate Key for olm pickle key") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageIv), "[EncryptionKeyManager] initKeys: Failed to generate IV for room last message") assert(keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageAesKey), "[EncryptionKeyManager] initKeys: Failed to generate AES Key for room last message encryption") + assert(keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoSDKStoreKey), "[EncryptionKeyManager] initKeys: Failed to generate Key for crypto sdk store") } // MARK: - MXKeyProviderDelegate @@ -64,6 +67,7 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { || dataType == MXKAccountManagerDataType || dataType == MXCryptoOlmPickleKeyDataType || dataType == MXRoomLastMessageDataType + || dataType == MXCryptoSDKStoreKeyDataType } func hasKeyForData(ofType dataType: String) -> Bool { @@ -77,7 +81,10 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { case MXRoomLastMessageDataType: return keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageIv) && keychainStore.containsObject(forKey: EncryptionKeyManager.roomLastMessageAesKey) + case MXCryptoSDKStoreKeyDataType: + return keychainStore.containsObject(forKey: EncryptionKeyManager.cryptoSDKStoreKey) default: + MXLog.warning("[EncryptionKeyManager] hasKeyForData: No key for \(dataType)") return false } } @@ -103,7 +110,12 @@ class EncryptionKeyManager: NSObject, MXKeyProviderDelegate { let aesKey = try? keychainStore.data(forKey: EncryptionKeyManager.roomLastMessageAesKey) { return MXAesKeyData(iv: ivKey, key: aesKey) } + case MXCryptoSDKStoreKeyDataType: + if let key = try? keychainStore.data(forKey: EncryptionKeyManager.cryptoSDKStoreKey) { + return MXRawDataKey(key: key) + } default: + MXLog.failure("[EncryptionKeyManager] keyDataForData: Attempting to get data for unknown type", dataType) return nil } return nil diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 90533cfd1..fc10edf0e 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3386,7 +3386,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> MXWeakify(self); [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:nil + UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk message:VectorL10n.settingsLabsConfirmCryptoSdk preferredStyle:UIAlertControllerStyleAlert]; diff --git a/changelog.d/pr-7310.change b/changelog.d/pr-7310.change new file mode 100644 index 000000000..4ba5e9ee1 --- /dev/null +++ b/changelog.d/pr-7310.change @@ -0,0 +1 @@ +CryptoV2: Generate Crypto SDK store key From 8c76c4a1ca1868e3dd101b5555734353023b0ee1 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 30 Jan 2023 14:45:07 +0000 Subject: [PATCH 302/468] Display backup import progress --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ ...verFromPrivateKeyViewController.storyboard | 26 ++++++++++++------- ...pRecoverFromPrivateKeyViewController.swift | 10 ++++--- ...BackupRecoverFromPrivateKeyViewModel.swift | 15 ++++++++++- ...BackupRecoverFromPrivateKeyViewState.swift | 2 +- changelog.d/pr-7319.change | 1 + 7 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 changelog.d/pr-7319.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 2c555f6fc..4d56129eb 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1469,6 +1469,7 @@ Tap the + to start adding people."; // Recover from private key "key_backup_recover_from_private_key_info" = "Restoring backup…"; +"key_backup_recover_from_private_key_progress" = "%@%% Complete"; // Recover from passphrase diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5df732af1..47f6a77ae 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2755,6 +2755,10 @@ public class VectorL10n: NSObject { public static var keyBackupRecoverFromPrivateKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_info") } + /// %@%% Complete + public static func keyBackupRecoverFromPrivateKeyProgress(_ p1: String) -> String { + return VectorL10n.tr("Vector", "key_backup_recover_from_private_key_progress", p1) + } /// Use your Security Key to unlock your secure message history public static var keyBackupRecoverFromRecoveryKeyInfo: String { return VectorL10n.tr("Vector", "key_backup_recover_from_recovery_key_info") diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard index 1c8ba341c..42e99205e 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.storyboard @@ -1,25 +1,23 @@ - - - - + + - + - + - + - + @@ -40,15 +38,24 @@ + + + + @@ -72,6 +79,7 @@ + @@ -79,10 +87,10 @@ - + diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift index 1aaf96e62..a02fed201 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewController.swift @@ -29,6 +29,7 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { @IBOutlet private weak var shieldImageView: UIImageView! @IBOutlet private weak var informationLabel: UILabel! + @IBOutlet private weak var progressLabel: UILabel! // MARK: Private @@ -118,8 +119,8 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { private func render(viewState: KeyBackupRecoverFromPrivateKeyViewState) { switch viewState { - case .loading: - self.renderLoading() + case .loading(let progress): + self.renderLoading(progress: progress) case .loaded: self.renderLoaded() case .error(let error): @@ -127,8 +128,11 @@ final class KeyBackupRecoverFromPrivateKeyViewController: UIViewController { } } - private func renderLoading() { + private func renderLoading(progress: Double) { self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + + let percent = Int(round(progress * 100)) + self.progressLabel.text = VectorL10n.keyBackupRecoverFromPrivateKeyProgress("\(percent)") } private func renderLoaded() { diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift index cef1d7c0c..04fb48850 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewModel.swift @@ -27,6 +27,7 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private let keyBackup: MXKeyBackup private var currentHTTPOperation: MXHTTPOperation? private let keyBackupVersion: MXKeyBackupVersion + private var progressUpdateTimer: Timer? // MARK: Public @@ -56,7 +57,14 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate private func recoverWithPrivateKey() { - self.update(viewState: .loading) + self.update(viewState: .loading(0)) + + // Update loading progress every second until no longer loading + progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + if let progress = self?.keyBackup.importProgress { + self?.update(viewState: .loading(progress.fractionCompleted)) + } + } self.currentHTTPOperation = keyBackup.restore(usingPrivateKeyKeyBackup: keyBackupVersion, room: nil, session: nil, success: { [weak self] (_, _) in guard let self = self else { @@ -91,6 +99,11 @@ final class KeyBackupRecoverFromPrivateKeyViewModel: KeyBackupRecoverFromPrivate } private func update(viewState: KeyBackupRecoverFromPrivateKeyViewState) { + if case .loading = viewState {} else { + progressUpdateTimer?.invalidate() + progressUpdateTimer = nil + } + self.viewDelegate?.keyBackupRecoverFromPrivateKeyViewModel(self, didUpdateViewState: viewState) } } diff --git a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift index bdd417853..b4ef05fb9 100644 --- a/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift +++ b/Riot/Modules/KeyBackup/Recover/PrivateKey/KeyBackupRecoverFromPrivateKeyViewState.swift @@ -20,7 +20,7 @@ import Foundation /// KeyBackupRecoverFromPrivateKeyViewController view state enum KeyBackupRecoverFromPrivateKeyViewState { - case loading + case loading(Double) case loaded case error(Error) } diff --git a/changelog.d/pr-7319.change b/changelog.d/pr-7319.change new file mode 100644 index 000000000..187b315b5 --- /dev/null +++ b/changelog.d/pr-7319.change @@ -0,0 +1 @@ +Backup: Display backup import progress From 246c5a0c450f6b4fc1b36b4c73ee1a298bf1b6ea Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 31 Jan 2023 13:07:16 +0000 Subject: [PATCH 303/468] Reset Crypto SDK on logout --- Config/AppConfiguration.swift | 3 + Config/CommonConfiguration.swift | 6 -- Config/CryptoSDKConfiguration.swift | 55 +++++++++++++++++++ Riot/Modules/Application/LegacyAppDelegate.m | 5 ++ .../Modules/Settings/SettingsViewController.m | 3 +- changelog.d/pr-7323.change | 1 + 6 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 Config/CryptoSDKConfiguration.swift create mode 100644 changelog.d/pr-7323.change diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 70b1d78d5..fe83fba1f 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -24,6 +24,9 @@ class AppConfiguration: CommonConfiguration { override func setupSettings() { super.setupSettings() setupAppSettings() +#if DEBUG + CryptoSDKConfiguration.shared.setup() +#endif } private func setupAppSettings() { diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index f3172a710..fee3796ff 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,12 +91,6 @@ class CommonConfiguration: NSObject, Configurable { MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - - #if DEBUG - if sdkOptions.isCryptoSDKAvailable { - sdkOptions.enableCryptoSDK = RiotSettings.shared.enableCryptoSDK - } - #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift new file mode 100644 index 000000000..6edde7871 --- /dev/null +++ b/Config/CryptoSDKConfiguration.swift @@ -0,0 +1,55 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG + +/// Configuration for enabling / disabling Matrix Crypto SDK +@objcMembers class CryptoSDKConfiguration: NSObject { + static let shared = CryptoSDKConfiguration() + + func setup() { + guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { + return + } + + let isEnabled = RiotSettings.shared.enableCryptoSDK + MXSDKOptions.sharedInstance().enableCryptoSDK = isEnabled + + MXLog.debug("[CryptoSDKConfiguration] setup: Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + } + + func enable() { + guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { + return + } + + RiotSettings.shared.enableCryptoSDK = true + MXSDKOptions.sharedInstance().enableCryptoSDK = true + + MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK") + } + + func disable() { + RiotSettings.shared.enableCryptoSDK = false + MXSDKOptions.sharedInstance().enableCryptoSDK = false + + MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK") + } +} + +#endif diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 7bf51dd46..1fb151e20 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2183,6 +2183,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; + // Reset Crypto SDK configuration (labs flag for which crypto module to use) +#if DEBUG + [CryptoSDKConfiguration.shared disable]; +#endif + // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index fc10edf0e..5cf6e933b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3400,8 +3400,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); - RiotSettings.shared.enableCryptoSDK = isEnabled; - MXSDKOptions.sharedInstance.enableCryptoSDK = isEnabled; + [CryptoSDKConfiguration.shared enable]; [[AppDelegate theDelegate] reloadMatrixSessions:YES]; }]]; diff --git a/changelog.d/pr-7323.change b/changelog.d/pr-7323.change new file mode 100644 index 000000000..308cf2813 --- /dev/null +++ b/changelog.d/pr-7323.change @@ -0,0 +1 @@ +CryptoV2: Reset Crypto SDK on logout From 3614f5ab05db73c5a11ce811a425a4f685f76116 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 1 Feb 2023 11:49:16 +0000 Subject: [PATCH 304/468] Fix crypto v2 config --- Config/AppConfiguration.swift | 3 --- Config/CommonConfiguration.swift | 10 ++++++++++ Config/CryptoSDKConfiguration.swift | 11 ----------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index fe83fba1f..70b1d78d5 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -24,9 +24,6 @@ class AppConfiguration: CommonConfiguration { override func setupSettings() { super.setupSettings() setupAppSettings() -#if DEBUG - CryptoSDKConfiguration.shared.setup() -#endif } private func setupAppSettings() { diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index fee3796ff..b98195e6d 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,6 +91,16 @@ class CommonConfiguration: NSObject, Configurable { MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature + + #if DEBUG + if sdkOptions.isCryptoSDKAvailable { + let isEnabled = RiotSettings.shared.enableCryptoSDK + MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + sdkOptions.enableCryptoSDK = isEnabled + } else { + MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") + } + #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift index 6edde7871..935988ba9 100644 --- a/Config/CryptoSDKConfiguration.swift +++ b/Config/CryptoSDKConfiguration.swift @@ -22,17 +22,6 @@ import Foundation @objcMembers class CryptoSDKConfiguration: NSObject { static let shared = CryptoSDKConfiguration() - func setup() { - guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { - return - } - - let isEnabled = RiotSettings.shared.enableCryptoSDK - MXSDKOptions.sharedInstance().enableCryptoSDK = isEnabled - - MXLog.debug("[CryptoSDKConfiguration] setup: Crypto SDK is \(isEnabled ? "enabled" : "disabled")") - } - func enable() { guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { return From 82bae5b210d74c27a9dcf467fe9dbd57f90350bd Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 2 Feb 2023 10:03:33 +0000 Subject: [PATCH 305/468] Refresh notification service on crypto change --- RiotNSE/NotificationService.swift | 18 +++++++++++++++++- changelog.d/pr-7332.change | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr-7332.change diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index f9779641f..aca892844 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,6 +41,9 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? + #if DEBUG + private var isCryptoSDKEnabled = false + #endif /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -195,7 +198,7 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in @@ -214,6 +217,19 @@ class NotificationService: UNNotificationServiceExtension { } } + /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require + /// rebuilding `MXBackgroundSyncService` + private func hasChangedCryptoSDK() -> Bool { + #if DEBUG + if isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK { + isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK + return true + } + #endif + + return false + } + /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content diff --git a/changelog.d/pr-7332.change b/changelog.d/pr-7332.change new file mode 100644 index 000000000..94a5bdc89 --- /dev/null +++ b/changelog.d/pr-7332.change @@ -0,0 +1 @@ +CryptoV2: Refresh notification service on crypto change From 5aa0ada7594cf47d9a97b2bf5c90a5177c8b8df3 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 31 Oct 2022 12:23:52 +0000 Subject: [PATCH 306/468] Enable Crypto SDK for production --- Config/CommonConfiguration.swift | 3 +-- Config/CryptoSDKConfiguration.swift | 6 ++---- .../MatrixSDKCrypto+LocalizedError.swift | 5 ----- Riot/Managers/Settings/RiotSettings.swift | 2 -- Riot/Modules/Application/LegacyAppDelegate.m | 2 -- .../Home/AllChats/AllChatsViewController.swift | 10 ++++++---- Riot/Modules/Settings/SettingsViewController.m | 6 ------ RiotNSE/NotificationService.swift | 14 +++++--------- changelog.d/pr-7333.change | 1 + 9 files changed, 15 insertions(+), 34 deletions(-) create mode 100644 changelog.d/pr-7333.change diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index b98195e6d..a8b420b20 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -92,15 +92,14 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - #if DEBUG if sdkOptions.isCryptoSDKAvailable { let isEnabled = RiotSettings.shared.enableCryptoSDK MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") sdkOptions.enableCryptoSDK = isEnabled + sdkOptions.enableStartupProgress = isEnabled } else { MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") } - #endif } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift index 935988ba9..3c922e547 100644 --- a/Config/CryptoSDKConfiguration.swift +++ b/Config/CryptoSDKConfiguration.swift @@ -16,8 +16,6 @@ import Foundation -#if DEBUG - /// Configuration for enabling / disabling Matrix Crypto SDK @objcMembers class CryptoSDKConfiguration: NSObject { static let shared = CryptoSDKConfiguration() @@ -29,6 +27,7 @@ import Foundation RiotSettings.shared.enableCryptoSDK = true MXSDKOptions.sharedInstance().enableCryptoSDK = true + MXSDKOptions.sharedInstance().enableStartupProgress = true MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK") } @@ -36,9 +35,8 @@ import Foundation func disable() { RiotSettings.shared.enableCryptoSDK = false MXSDKOptions.sharedInstance().enableCryptoSDK = false + MXSDKOptions.sharedInstance().enableStartupProgress = false MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK") } } - -#endif diff --git a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift index d802d54ff..4b314a0c1 100644 --- a/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift +++ b/Riot/Categories/MatrixSDKCrypto+LocalizedError.swift @@ -15,9 +15,6 @@ // import Foundation - -#if DEBUG - import MatrixSDKCrypto extension CryptoStoreError: LocalizedError { @@ -27,5 +24,3 @@ extension CryptoStoreError: LocalizedError { return VectorL10n.e2eNeedLogInAgain } } - -#endif diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index d9e64a1af..260a0aca7 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -192,11 +192,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults) var enableVoiceBroadcast - #if DEBUG /// Flag indicating if we are using rust-based `MatrixCryptoSDK` instead of `MatrixSDK`'s internal crypto module @UserDefault(key: "enableCryptoSDK", defaultValue: false, storage: defaults) var enableCryptoSDK - #endif // MARK: Calls diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 1fb151e20..302ba990e 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2184,9 +2184,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self clearCache]; // Reset Crypto SDK configuration (labs flag for which crypto module to use) -#if DEBUG [CryptoSDKConfiguration.shared disable]; -#endif // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 4b7ff566c..3e689a42e 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -885,10 +885,12 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { return } - let devices = mainSession.crypto.devices(forUser: mainSession.myUserId).values - let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) - if userHasOneUnverifiedDevice { - presentReviewUnverifiedSessionsAlert(with: session) + if let userId = mainSession.myUserId, let crypto = mainSession.crypto { + let devices = crypto.devices(forUser: userId).values + let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) + if userHasOneUnverifiedDevice { + presentReviewUnverifiedSessionsAlert(with: session) + } } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 5cf6e933b..93fffc9e8 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -588,12 +588,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - #if DEBUG if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable) { [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; } - #endif [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; @@ -2593,7 +2591,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } else { - #if DEBUG if (row == LABS_ENABLE_CRYPTO_SDK) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -2606,7 +2603,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = labelAndSwitchCell; } - #endif } } else if (section == SECTION_TAG_SECURITY) @@ -3379,7 +3375,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -#if DEBUG - (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender { BOOL isEnabled = sender.isOn; @@ -3407,7 +3402,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self presentViewController:confirmationAlert animated:YES completion:nil]; currentAlert = confirmationAlert; } -#endif - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index aca892844..6560290ab 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,9 +41,7 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? - #if DEBUG private var isCryptoSDKEnabled = false - #endif /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -201,6 +199,7 @@ class NotificationService: UNNotificationServiceExtension { if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() + NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in @@ -220,14 +219,11 @@ class NotificationService: UNNotificationServiceExtension { /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require /// rebuilding `MXBackgroundSyncService` private func hasChangedCryptoSDK() -> Bool { - #if DEBUG - if isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK { - isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK - return true + guard isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK else { + return false } - #endif - - return false + isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK + return true } /// Attempts to preprocess payload and attach room display name to the best attempt content diff --git a/changelog.d/pr-7333.change b/changelog.d/pr-7333.change new file mode 100644 index 000000000..fbf81e873 --- /dev/null +++ b/changelog.d/pr-7333.change @@ -0,0 +1 @@ +CryptoV2: Enable Crypto SDK for production From d800389ed3fff473217e2357d0252cae4efee201 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 17:53:35 +0100 Subject: [PATCH 307/468] Fix UTs --- RiotTests/PushRulesUpdaterTests.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift index c83cf233d..d56de081d 100644 --- a/RiotTests/PushRulesUpdaterTests.swift +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -42,12 +42,13 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) expectation.fulfill() } - waitForExpectations(timeout: 1.0) + + waitForExpectations(timeout: 2.0) } func testAffectedRulesAreUpdated() throws { @@ -59,7 +60,7 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { for rule in self.notificationService.rules { guard let id = rule.pushRuleId else { continue @@ -74,7 +75,7 @@ final class PushRulesUpdaterTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 1.0) + waitForExpectations(timeout: 2.0) } func testAffectedOneToOneRulesAreUpdated() throws { @@ -86,7 +87,7 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { for rule in self.notificationService.rules { guard let id = rule.pushRuleId else { continue @@ -101,7 +102,7 @@ final class PushRulesUpdaterTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 1.0) + waitForExpectations(timeout: 2.0) } } From 0bdde0e26c6dbacf32640746fd51ccebc83a2573 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 2 Feb 2023 14:31:36 +0000 Subject: [PATCH 308/468] Track crypto sdk being enabled --- Config/CommonConfiguration.swift | 4 ++-- Riot/Assets/en.lproj/Vector.strings | 6 +++--- Riot/Generated/Strings.swift | 6 +++--- Riot/Modules/Analytics/Analytics.swift | 16 ++++++++++++++++ Riot/Modules/Settings/SettingsViewController.m | 11 +++++------ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index a8b420b20..35001b1e4 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -94,11 +94,11 @@ class CommonConfiguration: NSObject, Configurable { if sdkOptions.isCryptoSDKAvailable { let isEnabled = RiotSettings.shared.enableCryptoSDK - MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + MXLog.debug("[CommonConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") sdkOptions.enableCryptoSDK = isEnabled sdkOptions.enableStartupProgress = isEnabled } else { - MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") + MXLog.debug("[CommonConfiguration] Crypto SDK is not available)") } } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 52d22bd50..abdc8a6e0 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -805,9 +805,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "End-to-end encryption 2.0"; -"settings_labs_confirm_crypto_sdk" = "This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed?"; -"settings_labs_disable_crypto_sdk" = "End-to-end encryption 2.0 (log out to disable)"; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; +"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 848458afb..de1b5a22e 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7583,7 +7583,7 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed? + /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. public static var settingsLabsConfirmCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") } @@ -7591,7 +7591,7 @@ public class VectorL10n: NSObject { public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// End-to-end encryption 2.0 (log out to disable) + /// Rust end-to-end encryption (log out to disable) public static var settingsLabsDisableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") } @@ -7607,7 +7607,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// End-to-end encryption 2.0 + /// Rust end-to-end encryption public static var settingsLabsEnableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") } diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index b608c862e..60e1560b9 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -324,6 +324,11 @@ extension Analytics { viewRoomTrigger = .unknown capture(event: event) } + + func trackCryptoSDKEnabled() { + let event = AnalyticsEvent.CryptoSDKEnabled() + capture(event: event) + } } // MARK: - MXAnalyticsDelegate @@ -393,3 +398,14 @@ extension Analytics: MXAnalyticsDelegate { monitoringClient.trackNonFatalIssue(issue, details: details) } } + +/// iOS-specific analytics event triggered when users select the Crypto SDK labs option +/// +/// Due to this event being iOS only, and temporary during gradual rollout of Crypto SDK, +/// this event is not added into the shared analytics schema +extension AnalyticsEvent { + struct CryptoSDKEnabled: AnalyticsEventProtocol { + let eventName = "CryptoSDKEnabled" + let properties: [String: Any] = [:] + } +} diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 93fffc9e8..ea8b57b22 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2599,7 +2599,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> labelAndSwitchCell.mxkSwitch.on = isEnabled; [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; cell = labelAndSwitchCell; } @@ -3375,16 +3375,14 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -- (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender +- (void)enableCryptoSDKFeature:(UISwitch *)sender { - BOOL isEnabled = sender.isOn; - MXWeakify(self); - [currentAlert dismissViewControllerAnimated:NO completion:nil]; UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk message:VectorL10n.settingsLabsConfirmCryptoSdk preferredStyle:UIAlertControllerStyleAlert]; + MXWeakify(self); [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); self->currentAlert = nil; @@ -3393,9 +3391,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> }]]; [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); [CryptoSDKConfiguration.shared enable]; + [Analytics.shared trackCryptoSDKEnabled]; + [[AppDelegate theDelegate] reloadMatrixSessions:YES]; }]]; From 0caab633424ef99b3d34fae5553e314df00dabad Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Thu, 2 Feb 2023 14:31:36 +0000 Subject: [PATCH 309/468] Track crypto sdk being enabled --- Config/CommonConfiguration.swift | 4 ++-- Riot/Assets/en.lproj/Vector.strings | 6 +++--- Riot/Generated/Strings.swift | 6 +++--- Riot/Modules/Analytics/Analytics.swift | 16 ++++++++++++++++ Riot/Modules/Settings/SettingsViewController.m | 11 +++++------ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index a8b420b20..35001b1e4 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -94,11 +94,11 @@ class CommonConfiguration: NSObject, Configurable { if sdkOptions.isCryptoSDKAvailable { let isEnabled = RiotSettings.shared.enableCryptoSDK - MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") + MXLog.debug("[CommonConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") sdkOptions.enableCryptoSDK = isEnabled sdkOptions.enableStartupProgress = isEnabled } else { - MXLog.debug("[CryptoSDKConfiguration] Crypto SDK is not available)") + MXLog.debug("[CommonConfiguration] Crypto SDK is not available)") } } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 4d56129eb..3b2871460 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -804,9 +804,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "End-to-end encryption 2.0"; -"settings_labs_confirm_crypto_sdk" = "This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed?"; -"settings_labs_disable_crypto_sdk" = "End-to-end encryption 2.0 (log out to disable)"; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; +"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 47f6a77ae..8c676d666 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7587,7 +7587,7 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// This option will enable a new, faster and more reliable engine for end-to-end encryption written in Rust. Once enabled, you will need to log out to disable it. Do you wish to proceed? + /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. public static var settingsLabsConfirmCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") } @@ -7595,7 +7595,7 @@ public class VectorL10n: NSObject { public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// End-to-end encryption 2.0 (log out to disable) + /// Rust end-to-end encryption (log out to disable) public static var settingsLabsDisableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") } @@ -7611,7 +7611,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// End-to-end encryption 2.0 + /// Rust end-to-end encryption public static var settingsLabsEnableCryptoSdk: String { return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") } diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index b608c862e..60e1560b9 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -324,6 +324,11 @@ extension Analytics { viewRoomTrigger = .unknown capture(event: event) } + + func trackCryptoSDKEnabled() { + let event = AnalyticsEvent.CryptoSDKEnabled() + capture(event: event) + } } // MARK: - MXAnalyticsDelegate @@ -393,3 +398,14 @@ extension Analytics: MXAnalyticsDelegate { monitoringClient.trackNonFatalIssue(issue, details: details) } } + +/// iOS-specific analytics event triggered when users select the Crypto SDK labs option +/// +/// Due to this event being iOS only, and temporary during gradual rollout of Crypto SDK, +/// this event is not added into the shared analytics schema +extension AnalyticsEvent { + struct CryptoSDKEnabled: AnalyticsEventProtocol { + let eventName = "CryptoSDKEnabled" + let properties: [String: Any] = [:] + } +} diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 93fffc9e8..ea8b57b22 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2599,7 +2599,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> labelAndSwitchCell.mxkSwitch.on = isEnabled; [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; cell = labelAndSwitchCell; } @@ -3375,16 +3375,14 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -- (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender +- (void)enableCryptoSDKFeature:(UISwitch *)sender { - BOOL isEnabled = sender.isOn; - MXWeakify(self); - [currentAlert dismissViewControllerAnimated:NO completion:nil]; UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk message:VectorL10n.settingsLabsConfirmCryptoSdk preferredStyle:UIAlertControllerStyleAlert]; + MXWeakify(self); [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); self->currentAlert = nil; @@ -3393,9 +3391,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> }]]; [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); [CryptoSDKConfiguration.shared enable]; + [Analytics.shared trackCryptoSDKEnabled]; + [[AppDelegate theDelegate] reloadMatrixSessions:YES]; }]]; From 16b124abf38e5a22478f015ccf94216f7136109c Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Feb 2023 18:05:50 +0000 Subject: [PATCH 310/468] changelog.d: Upgrade MatrixSDK version ([v0.25.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.0)). --- Config/AppVersion.xcconfig | 4 ++-- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 210603b23..d203e4e63 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.17 -CURRENT_PROJECT_VERSION = 1.9.17 +MARKETING_VERSION = 1.10.0 +CURRENT_PROJECT_VERSION = 1.10.0 diff --git a/Podfile b/Podfile index 35ba935b2..376ec852a 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.8' +$matrixSDKVersion = '= 0.25.0' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..497d15527 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.25.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.0)). \ No newline at end of file From d8bb6fc23155dd9b8e53913c3a4acf7a718b2be4 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Feb 2023 18:05:51 +0000 Subject: [PATCH 311/468] version++ --- CHANGES.md | 12 ++++++++++++ changelog.d/pr-7310.change | 1 - changelog.d/pr-7319.change | 1 - changelog.d/pr-7323.change | 1 - changelog.d/pr-7332.change | 1 - changelog.d/pr-7333.change | 1 - changelog.d/x-nolink-0.change | 1 - 7 files changed, 12 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/pr-7310.change delete mode 100644 changelog.d/pr-7319.change delete mode 100644 changelog.d/pr-7323.change delete mode 100644 changelog.d/pr-7332.change delete mode 100644 changelog.d/pr-7333.change delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index 3484fcf48..c24df14b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +## Changes in 1.10.0 (2023-02-02) + +🙌 Improvements + +- CryptoV2: Generate Crypto SDK store key ([#7310](https://github.com/vector-im/element-ios/pull/7310)) +- Backup: Display backup import progress ([#7319](https://github.com/vector-im/element-ios/pull/7319)) +- CryptoV2: Reset Crypto SDK on logout ([#7323](https://github.com/vector-im/element-ios/pull/7323)) +- CryptoV2: Refresh notification service on crypto change ([#7332](https://github.com/vector-im/element-ios/pull/7332)) +- CryptoV2: Enable Crypto SDK for production ([#7333](https://github.com/vector-im/element-ios/pull/7333)) +- Upgrade MatrixSDK version ([v0.25.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.0)). + + ## Changes in 1.9.17 (2023-01-26) 🙌 Improvements diff --git a/changelog.d/pr-7310.change b/changelog.d/pr-7310.change deleted file mode 100644 index 4ba5e9ee1..000000000 --- a/changelog.d/pr-7310.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Generate Crypto SDK store key diff --git a/changelog.d/pr-7319.change b/changelog.d/pr-7319.change deleted file mode 100644 index 187b315b5..000000000 --- a/changelog.d/pr-7319.change +++ /dev/null @@ -1 +0,0 @@ -Backup: Display backup import progress diff --git a/changelog.d/pr-7323.change b/changelog.d/pr-7323.change deleted file mode 100644 index 308cf2813..000000000 --- a/changelog.d/pr-7323.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Reset Crypto SDK on logout diff --git a/changelog.d/pr-7332.change b/changelog.d/pr-7332.change deleted file mode 100644 index 94a5bdc89..000000000 --- a/changelog.d/pr-7332.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Refresh notification service on crypto change diff --git a/changelog.d/pr-7333.change b/changelog.d/pr-7333.change deleted file mode 100644 index fbf81e873..000000000 --- a/changelog.d/pr-7333.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Enable Crypto SDK for production diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 497d15527..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.25.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.0)). \ No newline at end of file From b13baeb0dea094d142ef1aea62d4813330f4dc11 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 18:58:49 +0100 Subject: [PATCH 312/468] Refactor unit tests --- .../PushRulesUpdater/PushRulesUpdater.swift | 45 ++++++----- RiotTests/PushRulesUpdaterTests.swift | 74 +++++++++++-------- 2 files changed, 71 insertions(+), 48 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 8e984f3b2..90bd221ad 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -20,6 +20,11 @@ final class PushRulesUpdater { private var cancellables: Set = .init() private var rules: [NotificationPushRuleType] = [] private let notificationSettingsService: NotificationSettingsServiceType + private let didCompleteUpdateSubject: PassthroughSubject = .init() + + var didCompleteUpdate: AnyPublisher { + didCompleteUpdateSubject.eraseToAnyPublisher() + } init(notificationSettingsService: NotificationSettingsServiceType, needsCheck: AnyPublisher) { self.notificationSettingsService = notificationSettingsService @@ -39,31 +44,35 @@ final class PushRulesUpdater { private extension PushRulesUpdater { func syncRulesIfNeeded() { + let dispatchGroup: DispatchGroup = .init() + for rule in rules { - syncRelatedRulesIfNeeded(for: rule) - } - } - - func syncRelatedRulesIfNeeded(for rule: NotificationPushRuleType) { - guard let ruleId = rule.pushRuleId else { - return - } - - let relatedRules = ruleId.syncedRules(in: rules) - - for relatedRule in relatedRules { - guard rule.hasSameContentOf(relatedRule) == false else { + guard let ruleId = rule.pushRuleId else { continue } - sync(relatedRuleId: relatedRule.ruleId, with: rule) + let relatedRules = ruleId.syncedRules(in: rules) + + for relatedRule in relatedRules { + guard rule.hasSameContentOf(relatedRule) == false else { + continue + } + + dispatchGroup.enter() + Task { + try? await sync(relatedRuleId: relatedRule.ruleId, with: rule) + dispatchGroup.leave() + } + } + } + + dispatchGroup.notify(queue: .main) { [weak self] in + self?.didCompleteUpdateSubject.send(()) } } - func sync(relatedRuleId: String, with rule: NotificationPushRuleType) { - Task { - try? await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: rule.ruleActions) - } + func sync(relatedRuleId: String, with rule: NotificationPushRuleType) async throws { + try await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: rule.ruleActions) } } diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift index d56de081d..418efbe67 100644 --- a/RiotTests/PushRulesUpdaterTests.swift +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -22,12 +22,17 @@ final class PushRulesUpdaterTests: XCTestCase { private var notificationService: MockNotificationSettingsService! private var pushRulesUpdater: PushRulesUpdater! private var needsCheckPublisher: PassthroughSubject = .init() + private var subscriptions: Set = .init() override func setUpWithError() throws { notificationService = .init() notificationService.rules = [MockNotificationPushRule].default pushRulesUpdater = .init(notificationSettingsService: notificationService, needsCheck: needsCheckPublisher.eraseOutput()) } + + override func tearDownWithError() throws { + subscriptions.removeAll() + } func testNoRuleIsUpdated() throws { needsCheckPublisher.send() @@ -42,11 +47,14 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) - XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) - expectation.fulfill() - } + pushRulesUpdater + .didCompleteUpdate + .sink { _ in + XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) + expectation.fulfill() + } + .store(in: &subscriptions) waitForExpectations(timeout: 2.0) } @@ -60,21 +68,24 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - for rule in self.notificationService.rules { - guard let id = rule.pushRuleId else { - continue - } - - if affectedRules.contains(id) { - XCTAssertEqual(rule.ruleActions, targetActions) - } else { - XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + pushRulesUpdater + .didCompleteUpdate + .sink { _ in + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } } + expectation.fulfill() } - expectation.fulfill() - } - + .store(in: &subscriptions) + waitForExpectations(timeout: 2.0) } @@ -87,20 +98,23 @@ final class PushRulesUpdaterTests: XCTestCase { needsCheckPublisher.send(()) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - for rule in self.notificationService.rules { - guard let id = rule.pushRuleId else { - continue - } - - if affectedRules.contains(id) { - XCTAssertEqual(rule.ruleActions, targetActions) - } else { - XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + pushRulesUpdater + .didCompleteUpdate + .sink { _ in + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue + } + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } } + expectation.fulfill() } - expectation.fulfill() - } + .store(in: &subscriptions) waitForExpectations(timeout: 2.0) } From c8cbf9ceecc66b65e1f63ea8d179edb38331be45 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Feb 2023 19:20:46 +0000 Subject: [PATCH 313/468] finish version++ --- Podfile.lock | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 24b937005..a8c7f04f8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -55,22 +55,20 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.24.8): - - MatrixSDK/Core (= 0.24.8) - - MatrixSDK/Core (0.24.8): + - MatrixSDK (0.25.0): + - MatrixSDK/Core (= 0.25.0) + - MatrixSDK/Core (0.25.0): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDK/CryptoSDK + - MatrixSDKCrypto (= 0.2.0) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.24.8): - - MatrixSDKCrypto (= 0.1.8) - - MatrixSDK/JingleCallStack (0.24.8): + - MatrixSDK/JingleCallStack (0.25.0): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.1.8) + - MatrixSDKCrypto (0.2.0) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) @@ -122,8 +120,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.24.8) - - MatrixSDK/JingleCallStack (= 0.24.8) + - MatrixSDK (= 0.25.0) + - MatrixSDK/JingleCallStack (= 0.25.0) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -220,8 +218,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: cf1c1b2a9742f7f4fad21e94bd94cd8f13c47369 - MatrixSDKCrypto: 862d9b4dbb6861da030943f5a18c39258ed7345b + MatrixSDK: a9d05e760434eff941bbb35164cffb01b3f94b63 + MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -241,6 +239,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 079b57b800c666ad864e1f059ae69e150a98a4f0 +PODFILE CHECKSUM: 916221b3e9512715d5e1e1e310a0aa0552e1f0f1 COCOAPODS: 1.11.3 From c0310bc98b2bf0f2bc01834e240abefe10aa0da1 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Feb 2023 19:27:53 +0000 Subject: [PATCH 314/468] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index d203e4e63..f1efc9679 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.0 -CURRENT_PROJECT_VERSION = 1.10.0 +MARKETING_VERSION = 1.10.1 +CURRENT_PROJECT_VERSION = 1.10.1 From f1372542b77f6f697bd68186aae6a6a1069117d8 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 2 Feb 2023 22:42:41 +0100 Subject: [PATCH 315/468] DispatchGroup to TaskGroup refactor --- .../PushRulesUpdater/PushRulesUpdater.swift | 50 +++++++++---------- .../MXNotificationSettingsService.swift | 8 +-- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 90bd221ad..50620ecaf 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -44,35 +44,33 @@ final class PushRulesUpdater { private extension PushRulesUpdater { func syncRulesIfNeeded() { - let dispatchGroup: DispatchGroup = .init() - - for rule in rules { - guard let ruleId = rule.pushRuleId else { - continue - } - - let relatedRules = ruleId.syncedRules(in: rules) - - for relatedRule in relatedRules { - guard rule.hasSameContentOf(relatedRule) == false else { - continue - } - - dispatchGroup.enter() - Task { - try? await sync(relatedRuleId: relatedRule.ruleId, with: rule) - dispatchGroup.leave() + Task { + await withTaskGroup(of: Void.self) { [rules, notificationSettingsService] group in + for rule in rules { + guard let ruleId = rule.pushRuleId else { + continue + } + + let relatedRules = ruleId.syncedRules(in: rules) + + for relatedRule in relatedRules { + guard rule.hasSameContentOf(relatedRule) == false else { + continue + } + + group.addTask { + try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, + enabled: rule.enabled, + actions: rule.ruleActions) + } + } } } + + await MainActor.run { [weak self] in + self?.didCompleteUpdateSubject.send(()) + } } - - dispatchGroup.notify(queue: .main) { [weak self] in - self?.didCompleteUpdateSubject.send(()) - } - } - - func sync(relatedRuleId: String, with rule: NotificationPushRuleType) async throws { - try await notificationSettingsService.updatePushRuleActions(for: relatedRuleId, enabled: rule.enabled, actions: rule.ruleActions) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index 6ccd54c1a..9bf01ef4c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -89,10 +89,10 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { // Updating the actions before enabling the rule allows the homeserver to triggers just one sync update try await session.notificationCenter.updatePushRuleActions(ruleId, - kind: rule.kind, - notify: actions.notify, - soundName: actions.sound, - highlight: actions.highlight) + kind: rule.kind, + notify: actions.notify, + soundName: actions.sound, + highlight: actions.highlight) try await session.notificationCenter.enableRule(pushRule: rule, isEnabled: enabled) } From b259f9ed0d8c1e4322509cd626d92b2eea5c13a7 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 3 Feb 2023 10:49:59 +0100 Subject: [PATCH 316/468] Refactor PushRulesUpdater API --- .../PushRulesUpdater/PushRulesUpdater.swift | 55 ++++------ Riot/Modules/Application/AppCoordinator.swift | 24 +++-- RiotTests/PushRulesUpdaterTests.swift | 100 ++++++------------ 3 files changed, 68 insertions(+), 111 deletions(-) diff --git a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift index 50620ecaf..a0ffea92f 100644 --- a/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift +++ b/Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift @@ -20,56 +20,37 @@ final class PushRulesUpdater { private var cancellables: Set = .init() private var rules: [NotificationPushRuleType] = [] private let notificationSettingsService: NotificationSettingsServiceType - private let didCompleteUpdateSubject: PassthroughSubject = .init() - - var didCompleteUpdate: AnyPublisher { - didCompleteUpdateSubject.eraseToAnyPublisher() - } - - init(notificationSettingsService: NotificationSettingsServiceType, needsCheck: AnyPublisher) { + + init(notificationSettingsService: NotificationSettingsServiceType) { self.notificationSettingsService = notificationSettingsService notificationSettingsService .rulesPublisher .weakAssign(to: \.rules, on: self) .store(in: &cancellables) - - needsCheck - .sink { [weak self] _ in - self?.syncRulesIfNeeded() - } - .store(in: &cancellables) } -} - -private extension PushRulesUpdater { - func syncRulesIfNeeded() { - Task { - await withTaskGroup(of: Void.self) { [rules, notificationSettingsService] group in - for rule in rules { - guard let ruleId = rule.pushRuleId else { + + func syncRulesIfNeeded() async { + await withTaskGroup(of: Void.self) { [rules, notificationSettingsService] group in + for rule in rules { + guard let ruleId = rule.pushRuleId else { + continue + } + + let relatedRules = ruleId.syncedRules(in: rules) + + for relatedRule in relatedRules { + guard rule.hasSameContentOf(relatedRule) == false else { continue } - let relatedRules = ruleId.syncedRules(in: rules) - - for relatedRule in relatedRules { - guard rule.hasSameContentOf(relatedRule) == false else { - continue - } - - group.addTask { - try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, - enabled: rule.enabled, - actions: rule.ruleActions) - } + group.addTask { + try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId, + enabled: rule.enabled, + actions: rule.ruleActions) } } } - - await MainActor.run { [weak self] in - self?.didCompleteUpdateSubject.send(()) - } } } } diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index a53807db5..5bf34b7c8 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -87,7 +87,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { setupLogger() setupTheme() excludeAllItemsFromBackup() - setupPushRulesSync() + setupPushRulesSessionEvents() // Setup navigation router store _ = NavigationRouterStore.shared @@ -264,7 +264,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { self.splitViewCoordinator?.start(with: spaceId) } - private func setupPushRulesSync() { + private func setupPushRulesSessionEvents() { let sessionReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange) .compactMap { $0.object as? MXSession } .filter { $0.state == .running } @@ -274,10 +274,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { sessionReady .sink { [weak self] session in - let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() - let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher() - - self?.pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session), needsCheck: needsCheckPublisher) + self?.setupPushRulesUpdater(session: session) } .store(in: &cancellables) @@ -292,6 +289,21 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } .store(in: &cancellables) } + + private func setupPushRulesUpdater(session: MXSession) { + pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session)) + + let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput() + let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher() + + needsCheckPublisher + .sink { _ in + Task { @MainActor [weak self] in + await self?.pushRulesUpdater?.syncRulesIfNeeded() + } + } + .store(in: &cancellables) + } } // MARK: - LegacyAppDelegateDelegate diff --git a/RiotTests/PushRulesUpdaterTests.swift b/RiotTests/PushRulesUpdaterTests.swift index 418efbe67..1eec9dda5 100644 --- a/RiotTests/PushRulesUpdaterTests.swift +++ b/RiotTests/PushRulesUpdaterTests.swift @@ -21,102 +21,66 @@ import XCTest final class PushRulesUpdaterTests: XCTestCase { private var notificationService: MockNotificationSettingsService! private var pushRulesUpdater: PushRulesUpdater! - private var needsCheckPublisher: PassthroughSubject = .init() - private var subscriptions: Set = .init() override func setUpWithError() throws { notificationService = .init() notificationService.rules = [MockNotificationPushRule].default - pushRulesUpdater = .init(notificationSettingsService: notificationService, needsCheck: needsCheckPublisher.eraseOutput()) - } - - override func tearDownWithError() throws { - subscriptions.removeAll() + pushRulesUpdater = .init(notificationSettingsService: notificationService) } - func testNoRuleIsUpdated() throws { - needsCheckPublisher.send() + func testNoRuleIsUpdated() async throws { + await pushRulesUpdater.syncRulesIfNeeded() XCTAssertEqual(notificationService.rules as? [MockNotificationPushRule], [MockNotificationPushRule].default) } - func testSingleRuleAffected() throws { - let expectation = expectation(description: #function) - + func testSingleRuleAffected() async throws { let targetActions: NotificationActions = .init(notify: true, sound: "default") let targetRuleIndex = try mockRule(ruleId: .pollStart, enabled: false, actions: targetActions) - needsCheckPublisher.send(()) - - pushRulesUpdater - .didCompleteUpdate - .sink { _ in - XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) - XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) - expectation.fulfill() - } - .store(in: &subscriptions) - - waitForExpectations(timeout: 2.0) + await pushRulesUpdater.syncRulesIfNeeded() + + XCTAssertEqual(self.notificationService.rules[targetRuleIndex].ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + XCTAssertTrue(self.notificationService.rules[targetRuleIndex].enabled) } - func testAffectedRulesAreUpdated() throws { - let expectation = expectation(description: #function) - + func testAffectedRulesAreUpdated() async throws { let targetActions: NotificationActions = .init(notify: true, sound: "abc") try mockRule(ruleId: .allOtherMessages, enabled: true, actions: targetActions) let affectedRules: [NotificationPushRuleId] = [.allOtherMessages, .pollStart, .msc3930pollStart, .pollEnd, .msc3930pollEnd] - needsCheckPublisher.send(()) + await pushRulesUpdater.syncRulesIfNeeded() - pushRulesUpdater - .didCompleteUpdate - .sink { _ in - for rule in self.notificationService.rules { - guard let id = rule.pushRuleId else { - continue - } - - if affectedRules.contains(id) { - XCTAssertEqual(rule.ruleActions, targetActions) - } else { - XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) - } - } - expectation.fulfill() + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue } - .store(in: &subscriptions) - - waitForExpectations(timeout: 2.0) + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } } - func testAffectedOneToOneRulesAreUpdated() throws { - let expectation = expectation(description: #function) - + func testAffectedOneToOneRulesAreUpdated() async throws { let targetActions: NotificationActions = .init(notify: true, sound: "abc") try mockRule(ruleId: .oneToOneRoom, enabled: true, actions: targetActions) let affectedRules: [NotificationPushRuleId] = [.oneToOneRoom, .oneToOnePollStart, .msc3930oneToOnePollStart, .oneToOnePollEnd, .msc3930oneToOnePollEnd] - needsCheckPublisher.send(()) + await pushRulesUpdater.syncRulesIfNeeded() - pushRulesUpdater - .didCompleteUpdate - .sink { _ in - for rule in self.notificationService.rules { - guard let id = rule.pushRuleId else { - continue - } - - if affectedRules.contains(id) { - XCTAssertEqual(rule.ruleActions, targetActions) - } else { - XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) - } - } - expectation.fulfill() + for rule in self.notificationService.rules { + guard let id = rule.pushRuleId else { + continue } - .store(in: &subscriptions) - - waitForExpectations(timeout: 2.0) + + if affectedRules.contains(id) { + XCTAssertEqual(rule.ruleActions, targetActions) + } else { + XCTAssertEqual(rule.ruleActions, NotificationStandardActions.notifyDefaultSound.actions) + } + } } } From 18d202f3bd5404fd1dc70964cc194aa1d3fa0c69 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 6 Feb 2023 12:36:31 +0100 Subject: [PATCH 317/468] Improve SegmentedPicker UI --- .../PollHistory/View/SegmentedPicker.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift index 520a649c7..14b53d644 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift @@ -39,7 +39,7 @@ struct SegmentedPicker: View { } label: { Text(segment.description) .font(isSelectedSegment ? theme.fonts.headline : theme.fonts.body) - .underline(isSelectedSegment) + .underlineBar(isSelectedSegment) } .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent) .accessibilityLabel(segment.description) @@ -49,6 +49,23 @@ struct SegmentedPicker: View { } } +private extension Text { + @ViewBuilder + func underlineBar(_ isActive: Bool) -> some View { + if #available(iOS 15.0, *) { + overlay(alignment: .bottom) { + if isActive { + Rectangle() + .frame(height: 1) + .offset(y: 2) + } + } + } else { + underline(isActive) + } + } +} + struct SegmentedPicker_Previews: PreviewProvider { static var previews: some View { SegmentedPicker( From d96c7f9ec1b65c86c095a476eff778e5cca9c224 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Mon, 6 Feb 2023 12:42:34 +0100 Subject: [PATCH 318/468] Add changelog.d file --- changelog.d/pr-7341.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7341.change diff --git a/changelog.d/pr-7341.change b/changelog.d/pr-7341.change new file mode 100644 index 000000000..2129cec32 --- /dev/null +++ b/changelog.d/pr-7341.change @@ -0,0 +1 @@ +Polls: update poll history UI. From 2acd962a704e58c4e0777760f4ed3a28e2c6b935 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 6 Feb 2023 18:11:24 +0100 Subject: [PATCH 319/468] Hide the presence info if the presence status is unknown --- Riot/Utils/Tools.m | 3 +++ changelog.d/6597.change | 1 + 2 files changed, 4 insertions(+) create mode 100644 changelog.d/6597.change diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index 77ae6837e..ad2eab90d 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -37,6 +37,9 @@ break; case MXPresenceUnknown: // Do like matrix-js-sdk + presenceText = @""; + break; + case MXPresenceOffline: presenceText = [VectorL10n roomParticipantsOffline]; break; diff --git a/changelog.d/6597.change b/changelog.d/6597.change new file mode 100644 index 000000000..46ca176a7 --- /dev/null +++ b/changelog.d/6597.change @@ -0,0 +1 @@ +Hide the presence info if the presence status is unknown. From 72902f77af054c27b6292a890ababd8ef10bdeb7 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 7 Feb 2023 11:15:26 +0100 Subject: [PATCH 320/468] Cleanup --- Riot/Utils/Tools.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index ad2eab90d..128fe3694 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -36,9 +36,10 @@ presenceText = [VectorL10n roomParticipantsIdle]; break; - case MXPresenceUnknown: // Do like matrix-js-sdk - presenceText = @""; - break; + case MXPresenceUnknown: + // Fix https://github.com/vector-im/element-ios/issues/6597 + // Return nil because we don't want to display anything if the status is unknown + return nil; case MXPresenceOffline: presenceText = [VectorL10n roomParticipantsOffline]; From c67b813876a7858488ce3f0e24f25dd00de5995d Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 18:21:24 +0000 Subject: [PATCH 321/468] Translated using Weblate (Japanese) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/ja/ --- Riot/Assets/ja.lproj/Localizable.strings | 85 ++++++++++++------------ 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/Riot/Assets/ja.lproj/Localizable.strings b/Riot/Assets/ja.lproj/Localizable.strings index 4a63be21b..c76236c89 100644 --- a/Riot/Assets/ja.lproj/Localizable.strings +++ b/Riot/Assets/ja.lproj/Localizable.strings @@ -1,9 +1,9 @@ /* New message from a specific person, not referencing a room */ -"MSG_FROM_USER" = "%@ さんからメッセージ"; +"MSG_FROM_USER" = "%@さんがメッセージを送信しました"; /* New message from a specific person in a named room */ -"MSG_FROM_USER_IN_ROOM" = "%@ さんが %@ へ発言"; +"MSG_FROM_USER_IN_ROOM" = "%@さんが%@に投稿しました"; /* New message from a specific person, not referencing a room. Content included. */ -"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; +"MSG_FROM_USER_WITH_CONTENT" = "%@:%@"; /* New message from a specific person in a named room. Content included. */ "MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ in %@: %@"; /* New action message from a specific person, not referencing a room. */ @@ -12,62 +12,62 @@ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ /* New action message from a specific person in a named room. */ -"IMAGE_FROM_USER_IN_ROOM" = "%@ さんが写真を投稿 %@ in %@"; +"IMAGE_FROM_USER_IN_ROOM" = "%@さんが写真%@を%@に投稿しました"; /* Multiple unread messages in a room */ -"UNREAD_IN_ROOM" = "%@ 新しいメッセージ in %@"; +"UNREAD_IN_ROOM" = "%@件の新しいメッセージが%@にあります"; /* Multiple unread messages from a specific person, not referencing a room */ -"MSGS_FROM_USER" = "%@ 新しいメッセージ in %@"; +"MSGS_FROM_USER" = "%@件の新しいメッセージが%@にあります"; /* Multiple unread messages from two people */ -"MSGS_FROM_TWO_USERS" = "%@ 新しいメッセージ from %@ and %@"; +"MSGS_FROM_TWO_USERS" = "%@件の新しいメッセージを%@と%@から受信しました"; /* Multiple unread messages from three people */ -"MSGS_FROM_THREE_USERS" = "%@ 新しいメッセージ from %@, %@ and %@"; +"MSGS_FROM_THREE_USERS" = "%@件の新しいメッセージを%@、%@、%@から受信しました"; /* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ -"MSGS_FROM_TWO_PLUS_USERS" = "%@ 新しいメッセージ from %@, %@ 他"; +"MSGS_FROM_TWO_PLUS_USERS" = "%@件の新しいメッセージを%@、%@、ほか数人から受信しました"; /* Multiple messages in two rooms */ -"MSGS_IN_TWO_ROOMS" = "%@ 新しいメッセージ in %@ and %@"; +"MSGS_IN_TWO_ROOMS" = "%@件の新しいメッセージが%@と%@にあります"; /* Look, stuff's happened, alright? Just open the app. */ -"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 新しいメッセージ in %@, %@ 他"; +"MSGS_IN_TWO_PLUS_ROOMS" = "%@件の新しいメッセージが%@、%@などにあります"; /* A user has invited you to a chat */ -"USER_INVITE_TO_CHAT" = "%@ さんがあなたを対話に招待しました"; +"USER_INVITE_TO_CHAT" = "%@さんがあなたをチャットに招待しました"; /* A user has invited you to an (unamed) group chat */ -"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ さんがあなたをルームへ招待しました"; +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@さんがあなたをグループチャットに招待しました"; /* A user has invited you to a named room */ -"USER_INVITE_TO_NAMED_ROOM" = "%@ さんがルーム %@ へ招待しました"; +"USER_INVITE_TO_NAMED_ROOM" = "%@さんがルーム %@ に招待しました"; /* Incoming one-to-one voice call */ -"VOICE_CALL_FROM_USER" = "%@ さんから通話着信"; +"VOICE_CALL_FROM_USER" = "%@さんから通話着信"; /* Incoming one-to-one video call */ -"VIDEO_CALL_FROM_USER" = "%@ さんから映像つき通話着信"; +"VIDEO_CALL_FROM_USER" = "%@さんからビデオ通話の着信"; /* Incoming unnamed voice conference invite from a specific person */ -"VOICE_CONF_FROM_USER" = "%@ さんから会議通話の着信"; +"VOICE_CONF_FROM_USER" = "%@さんからグループ通話の着信"; /* Incoming unnamed video conference invite from a specific person */ -"VIDEO_CONF_FROM_USER" = "%@ さんから映像つき会議通話の着信"; +"VIDEO_CONF_FROM_USER" = "%@さんからビデオグループ通話の着信"; /* Incoming named voice conference invite from a specific person */ -"VOICE_CONF_NAMED_FROM_USER" = "会議通話の着信 from %@: '%@'"; +"VOICE_CONF_NAMED_FROM_USER" = "%@さんからグループ通話の着信:'%@'"; /* Incoming named video conference invite from a specific person */ -"VIDEO_CONF_NAMED_FROM_USER" = "映像つき会議通話の着信 from %@: '%@'"; +"VIDEO_CONF_NAMED_FROM_USER" = "%@さんからビデオグループ通話の着信:'%@'"; /* A single unread message in a room */ -"SINGLE_UNREAD_IN_ROOM" = "%@にメッセージを受け取りました"; +"SINGLE_UNREAD_IN_ROOM" = "%@でメッセージを受信しました"; /* A single unread message */ -"SINGLE_UNREAD" = "あなたはメッセージを受け取りました"; +"SINGLE_UNREAD" = "メッセージを受信しました"; /** Key verification **/ "KEY_VERIFICATION_REQUEST_FROM_USER" = "%@は認証を要求しています"; /* New message indicator on a room */ -"MESSAGE_IN_X" = "%@ 内のメッセージ"; +"MESSAGE_IN_X" = "%@内のメッセージ"; /* Sticker from a specific person, not referencing a room. */ -"STICKER_FROM_USER" = "%@ さんからのスタンプ"; +"STICKER_FROM_USER" = "%@さんがステッカーを送信しました"; /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@(%@ から)"; /* Group call from user, CallKit caller name */ -"GROUP_CALL_FROM_USER" = "%@ (グループ通話)"; +"GROUP_CALL_FROM_USER" = "%@(グループ通話)"; "MESSAGE_PROTECTED" = "新しいメッセージ"; /* New message indicator from a DM */ -"MESSAGE_FROM_X" = "%@ からのメッセージ"; +"MESSAGE_FROM_X" = "%@さんからのメッセージ"; /** Notification messages **/ @@ -78,52 +78,55 @@ "Notification" = "通知"; /* New message reply from a specific person in a named room. */ -"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ さんが %@ で返信"; +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@さんが%@で返信しました"; /* New message reply from a specific person, not referencing a room. */ -"REPLY_FROM_USER_TITLE" = "%@ さんが返信"; +"REPLY_FROM_USER_TITLE" = "%@さんが返信しました"; /** Reactions **/ /* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ -"REACTION_FROM_USER" = "%@ さんが %@ とリアクション"; +"REACTION_FROM_USER" = "%@さんが%@でリアクションしました"; /* A user has reacted to a message, but the reaction content is unknown */ -"GENERIC_REACTION_FROM_USER" = "%@ さんがリアクション"; +"GENERIC_REACTION_FROM_USER" = "%@さんがリアクションを送信しました"; /* New file message from a specific person, not referencing a room. */ -"LOCATION_FROM_USER" = "%@ さんが位置情報を共有"; +"LOCATION_FROM_USER" = "%@さんが位置情報を共有しました"; /* New voice message from a specific person, not referencing a room. */ -"VOICE_MESSAGE_FROM_USER" = "%@ さんが音声メッセージを送信"; +"VOICE_MESSAGE_FROM_USER" = "%@さんが音声メッセージを送信しました"; /* New video message from a specific person, not referencing a room. */ -"VIDEO_FROM_USER" = "%@ さんが動画を送信"; +"VIDEO_FROM_USER" = "%@さんが動画を送信しました"; /** Media Messages **/ /* New image message from a specific person, not referencing a room. */ -"PICTURE_FROM_USER" = "%@ さんが写真を送信"; +"PICTURE_FROM_USER" = "%@さんが写真を送信しました"; /* A user added a Jitsi call to a room */ -"GROUP_CALL_STARTED" = "グループ通話が開始されました"; +"GROUP_CALL_STARTED" = "グループ通話を開始しました"; /* A user's membership has updated in an unknown way */ -"USER_MEMBERSHIP_UPDATED" = "%@ がプロフィールを更新しました"; +"USER_MEMBERSHIP_UPDATED" = "%@さんがプロフィールを更新しました"; /* A user has change their avatar */ -"USER_UPDATED_AVATAR" = "%@ がアバター画像を変更しました"; +"USER_UPDATED_AVATAR" = "%@さんがアバターを変更しました"; /* A user has change their name to a new name which we don't know */ -"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ が名前を変更しました"; +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@さんが名前を変更しました"; /** Membership Updates **/ /* A user has change their name to a new name */ -"USER_UPDATED_DISPLAYNAME" = "%@ が名前を %@ に変更しました"; +"USER_UPDATED_DISPLAYNAME" = "%@さんが名前を%@に変更しました"; /* New file message from a specific person, not referencing a room. */ -"FILE_FROM_USER" = "%@ がファイルを送信しました: %@"; +"FILE_FROM_USER" = "%@がファイルを送信しました:%@"; /* New audio message from a specific person, not referencing a room. */ -"AUDIO_FROM_USER" = "%@ が音声ファイルを送信しました: %@"; +"AUDIO_FROM_USER" = "%@が音声ファイルを送信しました:%@"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@さんが音声配信を開始しました"; From acb96d67f7d2d55135dbb382f777a86bc89cbe16 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 31 Jan 2023 12:33:37 +0000 Subject: [PATCH 322/468] Translated using Weblate (Albanian) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/sq/ --- Riot/Assets/sq.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sq.lproj/Localizable.strings b/Riot/Assets/sq.lproj/Localizable.strings index a49dd9660..8036083e0 100644 --- a/Riot/Assets/sq.lproj/Localizable.strings +++ b/Riot/Assets/sq.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ tregoi vendndodhjen e vet"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ nisi një transmetim zanor"; From 83695ac84fe171ab1e1b82353324fd7de49167ca Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 18:28:13 +0000 Subject: [PATCH 323/468] Translated using Weblate (Japanese) Currently translated at 75.0% (6 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/ja/ --- Riot/Assets/ja.lproj/InfoPlist.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/InfoPlist.strings b/Riot/Assets/ja.lproj/InfoPlist.strings index d99e8eb80..2582895dd 100644 --- a/Riot/Assets/ja.lproj/InfoPlist.strings +++ b/Riot/Assets/ja.lproj/InfoPlist.strings @@ -1,6 +1,6 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真撮影、動画撮影に使用されます。"; -"NSPhotoLibraryUsageDescription" = "フォトライブラリは、写真や動画の送信に使用されます。"; +"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真、動画の撮影とアップロードに使用されます。"; +"NSPhotoLibraryUsageDescription" = "フォトライブラリーは、写真や動画のアップロードに使用されます。"; "NSMicrophoneUsageDescription" = "Elementは通話、動画撮影、ボイスメッセージの録音にマイクへのアクセスを必要としています。"; "NSContactsUsageDescription" = "Elementは、あなたが連絡先をチャットに招待できるように、連絡先を表示します。"; "NSCalendarsUsageDescription" = "予定されているミーティングをアプリで確認することができます。"; From 937aacf44bdefdab9df4c6bdde23bce4830e308e Mon Sep 17 00:00:00 2001 From: bluelullaby6 Date: Wed, 1 Feb 2023 22:57:36 +0000 Subject: [PATCH 324/468] Translated using Weblate (French) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/fr/ --- Riot/Assets/fr.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/fr.lproj/Localizable.strings b/Riot/Assets/fr.lproj/Localizable.strings index 64f1ed513..aae0fdf79 100644 --- a/Riot/Assets/fr.lproj/Localizable.strings +++ b/Riot/Assets/fr.lproj/Localizable.strings @@ -118,3 +118,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ a partagé sa localisation"; + +/* New voice broadcast from a specific person, not referencing a room. */ +"VOICE_BROADCAST_FROM_USER" = "%@ a lancé une diffusion vocale"; From 26fdd91f7c9e4ae9f223a6141b4e5c6a6f3ee753 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Feb 2023 03:47:03 +0000 Subject: [PATCH 325/468] Translated using Weblate (Japanese) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/ja/ --- Riot/Assets/ja.lproj/InfoPlist.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/ja.lproj/InfoPlist.strings b/Riot/Assets/ja.lproj/InfoPlist.strings index 2582895dd..cae22a109 100644 --- a/Riot/Assets/ja.lproj/InfoPlist.strings +++ b/Riot/Assets/ja.lproj/InfoPlist.strings @@ -1,9 +1,9 @@ // Permissions usage explanations "NSCameraUsageDescription" = "カメラは、ビデオ通話や写真、動画の撮影とアップロードに使用されます。"; -"NSPhotoLibraryUsageDescription" = "フォトライブラリーは、写真や動画のアップロードに使用されます。"; +"NSPhotoLibraryUsageDescription" = "フォトへのアクセスを許可すると、写真や動画をライブラリーからアップロードできるようになります。"; "NSMicrophoneUsageDescription" = "Elementは通話、動画撮影、ボイスメッセージの録音にマイクへのアクセスを必要としています。"; -"NSContactsUsageDescription" = "Elementは、あなたが連絡先をチャットに招待できるように、連絡先を表示します。"; +"NSContactsUsageDescription" = "あなたのIDサーバーに共有され、Matrixで連絡先を発見するのに使用されます。"; "NSCalendarsUsageDescription" = "予定されているミーティングをアプリで確認することができます。"; "NSFaceIDUsageDescription" = "Face IDはアプリへのアクセスに使用されます。"; "NSLocationWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。"; -"NSLocationAlwaysAndWhenInUseUsageDescription" = "あなたが他の人に位置を共有するとき、Elementは地図をその人に表示するアクセス権が必要です。"; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。"; From bea91957bb98655bfe8cdc07493fab78a77c1e19 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 24 Jan 2023 15:59:30 +0000 Subject: [PATCH 326/468] Translated using Weblate (German) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 1bff71b29..6f6ccebcf 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2723,3 +2723,7 @@ "settings_labs_disable_crypto_sdk" = "Krypto-SDK ist aktiviert. Zum Deaktivieren, bitte die App neu installieren"; "settings_labs_confirm_crypto_sdk" = "Dies kann nicht rückgängig gemacht werden"; "settings_labs_enable_crypto_sdk" = "Rust-basiertes Krypto-SDK aktivieren"; +"poll_history_no_past_poll_period_text" = "Für die vergangenen %@ Tage sind keine beendeten Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; +"poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; +"poll_history_load_more" = "Weitere Umfragen laden"; +"poll_history_loading_text" = "Zeige Umfragen an"; From 6835b05174121017a1b2828e45801aa2d2324eb8 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 24 Jan 2023 16:28:20 +0000 Subject: [PATCH 327/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 89455745f..105c76718 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2914,3 +2914,7 @@ "settings_labs_disable_crypto_sdk" = "Crypto SDK увімкнено. Щоб вимкнути, перевстановіть застосунок"; "settings_labs_confirm_crypto_sdk" = "Дію не можна скасувати"; "settings_labs_enable_crypto_sdk" = "Увімкнути новий заснований на rust Crypto SDK"; +"poll_history_load_more" = "Завантажити більше опитувань"; +"poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; +"poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; +"poll_history_loading_text" = "Показ опитувань"; From 8d978801de55e5b307a528021ea71b1d4f1b9834 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 24 Jan 2023 23:55:21 +0000 Subject: [PATCH 328/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 82ef32120..c14d5e27d 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2916,3 +2916,7 @@ "settings_labs_disable_crypto_sdk" = "SDK Kripto diaktifkan. Untuk menonaktifkan, mohon memasang ulang aplikasi"; "settings_labs_confirm_crypto_sdk" = "Tindakan ini tidak dapat diurungkan"; "settings_labs_enable_crypto_sdk" = "Aktifkan SDK Kripto baru berbasis Rust"; +"poll_history_load_more" = "Muat lebih banyak pemungutan suara"; +"poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya"; +"poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; +"poll_history_loading_text" = "Menampilkan pemungutan suara"; From 4ea847cf46d18c97303b32dc308becbbe4856e0d Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 24 Jan 2023 22:29:53 +0000 Subject: [PATCH 329/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 6d69e6c98..a825601f4 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2912,3 +2912,7 @@ "settings_labs_disable_crypto_sdk" = "Crypto SDK je povolené. Ak to chcete vypnúť, preinštalujte prosím aplikáciu"; "settings_labs_confirm_crypto_sdk" = "Túto akciu nemožno vrátiť späť"; "settings_labs_enable_crypto_sdk" = "Zapnúť nové Crypto SDK využívajúce Rust"; +"poll_history_load_more" = "Načítať ďalšie ankety"; +"poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; +"poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; +"poll_history_loading_text" = "Zobrazenie ankiet"; From c4604ef07ca15d1a3d19806b978546b1c88d59f8 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 25 Jan 2023 12:21:33 +0000 Subject: [PATCH 330/468] Translated using Weblate (German) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 6f6ccebcf..8909cbfd9 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2714,7 +2714,7 @@ "wysiwyg_composer_format_action_quote" = "Zitat umschalten"; "wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten"; "wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten"; -"voice_broadcast_recorder_connection_error" = "Verbindungsfehler – Aufzeichnung pausiert"; +"voice_broadcast_recorder_connection_error" = "Verbindungsfehler − Aufnahme pausiert"; "poll_timeline_reply_ended_poll" = "Beendete Umfrage"; // MARK: - Launch loading From c68e388310fc5c026bffbe28e539c5c2d19767c5 Mon Sep 17 00:00:00 2001 From: MomentQYC Date: Wed, 25 Jan 2023 09:22:14 +0000 Subject: [PATCH 331/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 83.0% (1968 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 33 +++++++++++++----------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index f7fd8166c..5ad8c44fa 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -287,8 +287,8 @@ "settings_old_password" = "旧密码"; "settings_new_password" = "新密码"; "settings_confirm_password" = "确认密码"; -"settings_fail_to_update_password" = "更新密码失败"; -"settings_password_updated" = "您的密码已经更新"; +"settings_fail_to_update_password" = "更新Matrix账户密码失败"; +"settings_password_updated" = "您的Matrix账户密码已经更新"; "settings_crypto_device_name" = "会话名称: "; "settings_crypto_device_id" = "\n会话ID: "; "settings_crypto_device_key" = "\n会话密钥:\n"; @@ -582,7 +582,7 @@ "deactivate_account_informations_part5" = "如果您希望我们忘记您的消息,请勾选下面的框\n\nMatrix中的消息可见性与电子邮件类似。 我们忘记您的消息意味着您已发送的消息将不会再与任何新用户或未注册用户共享,但已有权访问这些消息的注册用户仍可访问其副本。"; "deactivate_account_forget_messages_information_part1" = "当我的账户被停用时,请忘记我发送的所有消息("; "deactivate_account_forget_messages_information_part3" = ": 这会导致将来加入的用户看到的是一段不完整的对话)"; -"deactivate_account_password_alert_message" = "要继续,请输入您的密码"; +"deactivate_account_password_alert_message" = "要继续,请输入你的Matrix账户密码"; "rerequest_keys_alert_message" = "请在另一台可以解密消息的设备上启动%@,这样它就可以将密钥发送到此会话。"; "key_backup_setup_title" = "密钥备份"; "key_backup_setup_skip_alert_title" = "您确定吗?"; @@ -725,7 +725,7 @@ "settings_labs_enable_cross_signing" = "开启交叉签名按用户验证而不是按设备验证(开发中)"; "settings_add_3pid_password_title_email" = "添加邮箱地址"; "settings_add_3pid_password_title_msidsn" = "添加电话号码"; -"settings_add_3pid_password_message" = "请填写你的密码以继续"; +"settings_add_3pid_password_message" = "请填写你的Matrix账户的密码以继续"; "settings_add_3pid_invalid_password_message" = "验证信息无效"; "settings_key_backup_button_connect" = "关联此会话到密钥备份"; "settings_devices_description" = "会话的公开名字会对你联络的人可见"; @@ -1082,7 +1082,7 @@ "secrets_recovery_with_key_invalid_recovery_key_title" = "无法访问机密存储"; "secrets_recovery_with_key_invalid_recovery_key_message" = "请验证您输入的安全密钥是否正确。"; "rooms_empty_view_information" = "房间非常适合任何群聊,无论是私人的还是公共的。点击+以查找现有房间,或新建房间。"; -"security_settings_user_password_description" = "通过输入您的账户密码确认您的身份"; +"security_settings_user_password_description" = "通过输入您的Matrix账户密码确认您的身份"; "rooms_empty_view_title" = "房间"; "people_empty_view_information" = "与任何人安全聊天。点击+开始添加人员。"; "people_empty_view_title" = "用户"; @@ -1104,7 +1104,7 @@ "security_settings_secure_backup_synchronise" = "同步"; "security_settings_secure_backup_setup" = "设置"; "security_settings_secure_backup_description" = "备份你的账户数据备份和加密密钥,以防你无法访问会话。 你的密钥将受到唯一的安全密钥保护。"; -"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改密码并重置安全备份。"; +"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改Matrix账户的密码并重置安全备份。"; "settings_show_NSFW_public_rooms" = "显示 NSFW 公共房间"; "external_link_confirmation_message" = "此链接 %@ 会将您带至另一个网站:%@\n\n是否前往?"; "external_link_confirmation_title" = "双击此链接"; @@ -1167,14 +1167,14 @@ "room_info_list_section_other" = "其他"; "create_room_section_footer_encryption" = "加密一经启用,便无法禁用。"; "create_room_placeholder_address" = "#testroom:matrix.org"; -"create_room_section_header_address" = "房间地址"; -"create_room_section_header_type" = "房间类型"; +"create_room_section_header_address" = "地址"; +"create_room_section_header_type" = "谁可以加入"; "create_room_enable_encryption" = "启用加密"; -"create_room_section_header_encryption" = "房间加密"; +"create_room_section_header_encryption" = "加密"; "create_room_placeholder_topic" = "这个房间是关于什么的?"; -"create_room_section_header_topic" = "房间话题(可选)"; +"create_room_section_header_topic" = "话题(可选)"; "create_room_placeholder_name" = "名称"; -"create_room_section_header_name" = "房间名称"; +"create_room_section_header_name" = "名称"; // MARK: - Create Room @@ -1251,10 +1251,10 @@ "invite_friends_share_text" = "嗨,在 %@ 跟我说:%@"; "favourites_empty_view_information" = "你可以选择几种方法 - 最快只需按住。点击星星,它们会自动出现在这里,以确保安全。"; "home_empty_view_information" = "团队、朋友和组织的一体化安全聊天应用程序。 点击下面的「+」按钮添加人员和房间。"; -"create_room_show_in_directory" = "在目录中显示房间"; +"create_room_show_in_directory" = "在房间目录中显示"; "create_room_section_footer_type" = "人们只有在收到聊天室邀请后才可以进入私有房间。"; -"create_room_type_public" = "公开房间"; -"create_room_type_private" = "私有房间"; +"create_room_type_public" = "公开房间(任何人)"; +"create_room_type_private" = "私有房间(仅邀请)"; "biometrics_cant_unlocked_alert_message_login" = "重新登录"; "biometrics_cant_unlocked_alert_message_x" = "若要解锁,请使用 %@ 或重新登录并启用 %@"; "biometrics_cant_unlocked_alert_title" = "无法解锁应用程序"; @@ -1288,7 +1288,7 @@ // Banner "cross_signing_setup_banner_title" = "设置加密"; -"secrets_reset_authentication_message" = "请输入你的账户密码进行确认"; +"secrets_reset_authentication_message" = "请输入你的Matrix账户密码进行确认"; "secrets_reset_warning_message" = "您将重新启动,没有历史记录,消息,受信任的设备或受信任的用户。"; "secrets_reset_warning_title" = "如果你选择全部重置"; "secrets_reset_information" = "仅当没有其他设备可用来验证此设备时,才执行此操作。"; @@ -2275,3 +2275,6 @@ "user_session_learn_more" = "了解更多"; "manage_session_name_info_link" = "了解更多"; "threads_beta_information_link" = "了解更多"; +"authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的二维码。"; +"room_invite_to_space_option_detail" = "他们可以探索 %@,但不会成为 %@ 的成员。"; +"analytics_prompt_message_new_user" = "通过分享匿名的使用数据,帮助我们识别问题并改进 %@ 。为了了解人们如何使用多个设备,我们将生成一个随机的标识符,由你的设备共享。"; From 80fa0ff263f76bd61cf5b7ada1d2df05cfa163a0 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 26 Jan 2023 09:27:51 +0000 Subject: [PATCH 332/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 1cfd48147..c5a89d6c2 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2702,3 +2702,14 @@ "voice_broadcast_connection_error_message" = "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később."; "voice_broadcast_connection_error_title" = "Kapcsolat hiba"; "voice_broadcast_playback_lock_screen_placeholder" = "Hang közvetítés"; +"poll_history_load_more" = "Még több szavazás betöltése"; +"poll_history_no_past_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; +"poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; +"poll_history_loading_text" = "Szavazások megjelenítése"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Adatok migrálása\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Titkosítási SDK engedélyezve. A kikapcsolásához az alkalmazást újra kell telepíteni"; +"settings_labs_confirm_crypto_sdk" = "Ezt a műveletet nem lehet visszavonni"; +"settings_labs_enable_crypto_sdk" = "Az új Rust alapú Titkosítási SDK engedélyezése"; From 20f5086b2cb23297f03e35e0213f141580c10436 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 25 Jan 2023 09:54:19 +0000 Subject: [PATCH 333/468] Translated using Weblate (Italian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 952758a08..b26bb2ec2 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2682,3 +2682,14 @@ "voice_broadcast_recorder_connection_error" = "Errore di connessione - Registrazione in pausa"; "voice_broadcast_connection_error_message" = "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi."; "voice_broadcast_connection_error_title" = "Errore di connessione"; +"poll_history_load_more" = "Carica più sondaggi"; +"poll_history_no_past_poll_period_text" = "Non ci sono sondaggi passati negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti"; +"poll_history_no_active_poll_period_text" = "Non ci sono sondaggi attivi negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti"; +"poll_history_loading_text" = "Visualizzazione sondaggi"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrazione dati\n%@ %%"; +"settings_labs_disable_crypto_sdk" = "Crypto SDK attivato. Per disattivarlo devi reinstallare l'app"; +"settings_labs_confirm_crypto_sdk" = "Quest'azione è irreversibile"; +"settings_labs_enable_crypto_sdk" = "Attiva il nuovo Crypto SDK basato su rust"; From 6ddb0cc77bbd1a7aad581055dc006245f50f785a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 25 Jan 2023 19:01:01 +0000 Subject: [PATCH 334/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2370 of 2370 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index c64a5133f..1951f8d5d 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2661,3 +2661,7 @@ "settings_labs_disable_crypto_sdk" = "Uus Crypto SDK on kasutusel. Tema väljalülitamiseks palun paigalda rakendus uuesti"; "settings_labs_confirm_crypto_sdk" = "Seda toimingut ei saa tagasi pöörata"; "settings_labs_enable_crypto_sdk" = "Võta kasutusele uus Rust-keelel põhinev Crypto SDK"; +"poll_history_load_more" = "Laadi veel küsitlusi"; +"poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; +"poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; +"poll_history_loading_text" = "Küsitluste kuvamise ootel"; From cfb313322d816599c96559e9e75326887f116acb Mon Sep 17 00:00:00 2001 From: Vri Date: Thu, 26 Jan 2023 17:06:34 +0000 Subject: [PATCH 335/468] Translated using Weblate (German) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 8909cbfd9..7f1459e8b 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2727,3 +2727,4 @@ "poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; "poll_history_load_more" = "Weitere Umfragen laden"; "poll_history_loading_text" = "Zeige Umfragen an"; +"poll_history_fetching_error" = "Fehler beim Laden der Umfragen."; From e68a0fc68a422c8d56eace9cff26af52b2c3c4c4 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 26 Jan 2023 19:03:09 +0000 Subject: [PATCH 336/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 105c76718..b0adc41fc 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2918,3 +2918,4 @@ "poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; "poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; "poll_history_loading_text" = "Показ опитувань"; +"poll_history_fetching_error" = "Помилка отримання опитувань."; From e85fd96a6bc0f0b51ab285b6149af91ae2ece695 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Fri, 27 Jan 2023 20:56:30 +0000 Subject: [PATCH 337/468] Translated using Weblate (Swedish) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 189 ++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 872645f66..1862efe67 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2472,3 +2472,192 @@ "authentication_choose_password_not_verified_title" = "E-post inte verifierad"; "authentication_login_with_qr" = "Logga in med QR-kod"; "invite_to" = "Bjud in till %@"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "Äktheten för det här krypterade meddelandet kan inte garanteras på den här enheten."; +"notice_voice_broadcast_ended_by_you" = "Du avslutade en röstsändning."; +"notice_voice_broadcast_ended" = "%@ avslutade en röstsändning."; +"notice_voice_broadcast_live" = "Direktsändning"; +"deselect_all" = "Välj bort alla"; +"wysiwyg_composer_link_action_edit_title" = "Redigera länk"; +"wysiwyg_composer_link_action_create_title" = "Skapa en länk"; +"wysiwyg_composer_link_action_link" = "Länk"; + + + +// Links +"wysiwyg_composer_link_action_text" = "Text"; +"wysiwyg_composer_format_action_quote" = "Växla citat"; +"wysiwyg_composer_format_action_code_block" = "Växla kodblock"; +"wysiwyg_composer_format_action_ordered_list" = "Växla numrerad lista"; +"wysiwyg_composer_format_action_unordered_list" = "Växla punktlista"; +"wysiwyg_composer_format_action_inline_code" = "Tillämpa inline-kodstil"; +"wysiwyg_composer_format_action_link" = "Tillämpa länkformat"; +"wysiwyg_composer_format_action_strikethrough" = "Tillämpa understruken stil"; +"wysiwyg_composer_format_action_underline" = "Tillämpa genomstruken stil"; +"wysiwyg_composer_format_action_italic" = "Tillämpa kursiv stil"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Tillämpa fetstil"; +"wysiwyg_composer_start_action_voice_broadcast" = "Röstsändning"; +"wysiwyg_composer_start_action_text_formatting" = "Textformatering"; +"wysiwyg_composer_start_action_camera" = "Kamera"; +"wysiwyg_composer_start_action_location" = "Plats"; +"wysiwyg_composer_start_action_polls" = "Omröstningar"; +"wysiwyg_composer_start_action_attachments" = "Bilagor"; +"wysiwyg_composer_start_action_stickers" = "Dekaler"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Fotobibliotek"; +"user_session_overview_session_details_button_title" = "Sessionsdetaljer"; +"user_session_overview_session_title" = "Session"; +"user_session_overview_current_session_title" = "Nuvarande session"; +"user_session_details_application_url" = "URL"; +"user_session_details_application_version" = "Version"; +"user_session_details_application_name" = "Namn"; +"user_session_details_device_os" = "Operativsystem"; +"user_session_details_device_browser" = "Webbläsare"; +"user_session_details_device_model" = "Modell"; +"user_session_details_device_ip_location" = "IP-plats"; +"user_session_details_device_ip_address" = "IP-adress"; +"user_session_details_last_activity" = "Senaste aktivitet"; +"user_session_details_session_section_footer" = "Kopiera data genom att trycka på den och hålla nere."; +"user_session_details_session_id" = "Sessions-ID"; +"user_session_details_session_name" = "Sessionsnamn"; +"user_session_details_device_section_header" = "Enhet"; +"user_session_details_application_section_header" = "Applikation"; +"user_session_details_session_section_header" = "Session"; +"user_session_details_title" = "Sessionsdetaljer"; +"device_type_name_unknown" = "Okänd"; +"device_type_name_mobile" = "Mobil"; +"device_type_name_web" = "Webb"; +"device_type_name_desktop" = "Skrivbord"; +"device_name_unknown" = "Okänd klient"; +"device_name_mobile" = "%@ Mobil"; +"device_name_web" = "%@ Webb"; +"device_name_desktop" = "%@ Skrivbord"; +"user_inactive_session_item_with_date" = "Inaktiv i 90+ dagar (%@)"; +"user_inactive_session_item" = "Inaktiv i 90+ dagar"; +"user_session_item_details_last_activity" = "Senast aktiv %@"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_other_session_menu_sign_out_sessions" = "Logga ut ur %@ sessioner"; +"user_other_session_menu_select_sessions" = "Välj sessioner"; +"user_other_session_selected_count" = "%@ valda"; +"user_other_session_clear_filter" = "Rensa filter"; +"user_other_session_no_unverified_sessions" = "Inga overifierade sessioner hittade."; +"user_other_session_no_verified_sessions" = "Inga verifierade sessioner hittade."; +"user_other_session_no_inactive_sessions" = "Inga inaktiva sessioner hittade."; +"user_other_session_filter_menu_inactive" = "Inaktiva"; +"user_other_session_filter_menu_unverified" = "Overifierade"; +"user_other_session_filter_menu_verified" = "Verifierade"; +"user_other_session_filter_menu_all" = "Alla sessioner"; +"user_other_session_filter" = "Filtrera"; +"user_other_session_verified_sessions_header_subtitle" = "För bäst säkerhet, logga ut ur alla sessioner du inte känner igen eller använder längre."; +"user_other_session_current_session_details" = "Din nuvarande session"; +"user_other_session_unverified_sessions_header_subtitle" = "Verifiera dina sessioner för förbättrade säkra meddelanden eller logga ut ur de du inte känner igen eller använder längre."; +"user_other_session_security_recommendation_title" = "Andra sessioner"; +"user_session_rename_session_description" = "Andra användare i direktmeddelanden och rum du går med i kan se den fulla listan över dina sessioner.\n\nDetta gör att de kan lita på att de verkligen pratar med dig, men det betyder också att de kan se sessionsnamnet du anger här."; +"user_session_rename_session_title" = "Döper om sessioner"; +"user_session_inactive_session_description" = "Inaktiva sessioner är sessioner du inte har använt på ett tag, men de fortsätter att ta emot krypteringsnycklar.\n\nBorttagning av inaktiva sessioner förbättrar säkerhet och prestanda, och gör det enklare för dig att identifiera om en ny session ser misstänkt ut."; +"user_session_inactive_session_title" = "Inaktiva sessioner"; +"user_session_permanently_unverified_session_description" = "Sessionen stöder inte kryptering, så den kan inte verifieras.\n\nDu kommer inte kunna delta i rum där kryptering är aktiverat när du använder den här sessionen.\n\nFör bäst säkerhet så rekommenderas det att använda Matrixklienter som stöder kryptering."; +"user_session_unverified_session_description" = "Overifierade sessioner är sessioner som har loggat in med dina uppgifter men som inte har korsverifierats.\n\nDu bör speciellt försäkra att du känner igen dessa sessioner eftersom de kan representera obehörig användning av ditt konto."; +"user_session_unverified_session_title" = "Overifierad session"; +"user_session_verified_session_description" = "Verifierade sessioner är alla ställen där du använder Element efter att ha angett din lösenfras eller bekräftat din identitet med en annan verifierad session.\n\nDet betyder att du har alla nycklar som krävs för att låsa upp krypterade meddelanden och bekräfta för andra användare att du litar på den här sessionen."; +"user_session_verified_session_title" = "Verifierade sessioner"; +"user_session_got_it" = "Förstått"; +"user_session_push_notifications_message" = "När aktiverad så tar den här sessionen emot pushnotiser."; +"user_session_push_notifications" = "Pushnotiser"; +"user_other_session_verified_additional_info" = "Den här sessioner är redo för säkra meddelanden."; +"user_other_session_permanently_unverified_additional_info" = "Den här sessionen stöder inte kryptering och kan därför inte verifieras."; +"user_other_session_unverified_additional_info" = "Verifiera eller logga ut ur den här sessionen för bäst säkerhet och pålitlighet."; +"user_session_verification_unknown_additional_info" = "Verifiera din nuvarande session för att avslöja den här sessionens verifieringsstatus."; +"user_session_unverified_additional_info" = "Verifiera din nuvarande session för förbättrade säkra meddelanden."; +"user_session_verified_additional_info" = "Din nuvarande session är redo för säkra meddelanden."; +"user_session_learn_more" = "Läs mer"; +"user_session_view_details" = "Visa detaljer"; +"user_session_verify_action" = "Verifiera session"; +"user_session_verification_unknown_short" = "Okänd"; +"user_session_unverified_short" = "Overifierad"; +"user_session_verified_short" = "Verifierad"; +"user_session_verification_unknown" = "Okänd verifieringsstatus"; +"user_session_unverified" = "Overifierad session"; +"user_session_verified" = "Verifierad session"; +"user_sessions_view_all_action" = "Visa alla (%d)"; +"user_sessions_overview_link_device" = "Länka en enhet"; +"user_sessions_overview_current_session_section_title" = "Nuvarande session"; +"user_sessions_hide_location_info" = "Dölj IP-adress"; +"user_sessions_show_location_info" = "Visa IP-adress"; +"user_sessions_overview_other_sessions_section_info" = "För bäst säkerhet, verifiera dina sessioner och logga ut ur alla sessioner du inte känner igen eller använder längre."; +"user_sessions_overview_other_sessions_section_title" = "Andra sessioner"; +"user_sessions_overview_security_recommendations_inactive_info" = "Överväg att logga ut ur gamla sessioner (90 dagar eller äldre) du inte använder längre."; +"user_sessions_overview_security_recommendations_inactive_title" = "Inaktiva sessioner"; +"user_sessions_overview_security_recommendations_unverified_info" = "Verifiera eller logga ut från overifierade sessioner."; +"user_sessions_overview_security_recommendations_unverified_title" = "Overifierade sessioner"; +"user_sessions_overview_security_recommendations_section_info" = "Förbättra din kontosäkerhet genom att följa dessa rekommendationer."; +"user_sessions_overview_security_recommendations_section_title" = "Säkerhetsrekommendationer"; +"user_sessions_overview_title" = "Sessioner"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"location_sharing_map_loading_error" = "Kan inte ladda karta.\nDen här hemservern är inte konfigurerad för att visa kartor"; +"location_sharing_invalid_power_level_message" = "Du har inte de behörigheter som krävs för att dela realtidsplats i det här rummet."; +"location_sharing_invalid_power_level_title" = "Du är inte behörig att dela realtidsplats"; +"poll_timeline_reply_ended_poll" = "Avslutade omröstning"; +"poll_timeline_ended_text" = "Avslutade omröstningen"; +"poll_timeline_decryption_error" = "På grund av avkrypteringsfel så kanske inte vissa röster räknas"; +"poll_history_fetching_error" = "Fel vid hämtning av omröstningar."; +"poll_history_load_more" = "Ladda fler omröstningar"; +"poll_history_no_past_poll_period_text" = "Det finns inga tidigare omröstningar från det senaste %@ dagarna. Ladda fler omröstningar för att se omröstningar från tidigare månader"; +"poll_history_no_active_poll_period_text" = "Det finns inga aktiva omröstningar under de senaste %@ dagarna. Ladda fler omröstningar för att visa omröstningar för tidigare månader"; +"poll_history_no_past_poll_text" = "Det finns inga tidigare omröstningar i det här rummet"; +"poll_history_no_active_poll_text" = "Det finns inga aktiva omröstningar i det här rummet"; +"poll_history_past_segment_title" = "Tidigare omröstningar"; +"poll_history_active_segment_title" = "Aktiva omröstningar"; +"poll_history_loading_text" = "Visar omröstningar"; + +// MARK: - Polls history + +"poll_history_title" = "Omröstningshistorik"; +"space_invite_nav_title" = "Utrymmesinbjudan"; +"space_detail_nav_title" = "Utrymmesdetalj"; +"space_selector_create_space" = "Skapa utrymme"; +"space_selector_empty_view_information" = "Utrymmen är ett sätt att gruppera rum och personer. Skapa et utrymme för att komma igång."; +"space_selector_empty_view_title" = "Inga utrymmen än."; + +// MARK: - Space Selector + +"space_selector_title" = "Mina utrymmen"; +"room_invites_empty_view_information" = "Det här är vart dina inbjudningar hamnar."; + +// MARK: - Room invites + +"room_invites_empty_view_title" = "Inget nytt."; +"all_chats_edit_menu_space_settings" = "Utrymmesinställningar"; +"all_chats_edit_menu_leave_space" = "Lämna %@"; +"all_chats_user_menu_settings" = "Användarinställningar"; +"all_chats_user_menu_accessibility_label" = "Användarmeny"; +"room_recents_recently_viewed_section" = "Nyligen sedda"; +"all_chats_nothing_found_placeholder_message" = "Pröva att justera din sökning."; +"all_chats_nothing_found_placeholder_title" = "Inget hittat."; +"all_chats_empty_unreads_placeholder_message" = "Det här är vart dina olästa meddelanden kommer att hamna, när du har några."; +"voice_broadcast_recorder_connection_error" = "Anslutningsfel - Inspelning pausad"; +"voice_broadcast_connection_error_message" = "Tyvärr kan vi inte starta en röstsändning för tillfället. Vänligen pröva igen senare."; +"voice_broadcast_connection_error_title" = "Anslutningsfel"; +"voice_broadcast_playback_lock_screen_placeholder" = "Röstsändning"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migrerar data\n%@ %%"; +"room_details_polls" = "Omröstningshistorik"; +"settings_labs_disable_crypto_sdk" = "Krypto-SDK är aktiverad. För att inaktivera, vänligen installera om appen"; +"settings_labs_confirm_crypto_sdk" = "Den här åtgärden kan inte ångras"; +"settings_labs_enable_crypto_sdk" = "Aktivera ny Rust-baserad krypto-SDK"; +"accessibility_selected" = "vald"; From 99838637f3aa80b92c6b35b0b48647eb700feea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 26 Jan 2023 19:22:47 +0000 Subject: [PATCH 338/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 1951f8d5d..d16c075d9 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2665,3 +2665,4 @@ "poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; "poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; "poll_history_loading_text" = "Küsitluste kuvamise ootel"; +"poll_history_fetching_error" = "Viga küsitluste laadimisel."; From 2503023599d33f0c6ae70e53755f69d372f8b049 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 26 Jan 2023 23:57:08 +0000 Subject: [PATCH 339/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index c14d5e27d..9687859bf 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2920,3 +2920,4 @@ "poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya"; "poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; "poll_history_loading_text" = "Menampilkan pemungutan suara"; +"poll_history_fetching_error" = "Terjadi kesalahan mendapatkan pemungutan suara."; From d4839769ae333ce28764d29d2a40f25b2afcc481 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 26 Jan 2023 21:40:21 +0000 Subject: [PATCH 340/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index a825601f4..cb3f411de 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2916,3 +2916,4 @@ "poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; "poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; "poll_history_loading_text" = "Zobrazenie ankiet"; +"poll_history_fetching_error" = "Chyba pri načítavaní ankiet."; From 7179a7da63a4e38ad3eb953f34b936e47cd8eddf Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:01:21 +0000 Subject: [PATCH 341/468] Translated using Weblate (Japanese) Currently translated at 70.7% (1677 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index de3ff9574..c54cd8489 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1800,3 +1800,6 @@ "location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; "location_sharing_live_error" = "位置情報(ライブ)のエラー"; "all_chats_edit_layout" = "レイアウトの設定"; + +// Crypto +"e2e_enabling_on_app_update" = "Elementはエンドツーエンドの暗号化に対応しましたが、有効にするには再度ログインする必要があります。\n\nアプリケーションの設定から今すぐ、もしくは後で行うことができます。"; From 5de9f53761a0b7d8dde8cf8748ee5364edb55639 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:00:44 +0000 Subject: [PATCH 342/468] Translated using Weblate (Japanese) Currently translated at 70.7% (1677 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 244 +++++++++++++++++++++++----- 1 file changed, 203 insertions(+), 41 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index c54cd8489..0b412e397 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -119,7 +119,7 @@ // People tab "people_invites_section" = "招待中"; "people_conversation_section" = "会話"; -"people_no_conversation" = "会話なし"; +"people_no_conversation" = "会話がありません"; // Rooms tab "room_directory_no_public_room" = "利用可能な公開ルームはありません"; // Search @@ -149,8 +149,8 @@ "room_participants_add_participant" = "参加者を追加"; "room_participants_one_participant" = "参加者1名"; "room_participants_multi_participants" = "参加者%d名"; -"room_participants_leave_prompt_title" = "ルームを退出"; -"room_participants_leave_prompt_msg" = "ルームを退出してよろしいですか?"; +"room_participants_leave_prompt_title" = "ルームから退出"; +"room_participants_leave_prompt_msg" = "ルームから退出してよろしいですか?"; "room_participants_remove_prompt_title" = "確認"; "room_participants_remove_prompt_msg" = "本当に%@をチャットから退去させますか?"; "room_participants_remove_third_party_invite_msg" = "サードパーティの招待を削除することは、APIが存在するまでサポートされていません"; @@ -172,7 +172,7 @@ "room_participants_action_section_devices" = "セッション一覧"; "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; -"room_participants_action_leave" = "このルームを退出"; +"room_participants_action_leave" = "このルームから退出"; "room_participants_action_remove" = "このルームから削除"; "room_participants_action_ban" = "このルームからブロック"; "room_participants_action_unban" = "ブロックを解除"; @@ -187,8 +187,8 @@ "room_participants_action_mention" = "メンション"; // Chat "room_jump_to_first_unread" = "最初の未読位置へ移動"; -"room_new_message_notification" = "%d件の新しい発言"; -"room_new_messages_notification" = "%d件の新しい発言"; +"room_new_message_notification" = "%d件の新しいメッセージ"; +"room_new_messages_notification" = "%d件の新しいメッセージ"; "room_one_user_is_typing" = "%@さんが入力しています…"; "room_two_users_are_typing" = "%@さん、%@さんが入力しています…"; "room_many_users_are_typing" = "%@さん、%@さん他が入力しています…"; @@ -252,7 +252,7 @@ "settings_mark_all_as_read" = "全ての発言を既読にする"; "settings_report_bug" = "バグレポート"; "settings_config_home_server" = "接続先サーバーは %@"; -"settings_config_identity_server" = "認証サーバは %@"; +"settings_config_identity_server" = "IDサーバー:%@"; "settings_config_user_id" = "%@でログインしています"; "settings_user_settings" = "利用者設定"; "settings_notifications_settings" = "通知設定"; @@ -273,9 +273,9 @@ "settings_first_name" = "名"; "settings_surname" = "姓"; "settings_remove_prompt_title" = "確認"; -"settings_remove_email_prompt_msg" = "メールアドレス %@ を本当に削除してよろしいですか?"; -"settings_remove_phone_prompt_msg" = "電話番号 %@ を本当に削除してよろしいですか?"; -"settings_email_address" = "電子メール"; +"settings_remove_email_prompt_msg" = "メールアドレス %@ を削除してよろしいですか?"; +"settings_remove_phone_prompt_msg" = "電話番号 %@ を削除してよろしいですか?"; +"settings_email_address" = "メールアドレス"; "settings_email_address_placeholder" = "あなたのメールアドレスを入力してください"; "settings_add_email_address" = "メールアドレスを追加"; "settings_phone_number" = "電話番号"; @@ -656,7 +656,7 @@ "event_formatter_call_back" = "かけ直す"; "event_formatter_call_you_declined" = "通話を拒否しました"; "event_formatter_call_you_currently_in" = "通話中です"; -"event_formatter_call_has_ended" = "通話は有効です"; +"event_formatter_call_has_ended" = "通話が終了しました"; "event_formatter_call_video" = "ビデオ通話"; "event_formatter_call_voice" = "音声通話"; "event_formatter_message_edited_mention" = "(編集済)"; @@ -855,7 +855,7 @@ "settings_key_backup_info_trust_signature_invalid_device_verified" = "バックアップには%@による無効な署名があります"; "settings_key_backup_info_trust_signature_valid_device_unverified" = "バックアップには%@による署名があります"; "settings_key_backup_info_trust_signature_valid_device_verified" = "バックアップには%@による有効な署名があります"; -"settings_key_backup_info_trust_signature_valid" = "バックアップにはこのセッションの有効な署名があります"; +"settings_key_backup_info_trust_signature_valid" = "バックアップにはこのセッションによる有効な署名があります"; "settings_key_backup_info_trust_signature_unknown" = "バックアップにはID:%@によるセッションの署名があります"; "settings_key_backup_info_progress_done" = "全ての鍵がバックアップされています"; "settings_key_backup_info_progress" = "%@の鍵をバックアップしています…"; @@ -866,7 +866,7 @@ "settings_security" = "セキュリティー"; "settings_three_pids_management_information_part3" = ""; "settings_three_pids_management_information_part2" = "ディスカバリー"; -"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrixーーオープンな分散型通信の標準規格ーーで動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ(https://app.element.io)でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; +"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrixーーオープンな分散型通信の標準規格ーーで動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; "user_verification_session_details_information_untrusted_current_user" = "このセッションを認証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; @@ -990,7 +990,7 @@ "settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理します。誰があなたのことを発見できるかを管理する "; "settings_identity_server_settings" = "IDサーバー"; "external_link_confirmation_title" = "このリンクを再確認してください"; -"media_type_accessibility_sticker" = "スティッカー"; +"media_type_accessibility_sticker" = "ステッカー"; "media_type_accessibility_file" = "ファイル"; "media_type_accessibility_location" = "位置情報"; "media_type_accessibility_video" = "動画"; @@ -999,7 +999,7 @@ "room_open_dialpad" = "ダイヤルパッド"; "room_place_voice_call" = "ビデオ通話"; "room_accessibility_hangup" = "通話を切る"; -"room_event_action_delete_confirmation_message" = "この未送信メッセージを削除してもよろしいですか?"; +"room_event_action_delete_confirmation_message" = "この未送信メッセージを削除してよろしいですか?"; "room_accessibility_video_call" = "ビデオ通話"; "room_accessibility_call" = "通話"; "room_accessibility_integrations" = "統合"; @@ -1007,7 +1007,7 @@ "room_accessibility_upload" = "アップロード"; "room_message_edits_history_title" = "メッセージを編集"; "room_action_reply" = "返信"; -"room_action_send_file" = "ファイルを送る"; +"room_action_send_file" = "ファイルを送信"; "room_action_camera" = "写真やビデオの撮影"; "room_event_action_reaction_history" = "反応の履歴"; "room_event_action_reaction_show_less" = "表示しない"; @@ -1182,7 +1182,7 @@ "login_invalid_param" = "無効なパラメーター"; "register_error_title" = "登録に失敗しました"; "login_error_forgot_password_is_not_supported" = "Forgot passwordは現在サポートされていません"; -"login_mobile_device" = "携帯"; +"login_mobile_device" = "携帯端末"; "login_tablet_device" = "タブレット"; "login_desktop_device" = "デスクトップ"; "login_error_resource_limit_exceeded_title" = "リソース制限を超えました"; @@ -1211,7 +1211,7 @@ "cancel_download" = "ダウンロードをキャンセル"; "answer_call" = "通話に応答"; "reject_call" = "通話を拒否"; -"end_call" = "通話終了"; +"end_call" = "通話を終了"; "ignore" = "無視"; // Events formatter "notice_avatar_changed_too" = "(アバターも変更されました)"; @@ -1220,8 +1220,8 @@ "notice_event_redacted" = "<編集された%@>"; "notice_event_redacted_by" = " %@により"; "notice_event_redacted_reason" = " [理由: %@]"; -"notice_profile_change_redacted" = "%@が彼らのプロフィール %@を更新しました"; -"notice_room_created" = "%@がルームを作成しました"; +"notice_profile_change_redacted" = "%@がプロフィール%@を更新しました"; +"notice_room_created" = "%@がルームを作成し設定しました。"; "notice_room_join_rule" = "結合ルールは次のとおり: %@"; "notice_room_power_level_intro" = "ルームメンバーの権限レベル:"; "notice_room_power_level_acting_requirement" = "アクション前にユーザーの必要な最小権限レベル:"; @@ -1290,7 +1290,7 @@ "room_event_encryption_info_unverify" = "未認証"; "room_event_encryption_info_block" = "ブラックリスト"; "room_event_encryption_info_unblock" = "ブラックでないリスト"; -"room_event_encryption_verify_title" = "セッション認証\n\n"; +"room_event_encryption_verify_title" = "セッションを認証\n\n"; "room_event_encryption_verify_message" = "このセッションが信頼できることを確認するには、他の方法(対面や電話など)で所有者に連絡し、セッションのユーザー設定で表示される鍵が以下の鍵と一致するかどうかを訪ねてください。\n\nセッション名: %@\nセッションID: %@\nセッションキー: %@\n\n一致する場合は、下の確認ボタンを押します。 それ以外の人がこのセッションを傍受している場合は、代わりにブラックリストボタンを押してください。\n\n将来この認証プロセスはより洗練されたものになります。"; "room_event_encryption_verify_ok" = "認証"; // Account @@ -1335,7 +1335,7 @@ "message_reply_to_sender_sent_a_video" = "動画を送りました。"; "message_reply_to_sender_sent_an_audio_file" = "オーディオファイルを送信しました。"; "message_reply_to_sender_sent_a_file" = "ファイルを送信しました。"; -"message_reply_to_message_to_reply_to_prefix" = "に返信"; +"message_reply_to_message_to_reply_to_prefix" = "返信先"; // Room members "room_member_ignore_prompt" = "このユーザーからの全てのメッセージを非表示にしますか?"; "room_member_power_level_prompt" = "この変更を元に戻すことはできません。ユーザーが自分と同じレベルの権限を持つように促しますが、よろしいですか?"; @@ -1409,15 +1409,15 @@ "notice_room_reject" = "%@が招待を拒否しました"; "notice_room_kick" = "%@が%@を追い出しました"; "notice_room_unban" = "%@が%@を追放解除しました"; -"notice_room_ban" = "%@が%@を追放しました"; -"notice_room_withdraw" = "%@が%@の招待を辞退しました"; +"notice_room_ban" = "%@が%@をブロックしました"; +"notice_room_withdraw" = "%@が%@の招待を取り下げました"; "notice_room_reason" = ". 理由: %@"; "notice_avatar_url_changed" = "%@がアバターを変更しました"; "notice_display_name_set" = "%@が表示名を%@に設定しました"; "notice_display_name_changed_from" = "%@が表示名を%@から%@に変更しました"; "notice_display_name_removed" = "%@が表示名を削除しました"; -"notice_topic_changed" = "%@がトピックを次のように変更しました:%@"; -"notice_room_name_changed" = "%@がルーム名を次のように変更しました:%@"; +"notice_topic_changed" = "%@がトピックを「%@」に変更しました。"; +"notice_room_name_changed" = "%@がルーム名を%@に変更しました。"; "notice_placed_voice_call" = "%@が電話をかけました"; "notice_placed_video_call" = "%@がビデオ電話をかけました"; "notice_answered_video_call" = "%@が電話に出ました"; @@ -1429,7 +1429,7 @@ "send" = "送信"; "copy_button_name" = "コピー"; "resend" = "再送信"; -"redact" = "編集"; +"redact" = "削除"; "share" = "共有"; "set_power_level" = "権限レベル"; "delete" = "削除"; @@ -1485,12 +1485,12 @@ // gcm section // call string "call_waiting" = "待機中..."; -"call_connecting" = "通話接続中…"; -"call_ended" = "通話終了"; +"call_connecting" = "接続しています…"; +"call_ended" = "通話が終了しました"; "call_ring" = "呼び出し中..."; -"incoming_video_call" = "着信ビデオ通話"; -"incoming_voice_call" = "着信音声通話"; -"call_invite_expired" = "期限切れの招待コール"; +"incoming_video_call" = "ビデオ通話の着信中"; +"incoming_voice_call" = "音声通話の着信中"; +"call_invite_expired" = "通話の招待の期限が切れました"; // unrecognized SSL certificate "ssl_trust" = "信頼"; "ssl_logout_account" = "ログアウト"; @@ -1595,7 +1595,7 @@ // Onboarding "onboarding_splash_register_button_title" = "アカウントを作成"; "notice_room_created_by_you_for_dm" = "参加しました"; -"notice_room_created_for_dm" = "%@が参加しました"; +"notice_room_created_for_dm" = "%@が参加しました。"; "onboarding_use_case_existing_server_button" = "サーバーに接続"; "callbar_only_single_active_group" = "タップしてグループ通話に参加 (%@)"; "settings_confirm_media_size" = "送信時のサイズ確認"; @@ -1621,12 +1621,12 @@ "spaces_creation_visibility_title" = "作成するスペースの種類を選択してください"; "space_public_join_rule_detail" = "誰でも参加可能、コミュニティー向け"; "space_private_join_rule_detail" = "招待者のみ参加可能、個人やチーム向け"; -"onboarding_use_case_title" = "誰と話すことが一番多いですか?"; +"onboarding_use_case_title" = "誰と最もよく会話しますか?"; "onboarding_splash_page_4_message" = "Elementは職場利用にも最適です。世界で最も安全な組織によって信頼されています。"; -"onboarding_splash_page_4_title_no_pun" = "チームのためのメッセージング。"; -"onboarding_splash_page_3_message" = "E2Eで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; +"onboarding_splash_page_4_title_no_pun" = "あなたのチームのメッセージングに。"; +"onboarding_splash_page_3_message" = "エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; "onboarding_splash_page_3_title" = "安全なメッセージ。"; -"onboarding_splash_page_2_message" = "データがどこに保存されるかを自分で選び、主導権と独立を手に入れよう。Matrixで接続。"; +"onboarding_splash_page_2_message" = "会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。"; "onboarding_splash_page_2_title" = "主導権はあなたにある。"; "onboarding_splash_page_1_message" = "オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。"; "saving" = "保存中"; @@ -1696,7 +1696,7 @@ "home_context_menu_make_dm" = "連絡先に移動"; "home_context_menu_make_room" = "ルームに移動"; "leave_space_title" = "%@ を退出"; -"room_participants_leave_success" = "ルームを退出しました"; +"room_participants_leave_success" = "ルームから退出しました"; "room_participants_leave_processing" = "退出しています"; "event_formatter_group_call_leave" = "退出"; "home_context_menu_leave" = "退出"; @@ -1730,9 +1730,9 @@ "password_validation_error_contain_uppercase_letter" = "大文字を含める"; "password_validation_error_contain_lowercase_letter" = "小文字を含める"; /* The placeholder will show a number */ -"password_validation_error_max_length" = "%d 文字以下"; +"password_validation_error_max_length" = "%d文字以下"; /* The placeholder will show a number */ -"password_validation_error_min_length" = "%d 文字以上"; +"password_validation_error_min_length" = "%d文字以上"; // MARK: Password Validation "password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; @@ -1797,9 +1797,171 @@ // Alert explaining what an identity server / integration manager is. "service_terms_modal_information_title_identity_server" = "IDサーバー"; -"location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; +"location_sharing_invalid_power_level_message" = "このルームでの位置情報(ライブ)の共有には適切な権限が必要です。"; "location_sharing_live_error" = "位置情報(ライブ)のエラー"; "all_chats_edit_layout" = "レイアウトの設定"; // Crypto "e2e_enabling_on_app_update" = "Elementはエンドツーエンドの暗号化に対応しましたが、有効にするには再度ログインする必要があります。\n\nアプリケーションの設定から今すぐ、もしくは後で行うことができます。"; +"analytics_prompt_stop" = "共有を停止"; +"analytics_prompt_not_now" = "後で"; +"analytics_prompt_point_3" = "これはいつでも設定から無効にできます"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "私たちは、情報を第三者と共有することはありません"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "私たちは、アカウントのデータを記録したり分析したりすることはありません"; +"analytics_prompt_terms_link_upgrade" = "ここ"; +"call_jitsi_unable_to_start" = "グループ通話を開始できません"; +"network_offline_message" = "オフラインです。接続を確認してください。"; +"network_offline_title" = "オフラインです"; +"event_formatter_group_call_join" = "参加"; +"event_formatter_group_call" = "グループ通話"; +"event_formatter_call_end_call" = "通話を終了"; +"event_formatter_call_retry" = "再試行"; +"event_formatter_call_decline" = "拒否"; +"event_formatter_call_connection_failed" = "接続に失敗しました"; +"event_formatter_call_ringing" = "呼び出しています…"; +"event_formatter_call_connecting" = "接続しています…"; +"call_ringing" = "呼び出しています…"; +"room_notifs_settings_manage_notifications" = "通知は%@で管理できます"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "メンバーを新しいルームに自動的に招待"; +"room_access_settings_screen_upgrade_alert_note" = "アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。"; +"room_access_settings_screen_upgrade_alert_message_no_param" = "親のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; +"room_access_settings_screen_upgrade_alert_message" = "%@に属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "ルームへのアクセス"; +"room_details_polls" = "アンケートの履歴"; +// User sessions management +"user_sessions_settings" = "セッションを管理"; +"manage_session_sign_out_other_sessions" = "他の全てのセッションからサインアウト"; +"manage_session_rename" = "セッション名を変更"; +"manage_session_name_info_link" = "詳細を表示"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "セッション名は連絡先にも表示されます。%@"; +"manage_session_name_hint" = "セッション名を設定すると、端末をより簡単に認識できるようになります。"; +"security_settings_coming_soon" = "申し訳ありません。このアクションは%@ iOSではまだ利用できません。他のMatrixクライアントを使って設定してください。将来的には%@ iOSでも実装される予定です。"; +"security_settings_secure_backup_reset" = "再設定"; +"security_settings_secure_backup_info_checking" = "確認しています…"; +"settings_presence_offline_mode_description" = "有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。"; +"settings_presence_offline_mode" = "オフラインモード"; +"settings_enable_room_message_bubbles" = "吹き出しでメッセージを表示"; +"settings_discovery_accept_terms" = "IDサーバーの利用規約を承諾"; +"settings_labs_confirm_crypto_sdk" = "この操作は取り消せません"; +"settings_labs_enable_voice_broadcast" = "音声配信"; +"settings_labs_enable_new_app_layout" = "アプリケーションの新しいレイアウト"; +"settings_labs_enable_new_client_info_feature" = "クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定"; +"settings_labs_enable_new_session_manager" = "新しいセッションマネージャー"; +"settings_labs_use_only_latest_user_avatar_and_name" = "ユーザーのアバターと名前をメッセージの履歴に表示"; +"settings_labs_enable_threads" = "メッセージのスレッド機能"; +"settings_labs_enabled_polls" = "アンケート"; +"settings_ui_show_redactions_in_room_history" = "削除されたメッセージに関する通知を表示"; +"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は%@を許可(IPアドレスは通話中に共有されます)。"; +"settings_callkit_info" = "画面がロックされているときに着信がありました。%@の着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; +"settings_notifications_disabled_alert_title" = "通知が無効です"; +"threads_discourage_information_1" = "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。 "; +"threads_beta_cancel" = "後で"; +"threads_beta_enable" = "試してみる"; +"threads_beta_information_link" = "詳細を表示"; +"threads_beta_title" = "スレッド"; +"threads_notice_done" = "了解"; +"threads_notice_title" = "スレッドは正式版になりました🎉"; +"message_from_a_thread" = "スレッドから"; +"room_accessibility_record_voice_message" = "音声メッセージを録音"; +"room_event_copy_link_info" = "リンクをクリップボードにコピーしました。"; +"room_event_action_end_poll" = "アンケートを終了"; +"room_event_action_remove_poll" = "アンケートを削除"; +"room_participants_invite_prompt_to_msg" = "%@を%@に招待してよろしいですか?"; +"find_your_contacts_identity_service_error" = "IDサーバーに接続できません。"; +"contacts_address_book_permission_denied" = "端末の電話帳を%@が読み取ることは許可されていません"; +/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. Note the > at the start indicates "more than 20 results". */ +"directory_search_results_more_than" = ">%2$@の検索結果%1$tu件"; +/* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. */ +"directory_search_results" = "%2$@の検索結果%1$tu件"; +"room_recents_unknown_room_error_message" = "このルームが発見できません。存在することを確認してください"; +"room_creation_dm_error" = "ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。"; +"password_policy_pwd_in_dict_error" = "パスワードが辞書で見つかりました。許可できません。"; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "パスワードが短すぎます"; +"password_validation_error_header" = "指定したパスワードは以下の要件を満たしていません:"; +"authentication_qr_login_failure_retry" = "もう一度試す"; +"authentication_qr_login_failure_request_denied" = "リクエストはもう一方の端末で拒否されました。"; +"authentication_qr_login_failure_invalid_qr" = "QRコードが不正です。"; +"authentication_qr_login_loading_waiting_signin" = "端末のサインインを待機しています。"; +"authentication_qr_login_loading_connecting_device" = "端末に接続しています"; +"authentication_qr_login_confirm_subtitle" = "以下のコードが他の端末と一致していることを確認してください:"; +"authentication_qr_login_confirm_title" = "安全な接続を確立しました"; +"authentication_qr_login_scan_title" = "QRコードをスキャン"; +"authentication_qr_login_display_subtitle" = "サインアウトした端末で以下のQRコードをスキャンしてください。"; +"authentication_qr_login_start_title" = "QRコードをスキャン"; +"authentication_terms_policy_url_error" = "選択した運営方針が見つかりませんでした。後でもう一度やり直す後でもう一度やり直してください。"; +/* The placeholder will show the homeserver's domain */ +"authentication_terms_message" = "%sの利用規約と運営方針を確認してください"; +"authentication_verify_msisdn_invalid_phone_number" = "無効な電話番号"; +/* The placeholder will show the phone number that was entered. */ +"authentication_verify_msisdn_waiting_message" = "コードが%@に送信されました"; +"authentication_verify_msisdn_waiting_title" = "電話番号を認証してください"; +"authentication_verify_msisdn_otp_text_field_placeholder" = "確認コード"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_msisdn_input_message" = "%@はアカウントの認証が必要です"; +"authentication_verify_msisdn_input_title" = "電話番号を入力してください"; +"authentication_choose_password_not_verified_message" = "メールボックスを確認してください"; +"authentication_choose_password_input_message" = "パスワードは8文字以上に設定してください"; +"authentication_choose_password_input_title" = "パスワードを選択"; +"authentication_forgot_password_waiting_button" = "電子メールを再送信"; +/* The placeholder will show the email address that was entered. */ +"authentication_forgot_password_waiting_message" = "%@に送信された手順に従ってください"; +"authentication_forgot_password_waiting_title" = "電子メールを確認してください。"; +"authentication_forgot_password_text_field_placeholder" = "メールアドレス"; +/* The placeholder will show the homeserver's domain */ +"authentication_forgot_password_input_message" = "%@は認証リンクを送信します"; +"authentication_forgot_password_input_title" = "電子メールを入力してください"; +"authentication_verify_email_waiting_button" = "電子メールを再送信"; +"authentication_verify_email_waiting_hint" = "電子メールが届いていませんか?"; +/* The placeholder will show the email address that was entered. */ +"authentication_verify_email_waiting_message" = "%@に送信された手順に従ってください"; +"authentication_verify_email_waiting_title" = "メールアドレスを認証してください。"; +"authentication_verify_email_text_field_placeholder" = "メールアドレス"; +/* The placeholder will show the homeserver's domain */ +"authentication_verify_email_input_message" = "%@はアカウントの認証が必要です"; +"authentication_verify_email_input_title" = "電子メールを入力してください"; +"authentication_cancel_flow_confirmation_message" = "アカウントがまだ作成されていません。登録を中止しますか?"; +"authentication_server_selection_server_url" = "ホームサーバーのURL"; +"authentication_login_with_qr" = "QRコードでサインイン"; +"authentication_login_username" = "ユーザー名 / メールアドレス / 電話番号"; +"authentication_login_title" = "おかえりなさい!"; +"authentication_registration_password_footer" = "8文字以上にしてください"; +"authentication_registration_username_footer" = "これは後から変更できません"; +"authentication_registration_username" = "ユーザー名"; + +// MARK: Authentication +"authentication_registration_title" = "アカウントを作成"; +"onboarding_celebration_button" = "進みましょう"; +"onboarding_celebration_message" = "設定画面からいつでもプロフィールを更新できます"; +"onboarding_celebration_title" = "問題ありません!"; +"onboarding_avatar_accessibility_label" = "プロフィール画像"; +"onboarding_avatar_title" = "プロフィール画像を追加"; +"onboarding_display_name_max_length" = "表示名は256字以下にしてください"; +"onboarding_display_name_placeholder" = "表示名"; +"onboarding_display_name_message" = "メッセージを送信する際に表示されます。"; +"onboarding_display_name_title" = "表示名を選択"; +"onboarding_personalization_skip" = "このステップをスキップ"; +"onboarding_personalization_save" = "保存して続行"; +"onboarding_congratulations_home_button" = "ホームに移動"; +"onboarding_congratulations_personalize_button" = "プロフィールを変更"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "あなたのアカウント %@ が作成されました"; +"onboarding_congratulations_title" = "おめでとうございます!"; +"onboarding_use_case_existing_server_message" = "既存のサーバーに参加しますか?"; +"onboarding_use_case_skip_button" = "この質問をスキップ"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "迷っていますか?%@"; +"onboarding_use_case_community_messaging" = "コミュニティー"; +"onboarding_use_case_work_messaging" = "チーム"; +"onboarding_use_case_personal_messaging" = "友達と家族"; +"onboarding_use_case_message" = "みんなと繋がる手助けをいたします"; +"onboarding_splash_page_1_title" = "自分の会話は、自分のもの。"; +"accessibility_selected" = "選択済"; +"invite_to" = "%@に招待"; +"joining" = "参加しています"; From 9c2830bbbfdffc44fbd340a6c1dcd25b0f48534a Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:15:27 +0000 Subject: [PATCH 343/468] Translated using Weblate (Japanese) Currently translated at 72.1% (1710 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 0b412e397..6bbee1481 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1965,3 +1965,4 @@ "accessibility_selected" = "選択済"; "invite_to" = "%@に招待"; "joining" = "参加しています"; +"key_backup_setup_passphrase_passphrase_placeholder" = "パスフレーズを入力する"; From 5c946c48e9275ec7c8b2e59da29cd09e3f2631e7 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:15:00 +0000 Subject: [PATCH 344/468] Translated using Weblate (Japanese) Currently translated at 72.1% (1710 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 69 ++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 6bbee1481..21db92296 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -37,7 +37,7 @@ "auth_login" = "ログイン"; "auth_register" = "利用者登録"; "auth_submit" = "受諾"; -"auth_skip" = "省く"; +"auth_skip" = "スキップ"; "auth_send_reset_email" = "初期化メール送信"; "auth_return_to_login" = "ログイン画面へ戻る"; "auth_user_id_placeholder" = "ユーザー名または電子メール"; @@ -266,7 +266,7 @@ "settings_devices" = "セッション"; "settings_cryptography" = "暗号化"; "settings_sign_out" = "サインアウト"; -"settings_sign_out_confirmation" = "本当によろしいですか?"; +"settings_sign_out_confirmation" = "よろしいですか?"; "settings_sign_out_e2e_warn" = "あなたはエンドツーエンド暗号鍵を失ってしまいます。この端末で暗号化されたルームの昔の発言を読むことができなくなります。"; "settings_profile_picture" = "プロフィール画像"; "settings_display_name" = "表示名"; @@ -354,8 +354,8 @@ "room_details_addresses_disable_main_address_prompt_title" = "メインアドレスの警告"; "room_details_addresses_disable_main_address_prompt_msg" = "メインアドレスが設定されていません。このルームのメインアドレスは無作為に選択、設定されます"; "room_details_banned_users_section" = "ブロックされたユーザー"; -"room_details_advanced_section" = "拡張設定"; -"room_details_advanced_room_id" = "ルームの固有ID:"; +"room_details_advanced_section" = "高度な設定"; +"room_details_advanced_room_id" = "ルームID:"; "room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告: 有効後にこれを無効にすることはできません!)"; "room_details_advanced_e2e_encryption_enabled" = "このルームの発言は暗号化されています"; "room_details_advanced_e2e_encryption_disabled" = "このルームの発言は暗号化されていません。"; @@ -554,7 +554,7 @@ "close" = "閉じる"; // Accessibility "accessibility_checkbox_label" = "チェックボックス"; -"auth_login_single_sign_on" = "シングルサインオン(SSO)でサインイン"; +"auth_login_single_sign_on" = "サインイン"; "auth_softlogout_clear_data_sign_out" = "サインアウト"; "room_message_unable_open_link_error_message" = "リンクを開くことができません。"; "user_verification_session_details_verify_action_other_user" = "手動で確認"; @@ -624,7 +624,7 @@ // Widget Picker "widget_picker_title" = "インテグレーションマネージャー"; "widget_integration_manager_disabled" = "設定でインテグレーションマネージャーを有効にする必要があります"; -"widget_menu_remove" = "全て取り除く"; +"widget_menu_remove" = "全員から削除"; "widget_menu_revoke_permission" = "アクセスを取り消す"; "widget_menu_open_outside" = "ブラウザーで開く"; "widget_menu_refresh" = "リフレッシュ"; @@ -669,7 +669,7 @@ "media_picker_title" = "メディアライブラリ"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "ここは暗号化が有効ではありません。"; "room_details_advanced_e2e_encryption_enabled_for_dm" = "ここは暗号化が有効です"; -"room_details_advanced_room_id_for_dm" = "ID:"; +"room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; "room_details_access_section_anyone_apart_from_guest_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを除く)"; @@ -761,7 +761,7 @@ "social_login_list_title_continue" = "続きはこちら"; "auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?再びサインインするとアカウントデータやメッセージにアクセスできます。"; -"auth_softlogout_clear_data_sign_out_title" = "続行してよろしいですか?"; +"auth_softlogout_clear_data_sign_out_title" = "よろしいですか?"; "auth_softlogout_clear_data_button" = "全てのデータをクリア"; "auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; "auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; @@ -1669,7 +1669,7 @@ "sign_out_existing_key_backup_alert_title" = "サインアウトしてよろしいですか?"; "find_your_contacts_message" = "%@ であなたの連絡先を表示し、知人とのチャットを素早く始めます。"; -"find_your_contacts_footer" = "この設定はいつでも無効にできます"; +"find_your_contacts_footer" = "この設定はいつでも無効にできます。"; "find_your_contacts_button_title" = "連絡先を検索"; "find_your_contacts_title" = "連絡先をリストアップ"; @@ -1802,7 +1802,7 @@ "all_chats_edit_layout" = "レイアウトの設定"; // Crypto -"e2e_enabling_on_app_update" = "Elementはエンドツーエンドの暗号化に対応しましたが、有効にするには再度ログインする必要があります。\n\nアプリケーションの設定から今すぐ、もしくは後で行うことができます。"; +"e2e_enabling_on_app_update" = "%@はエンドツーエンドの暗号化に対応しましたが、有効にするには再度ログインする必要があります。\n\nアプリケーションの設定から今すぐ、もしくは後で行うことができます。"; "analytics_prompt_stop" = "共有を停止"; "analytics_prompt_not_now" = "後で"; "analytics_prompt_point_3" = "これはいつでも設定から無効にできます"; @@ -1966,3 +1966,52 @@ "invite_to" = "%@に招待"; "joining" = "参加しています"; "key_backup_setup_passphrase_passphrase_placeholder" = "パスフレーズを入力する"; +"key_backup_setup_passphrase_passphrase_title" = "入力"; +"key_backup_setup_passphrase_info" = "鍵のコピーを暗号化してサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。\n\n最大限のセキュリティーを確保するために、Matrixのアカウントのパスワードと異なるものに設定することが大切です。"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "バックアップをセキュリティーフレーズで保護"; +"key_backup_setup_intro_manual_export_action" = "手動で鍵をエクスポート"; +"key_backup_setup_intro_manual_export_info" = "(高度)"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "このセッションを鍵のバックアップに接続"; +"key_backup_setup_intro_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。\n\n鍵を失くさないよう、鍵を安全にバックアップしてください。"; + +// Intro + +"key_backup_setup_intro_title" = "暗号化されたメッセージを決して失わないために"; +"key_backup_setup_skip_alert_skip_action" = "スキップ"; +"key_backup_setup_skip_alert_message" = "ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。"; +"key_backup_setup_skip_alert_title" = "よろしいですか?"; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "鍵のバックアップ"; + +// Banner + +"secure_backup_setup_banner_title" = "セキュアバックアップ"; +"secure_key_backup_setup_cancel_alert_message" = "いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。\n\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。"; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "よろしいですか?"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "削除"; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "ロックを解除"; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "セキュリティーフレーズを使用"; +"service_terms_modal_table_header_integration_manager" = "インテグレーションマネージャーの利用規約"; +"service_terms_modal_table_header_identity_server" = "IDサーバーの利用規約"; +"service_terms_modal_footer" = "この設定はいつでも無効にできます。"; +"share_extension_send_now" = "送信"; +"room_widget_permission_room_id_permission" = "ルームID"; +"room_widget_permission_widget_id_permission" = "ウィジェットID"; +"room_widget_permission_theme_permission" = "あなたのテーマ"; +"room_widget_permission_user_id_permission" = "あなたのユーザーID"; +"room_widget_permission_avatar_url_permission" = "あなたのアバターのURL"; +"room_widget_permission_display_name_permission" = "あなたの表示名"; +"room_widget_permission_information_title" = "これを使用するとデータが%@と共有される可能性があります:\n"; +"room_widget_permission_webview_information_title" = "これを使用すると、クッキーが設定され、データが%@と共有される可能性があります:\n"; +"room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; From e193e718b760ffaef6ff5dd6b387ee8078d3ce7b Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:18:16 +0000 Subject: [PATCH 345/468] Translated using Weblate (Japanese) Currently translated at 72.2% (1712 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 21db92296..0e4197909 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2015,3 +2015,5 @@ "room_widget_permission_information_title" = "これを使用するとデータが%@と共有される可能性があります:\n"; "room_widget_permission_webview_information_title" = "これを使用すると、クッキーが設定され、データが%@と共有される可能性があります:\n"; "room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認する"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; From 94abfdcc73c54fd099f3e89eee7d77a2a4931cf3 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:15:38 +0000 Subject: [PATCH 346/468] Translated using Weblate (Japanese) Currently translated at 72.2% (1712 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 0e4197909..2f58054cc 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1965,7 +1965,7 @@ "accessibility_selected" = "選択済"; "invite_to" = "%@に招待"; "joining" = "参加しています"; -"key_backup_setup_passphrase_passphrase_placeholder" = "パスフレーズを入力する"; +"key_backup_setup_passphrase_passphrase_placeholder" = "パスフレーズを入力"; "key_backup_setup_passphrase_passphrase_title" = "入力"; "key_backup_setup_passphrase_info" = "鍵のコピーを暗号化してサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。\n\n最大限のセキュリティーを確保するために、Matrixのアカウントのパスワードと異なるものに設定することが大切です。"; From 3bafd8c82e02ba473179410c304af40a366d5943 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:18:57 +0000 Subject: [PATCH 347/468] Translated using Weblate (Japanese) Currently translated at 72.2% (1714 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 2f58054cc..7a65e289b 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2017,3 +2017,5 @@ "room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; "key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認する"; "key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; +"key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定してください"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; From 18a727a3ff97de19ca7417fbac89ae85ab78a4a8 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:18:22 +0000 Subject: [PATCH 348/468] Translated using Weblate (Japanese) Currently translated at 72.2% (1714 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 7a65e289b..9ebca1361 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2015,7 +2015,7 @@ "room_widget_permission_information_title" = "これを使用するとデータが%@と共有される可能性があります:\n"; "room_widget_permission_webview_information_title" = "これを使用すると、クッキーが設定され、データが%@と共有される可能性があります:\n"; "room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; -"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認する"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認"; "key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; "key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定してください"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; From 5e30bc1178dd3ceece3daaa655eb9075b2eacf1e Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:19:12 +0000 Subject: [PATCH 349/468] Translated using Weblate (Japanese) Currently translated at 72.3% (1715 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 9ebca1361..933bcbc73 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2019,3 +2019,4 @@ "key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; "key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定してください"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存します。"; From 29e5cc89dcd8a25adaa2ce4bd0ebe3a294b13f33 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:19:04 +0000 Subject: [PATCH 350/468] Translated using Weblate (Japanese) Currently translated at 72.3% (1715 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 933bcbc73..8b5af0a72 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2017,6 +2017,6 @@ "room_widget_permission_creator_info_title" = "ウィジェットを追加した人:"; "key_backup_setup_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認"; "key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; -"key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定してください"; +"key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; "key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存します。"; From a5ec8d0b1066c7bc1a992b09518a65ae0f2366d4 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:19:49 +0000 Subject: [PATCH 351/468] Translated using Weblate (Japanese) Currently translated at 72.3% (1716 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 8b5af0a72..bcc274244 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2020,3 +2020,4 @@ "key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; "key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存します。"; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(上級者向け) リカバリーキーを設定する"; From 21952187b633336d25ba3413022f01de9ee43765 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:19:21 +0000 Subject: [PATCH 352/468] Translated using Weblate (Japanese) Currently translated at 72.3% (1716 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index bcc274244..d6edda1cb 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2019,5 +2019,5 @@ "key_backup_setup_passphrase_confirm_passphrase_title" = "確認"; "key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; -"key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存します。"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存してください。"; "key_backup_setup_passphrase_setup_recovery_key_action" = "(上級者向け) リカバリーキーを設定する"; From c4ec5db7190ab2ee0a6dd016df65636cb86b7ae4 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:20:38 +0000 Subject: [PATCH 353/468] Translated using Weblate (Japanese) Currently translated at 72.4% (1717 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index d6edda1cb..28ed563e6 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2021,3 +2021,7 @@ "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; "key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存してください。"; "key_backup_setup_passphrase_setup_recovery_key_action" = "(上級者向け) リカバリーキーを設定する"; + +// Success + +"key_backup_setup_success_title" = "成功しました!"; From 7ff69046d80ab1f12cc18595c8f2bd9e80b6df55 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:20:13 +0000 Subject: [PATCH 354/468] Translated using Weblate (Japanese) Currently translated at 72.4% (1717 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 28ed563e6..926fdaa8a 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2020,7 +2020,7 @@ "key_backup_setup_passphrase_set_passphrase_action" = "パスフレーズを設定"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "パスフレーズが一致しません"; "key_backup_setup_passphrase_setup_recovery_key_info" = "または、リカバリーキーでバックアップを確保し、安全な場所に保存してください。"; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(上級者向け) リカバリーキーを設定する"; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(高度)セキュリティーキーで設定"; // Success From e838ca175185b0def6e0c717928a992b563e8f02 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:25:42 +0000 Subject: [PATCH 355/468] Translated using Weblate (Japanese) Currently translated at 72.5% (1721 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 926fdaa8a..59d112e5b 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2025,3 +2025,4 @@ // Success "key_backup_setup_success_title" = "成功しました!"; +"key_backup_recover_invalid_passphrase_title" = "リカバリーパスフレーズが正しくありません"; From 442f9ed5094d30d5f472700fa525f1b2aa8c6d11 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:24:12 +0000 Subject: [PATCH 356/468] Translated using Weblate (Japanese) Currently translated at 72.5% (1721 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 59d112e5b..2533a3e79 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2024,5 +2024,12 @@ // Success -"key_backup_setup_success_title" = "成功しました!"; +"key_backup_setup_success_title" = "成功しました!"; "key_backup_recover_invalid_passphrase_title" = "リカバリーパスフレーズが正しくありません"; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "鍵をバックアップしています。"; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "セキュリティーキーを保存"; + +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; From 6640301a804e98c0e00d8d2f994a83c2023672b6 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:26:39 +0000 Subject: [PATCH 357/468] Translated using Weblate (Japanese) Currently translated at 72.6% (1722 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 2533a3e79..4b71fdadd 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2033,3 +2033,4 @@ // Success from passphrase "key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; +"key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした: 正しいパスフレーズが入力されているか確認してください。"; From 82d51ba4ff0ac91282c55a578b583d9407ac74c8 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:25:51 +0000 Subject: [PATCH 358/468] Translated using Weblate (Japanese) Currently translated at 72.6% (1722 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 4b71fdadd..623f1ee48 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2025,7 +2025,7 @@ // Success "key_backup_setup_success_title" = "成功しました!"; -"key_backup_recover_invalid_passphrase_title" = "リカバリーパスフレーズが正しくありません"; +"key_backup_recover_invalid_passphrase_title" = "セキュリティーフレーズが正しくありません"; // Success from secure backup "key_backup_setup_success_from_secure_backup_info" = "鍵をバックアップしています。"; From 778d46d0a03179521c43b900f8579aed90703ab3 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:28:25 +0000 Subject: [PATCH 359/468] Translated using Weblate (Japanese) Currently translated at 72.6% (1723 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 623f1ee48..a191f99b9 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2034,3 +2034,4 @@ // Success from passphrase "key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; "key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした: 正しいパスフレーズが入力されているか確認してください。"; +"key_backup_recover_invalid_recovery_key_title" = "リカバリーキーが一致しません"; From 25eb9239ee74218b083232c64c50bbdf094918c0 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:26:55 +0000 Subject: [PATCH 360/468] Translated using Weblate (Japanese) Currently translated at 72.6% (1723 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index a191f99b9..acf403759 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2033,5 +2033,5 @@ // Success from passphrase "key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; -"key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした: 正しいパスフレーズが入力されているか確認してください。"; +"key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした。正しいセキュリティーフレーズを入力したことを確認してください。"; "key_backup_recover_invalid_recovery_key_title" = "リカバリーキーが一致しません"; From ac6795431cf942b984ea5ffce054d3605b157b33 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:29:37 +0000 Subject: [PATCH 361/468] Translated using Weblate (Japanese) Currently translated at 72.7% (1725 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index acf403759..5fc99569f 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2035,3 +2035,6 @@ "key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; "key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした。正しいセキュリティーフレーズを入力したことを確認してください。"; "key_backup_recover_invalid_recovery_key_title" = "リカバリーキーが一致しません"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "バックアップを復元する…"; From a87c30c8090624ca9972bd38e1cb26f20816d5fd Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:29:14 +0000 Subject: [PATCH 362/468] Translated using Weblate (Japanese) Currently translated at 72.7% (1725 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 5fc99569f..afc49b9e2 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2034,7 +2034,8 @@ // Success from passphrase "key_backup_setup_success_from_passphrase_info" = "あなたの鍵はバックアップされています。\n\nセキュリティーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、セキュリティーキーを使えば、暗号化されたメッセージにアクセスすることができます。\n\nセキュリティーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。"; "key_backup_recover_invalid_passphrase" = "このパスフレーズではバックアップを復号化できませんでした。正しいセキュリティーフレーズを入力したことを確認してください。"; -"key_backup_recover_invalid_recovery_key_title" = "リカバリーキーが一致しません"; +"key_backup_recover_invalid_recovery_key_title" = "セキュリティーキーが一致しません"; // Recover from private key "key_backup_recover_from_private_key_info" = "バックアップを復元する…"; +"key_backup_recover_invalid_recovery_key" = "この鍵ではバックアップを復号化できませんでした。正しいセキュリティーキーを入力したことを確認してください。"; From a932188deb21aaab4987fad2b2fbea39a9a6d694 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:30:02 +0000 Subject: [PATCH 363/468] Translated using Weblate (Japanese) Currently translated at 72.7% (1726 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index afc49b9e2..179bc570b 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2039,3 +2039,7 @@ // Recover from private key "key_backup_recover_from_private_key_info" = "バックアップを復元する…"; "key_backup_recover_invalid_recovery_key" = "この鍵ではバックアップを復号化できませんでした。正しいセキュリティーキーを入力したことを確認してください。"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "リカバリー・パスフレーズを使ってメッセージ履歴を解除する"; From 915c942f8331e92dea3dc013013ffc2467fd9763 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:29:48 +0000 Subject: [PATCH 364/468] Translated using Weblate (Japanese) Currently translated at 72.7% (1726 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 179bc570b..72f9fb4c4 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2037,7 +2037,7 @@ "key_backup_recover_invalid_recovery_key_title" = "セキュリティーキーが一致しません"; // Recover from private key -"key_backup_recover_from_private_key_info" = "バックアップを復元する…"; +"key_backup_recover_from_private_key_info" = "バックアップを復元しています…"; "key_backup_recover_invalid_recovery_key" = "この鍵ではバックアップを復号化できませんでした。正しいセキュリティーキーを入力したことを確認してください。"; // Recover from passphrase From 7747dd19d2c40333e32ffd4989446dcf21b1cccf Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 08:33:37 +0000 Subject: [PATCH 365/468] Translated using Weblate (Japanese) Currently translated at 72.9% (1730 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 72f9fb4c4..10d210563 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2043,3 +2043,5 @@ // Recover from passphrase "key_backup_recover_from_passphrase_info" = "リカバリー・パスフレーズを使ってメッセージ履歴を解除する"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "リカバリー・パスフレーズがわかりませんか?そんなときは "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; From 2ce019012ed9a8ec76fc7294b790ab131d3f8aca Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 08:33:11 +0000 Subject: [PATCH 366/468] Translated using Weblate (Japanese) Currently translated at 72.9% (1730 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 10d210563..1f1ddb7d3 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2042,6 +2042,8 @@ // Recover from passphrase -"key_backup_recover_from_passphrase_info" = "リカバリー・パスフレーズを使ってメッセージ履歴を解除する"; +"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、メッセージの履歴を解除できます"; "key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "リカバリー・パスフレーズがわかりませんか?そんなときは "; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; +"key_backup_recover_from_passphrase_recover_action" = "履歴を解除"; From 0d801a4daca1e7ae85cdd559ac6c2b03be08c50e Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 10:33:31 +0000 Subject: [PATCH 367/468] Translated using Weblate (Japanese) Currently translated at 83.5% (1982 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 367 ++++++++++++++++++++++++++-- 1 file changed, 347 insertions(+), 20 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 1f1ddb7d3..ee907d40a 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -227,9 +227,9 @@ // Unknown devices "unknown_devices_alert_title" = "ルームに未知のセッションが存在します"; "unknown_devices_alert" = "このルームには、確認されていない未知のセッションが含まれています。\nすなわち、セッションがをユーザー本人が所有しているという保証はありません。\n続ける前に各セッションの確認を行うことをおすすめしますが、確認することなく発言を再送信することができます。"; -"unknown_devices_send_anyway" = "とにかく送る"; -"unknown_devices_call_anyway" = "とにかく通話"; -"unknown_devices_answer_anyway" = "とにかく応答"; +"unknown_devices_send_anyway" = "無視して送信"; +"unknown_devices_call_anyway" = "無視して通話"; +"unknown_devices_answer_anyway" = "無視して応答"; "unknown_devices_verify" = "確認…"; "unknown_devices_title" = "未知のセッション"; // Room Title @@ -330,7 +330,7 @@ "room_details_topic" = "トピック"; "room_details_favourite_tag" = "お気に入り"; "room_details_low_priority_tag" = "低優先度"; -"room_details_mute_notifs" = "発言があっても通知しない"; +"room_details_mute_notifs" = "通知をミュート"; "room_details_direct_chat" = "対話"; "room_details_access_section" = "このルームにアクセスできる人は?"; "room_details_access_section_invited_only" = "招待された人のみ"; @@ -604,7 +604,7 @@ "device_verification_emoji_light bulb" = "電球"; "device_verification_emoji_gift" = "ギフト"; "device_verification_emoji_clock" = "時計"; -"device_verification_emoji_hourglass" = "スバ時計"; +"device_verification_emoji_hourglass" = "砂時計"; "device_verification_emoji_umbrella" = "雨"; "device_verification_emoji_thumbs up" = "親指を立てる"; "device_verification_emoji_spanner" = "スパナ"; @@ -735,7 +735,7 @@ "room_participants_action_security_status_warning" = "警告"; "room_participants_action_security_status_complete_security" = "セキュリティーを確認"; "room_participants_action_security_status_verify" = "認証"; -"room_participants_action_security_status_verified" = "検証済み"; +"room_participants_action_security_status_verified" = "認証済"; "room_participants_action_section_security" = "セキュリティー"; "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使って連絡先とチャットを開始することができません。"; "room_participants_filter_room_members_for_dm" = "メンバーを検索"; @@ -881,8 +881,8 @@ "user_verification_sessions_list_session_untrusted" = "信頼されていません"; "user_verification_sessions_list_session_trusted" = "信頼済"; "user_verification_sessions_list_table_title" = "セッション一覧"; -"user_verification_sessions_list_information" = "このルームにいるこのユーザーとのメッセージはエンドツーエンドで暗号化されており第三者が読み取ることはできません。"; -"user_verification_sessions_list_user_trust_level_unknown_title" = "未知"; +"user_verification_sessions_list_information" = "このルームにいるこのユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "不明"; "user_verification_sessions_list_user_trust_level_warning_title" = "警告"; // Sessions list @@ -928,14 +928,14 @@ "key_verification_incoming_request_incoming_alert_message" = "%@は認証を要求しています"; "key_verification_tile_conclusion_warning_title" = "信頼されていないサインイン"; -"key_verification_tile_conclusion_done_title" = "検証済み"; +"key_verification_tile_conclusion_done_title" = "認証済"; "key_verification_tile_request_incoming_approval_decline" = "却下"; "key_verification_tile_request_incoming_approval_accept" = "承認"; "key_verification_tile_request_status_accepted" = "あなたは承認しました"; "key_verification_tile_request_status_cancelled" = "%@はキャンセルしました"; "key_verification_tile_request_status_cancelled_by_me" = "あなたはキャンセルしました"; "key_verification_tile_request_status_expired" = "期限切れ"; -"key_verification_tile_request_status_waiting" = "お待ちください…"; +"key_verification_tile_request_status_waiting" = "待機しています…"; "key_verification_tile_request_status_data_loading" = "日時を読み込み…"; "key_verification_tile_request_outgoing_title" = "認証を送信済"; @@ -1014,7 +1014,7 @@ "room_event_action_reaction_show_all" = "全てを見る"; "room_event_action_edit" = "編集"; "room_event_action_reply" = "返信"; -"device_verification_security_advice_emoji" = "絵文字の順番はもう一方のログインと一致しますか?"; +"device_verification_security_advice_emoji" = "絵文字を比較して、同じ順番で現れているのを確認してください。"; "key_verification_verify_sas_validate_action" = "一致しています"; "key_verification_verify_sas_cancel_action" = "一致しません"; @@ -1283,8 +1283,8 @@ "room_event_encryption_info_device_id" = "ID\n"; "room_event_encryption_info_device_verification" = "認証\n"; "room_event_encryption_info_device_fingerprint" = "Ed25519 fingerprint\n"; -"room_event_encryption_info_device_verified" = "検証済み"; -"room_event_encryption_info_device_not_verified" = "認証されていない"; +"room_event_encryption_info_device_verified" = "認証済"; +"room_event_encryption_info_device_not_verified" = "認証されていません"; "room_event_encryption_info_device_blocked" = "ブラックリストに載せた"; "room_event_encryption_info_verify" = "認証しています…"; "room_event_encryption_info_unverify" = "未認証"; @@ -1391,8 +1391,8 @@ "user_id_placeholder" = "例: @bob:homeserver"; "ssl_homeserver_url" = "ホームサーバーのURL: %@"; // Permissions -"camera_access_not_granted_for_call" = "ビデオ通話はカメラにアクセスする必要がありますが、%@にはそのカメラを使用する権限がありません"; -"microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@には使用許可がありません"; +"camera_access_not_granted_for_call" = "ビデオ通話にはカメラへのアクセスが必要ですが、%@にはカメラを使用する権限がありません"; +"microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; "local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはそのアクセス権限がありません"; "local_contacts_access_discovery_warning_title" = "ユーザーの探索"; "local_contacts_access_discovery_warning" = "%@は、ユーザーを検索するためにあなたの連絡先から電子メールと電話番号をアップロードしたい"; @@ -1402,7 +1402,7 @@ "language_picker_title" = "言語を選択"; "language_picker_default_language" = "既定値 (%@)"; "notice_room_invite" = "%@が%@を招待しました"; -"notice_room_third_party_invite" = "%@が%@にルームへの招待状を送りました"; +"notice_room_third_party_invite" = "%@が%@にルームへの招待を送りました"; "notice_room_third_party_registered_invite" = "%@が%@の招待を受け入れました"; "notice_room_join" = "%@が参加しました"; "notice_room_leave" = "%@が退出しました"; @@ -1512,7 +1512,7 @@ "notice_room_invite_by_you" = "%@を招待しました"; "notice_room_invite_you" = "%@があなたを招待しました"; "notice_room_join_by_you" = "参加しました"; -"notice_room_leave_by_you" = "あなたが退出しました"; +"notice_room_leave_by_you" = "退出しました"; "notice_room_kick_by_you" = "%@をキックしました"; "notice_room_unban_by_you" = "%@のブロックを解除しました"; "notice_room_ban_by_you" = "%@をブロックしました"; @@ -2042,8 +2042,335 @@ // Recover from passphrase -"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、メッセージの履歴を解除できます"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "リカバリー・パスフレーズがわかりませんか?そんなときは "; +"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、メッセージの履歴のロックを解除できます"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "セキュリティーフレーズが分かりませんか?そんなときは "; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; "key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; -"key_backup_recover_from_passphrase_recover_action" = "履歴を解除"; +"key_backup_recover_from_passphrase_recover_action" = "履歴のロックを解除"; +"location_sharing_live_timer_selector_title" = "位置情報を共有する期間を選択してください。"; +"location_sharing_live_timer_selector_short" = "15分"; +"location_sharing_live_timer_selector_medium" = "1時間"; +"location_sharing_live_timer_selector_long" = "8時間"; +"location_sharing_live_no_user_locations_error_title" = "ユーザーの位置情報はありません"; +"location_sharing_live_stop_sharing_error" = "位置情報の共有の停止に失敗しました"; +"location_sharing_live_stop_sharing_progress" = "位置情報の共有を停止"; +"location_sharing_live_lab_promotion_text" = "注意:これは一時的な実装による試験機能です。あなたの位置情報の履歴はルームのメンバーに対して永続的に閲覧可能となります。"; +"location_sharing_live_lab_promotion_title" = "位置情報(ライブ)の共有"; +"location_sharing_live_lab_promotion_activation" = "位置情報(ライブ)の共有を有効にする"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"user_sessions_overview_title" = "セッション"; +"user_sessions_overview_security_recommendations_section_title" = "セキュリティーに関する勧告"; +"user_sessions_overview_security_recommendations_section_info" = "以下の勧告に従い、アカウントのセキュリティーを改善しましょう。"; +"user_sessions_overview_security_recommendations_unverified_title" = "未認証のセッション"; +"user_sessions_overview_security_recommendations_inactive_title" = "非アクティブなセッション"; +"user_sessions_overview_security_recommendations_inactive_info" = "使用していない古いセッション(90日以上使用されていません)からサインアウトすることを検討してください。"; +"user_sessions_overview_other_sessions_section_title" = "その他のセッション"; +"user_sessions_overview_other_sessions_section_info" = "セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。"; +"user_sessions_show_location_info" = "IPアドレスを表示"; +"user_sessions_hide_location_info" = "IPアドレスを表示しない"; +"user_sessions_overview_current_session_section_title" = "現在のセッション"; +"user_sessions_view_all_action" = "全て表示(%d)"; +"user_session_verified" = "認証済のセッション"; +"user_session_unverified" = "未認証のセッション"; +"user_session_verification_unknown" = "認証の状態が不明です"; +"user_session_verified_short" = "認証済"; +"user_session_unverified_short" = "未認証"; +"user_session_verification_unknown_short" = "不明"; +"user_session_verify_action" = "セッションを認証"; +"user_session_view_details" = "詳細を表示"; +"major_update_learn_more_action" = "詳細を表示"; +"user_session_learn_more" = "詳細を表示"; +"user_session_verified_additional_info" = "現在のセッションは安全なメッセージのやりとりに対応しています。"; +"user_session_unverified_additional_info" = "より安全なメッセージのやりとりのために、現在のセッションを認証しましょう。"; +"user_other_session_unverified_additional_info" = "セキュリティーと安定性の観点から、このセッションを認証するかサインアウトしてください。"; +"user_other_session_permanently_unverified_additional_info" = "このセッションは暗号化をサポートしていないため、認証できません。"; +"user_other_session_verified_additional_info" = "このセッションは安全なメッセージのやりとりの準備ができています。"; +"user_session_push_notifications" = "プッシュ通知"; +"user_session_got_it" = "了解"; +"user_session_verified_session_title" = "認証済のセッション"; +"user_session_unverified_session_title" = "未認証のセッション"; +"user_session_inactive_session_title" = "非アクティブなセッション"; +"user_session_rename_session_title" = "セッション名を変更"; +"user_other_session_security_recommendation_title" = "その他のセッション"; +"user_other_session_unverified_sessions_header_subtitle" = "セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。"; +"user_other_session_current_session_details" = "現在のセッション"; +"user_other_session_verified_sessions_header_subtitle" = "セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。"; +"user_other_session_filter" = "絞り込み"; +"user_other_session_filter_menu_all" = "全てのセッション"; +"user_other_session_filter_menu_verified" = "認証済"; +"user_other_session_filter_menu_unverified" = "未認証"; +"user_other_session_filter_menu_inactive" = "非アクティブ"; +"user_other_session_no_inactive_sessions" = "使用していないセッションはありません。"; +"user_other_session_no_verified_sessions" = "認証済のセッションはありません。"; +"user_other_session_no_unverified_sessions" = "未認証のセッションはありません。"; +"user_other_session_clear_filter" = "絞り込みを解除"; +"user_other_session_menu_select_sessions" = "セッションを選択"; +"user_other_session_menu_sign_out_sessions" = "%@件のセッションからサインアウト"; +"device_name_desktop" = "%@デスクトップ"; +"device_name_unknown" = "不明なクライアント"; +"device_type_name_desktop" = "デスクトップ"; +"device_type_name_web" = "ウェブ"; +"device_type_name_mobile" = "携帯端末"; +"device_type_name_unknown" = "不明"; +"user_session_details_title" = "セッションの詳細"; +"user_session_details_session_section_header" = "セッション"; +"user_session_details_application_section_header" = "アプリケーション"; +"user_session_details_device_section_header" = "端末"; +"user_session_details_session_name" = "セッション名"; +"user_session_details_session_id" = "セッションID"; +"user_session_details_last_activity" = "直近のアクティビティー"; +"user_session_details_device_ip_address" = "IPアドレス"; +"user_session_details_device_browser" = "ブラウザー"; +"user_session_details_device_os" = "オペレーティングシステム"; +"user_session_details_application_name" = "名前"; +"user_session_details_application_version" = "バージョン"; +"user_session_details_application_url" = "URL"; +"user_session_overview_current_session_title" = "現在のセッション"; +"user_session_overview_session_title" = "セッション"; +"user_session_overview_session_details_button_title" = "セッションの詳細"; +"wysiwyg_composer_start_action_stickers" = "ステッカー"; +"wysiwyg_composer_start_action_attachments" = "添付ファイル"; +"wysiwyg_composer_start_action_polls" = "アンケート"; +"wysiwyg_composer_start_action_location" = "位置情報"; +"wysiwyg_composer_start_action_camera" = "カメラ"; +"wysiwyg_composer_start_action_voice_broadcast" = "音声配信"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "太字にする"; +"wysiwyg_composer_format_action_italic" = "斜字体にする"; + + + +// Links +"wysiwyg_composer_link_action_text" = "テキスト"; +"wysiwyg_composer_link_action_link" = "リンク"; +"wysiwyg_composer_link_action_create_title" = "リンクを作成"; +"wysiwyg_composer_link_action_edit_title" = "リンクを編集"; +"deselect_all" = "全ての選択を解除"; +"ignore_user" = "ユーザーを無視"; +"notice_room_name_removed_for_dm" = "%@が名前を削除しました"; +// New +"notice_room_join_rule_invite" = "%@がこのルームを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_for_dm" = "%@がこれを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_by_you" = "このルームを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_invite_by_you_for_dm" = "これを「招待者のみ参加可能」に設定しました。"; +"notice_room_join_rule_public" = "%@がルームを公開しました。"; +"notice_room_join_rule_public_for_dm" = "%@が公開しました。"; +"notice_room_join_rule_public_by_you" = "ルームを公開しました。"; +"notice_room_join_rule_public_by_you_for_dm" = "公開しました。"; +"notice_room_power_level_intro_for_dm" = "メンバーの権限レベル:"; +"notice_room_aliases_for_dm" = "エイリアス:%@"; +"notice_voice_broadcast_live" = "ライブ配信"; +"notice_voice_broadcast_ended" = "%@が音声配信を終了しました。"; +"notice_voice_broadcast_ended_by_you" = "音声配信を終了しました。"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "この暗号化されたメッセージの真正性はこの端末では保証できません。"; +"room_left_for_dm" = "退出しました"; +"message_reply_to_sender_sent_their_live_location" = "位置情報(ライブ)。"; +"attachment_unsupported_preview_title" = "プレビューできません"; +"attachment_unsupported_preview_message" = "このファイルの種類はサポートしていません。"; +"microphone_access_not_granted_for_voice_message" = "音声メッセージにはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; +"notice_room_third_party_invite_for_dm" = "%@が%@を招待しました"; +"notice_room_name_changed_for_dm" = "%@が名前を%@に変更しました。"; +"notice_room_third_party_invite_by_you" = "%@にルームへの招待を送りました"; +"notice_room_third_party_invite_by_you_for_dm" = "%@を招待しました"; +"notice_room_third_party_registered_invite_by_you" = "%@の招待を受け入れました"; +"notice_room_reject_by_you" = "招待を拒否しました"; +"notice_room_withdraw_by_you" = "%@の招待を取り下げました"; +"notice_declined_video_call_by_you" = "通話を拒否しました"; +"notice_room_history_visible_to_anyone_by_you" = "今後のルーム履歴を「誰でも」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_by_you" = "今後のルーム履歴を「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_by_you_for_dm" = "今後のメッセージを「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "今後のメッセージを「全員 (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "今後のルーム履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; +"call_more_actions_audio_use_device" = "端末のスピーカー"; +"call_more_actions_transfer" = "転送"; +"call_voice_with_user" = "%@との音声通話"; +"call_transfer_to_user" = "%@に転送"; +"pin_protection_confirm_pin" = "PINコードを確認してください"; +"pin_protection_choose_pin" = "PINコードを設定してください"; +"pin_protection_choose_pin_welcome_after_register" = "ようこそ。"; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "おかえりなさい。"; +"major_update_done_action" = "了解"; +"cross_signing_setup_banner_subtitle" = "他の端末をより簡単に認証"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "暗号化の設定"; +"secrets_reset_reset_action" = "リセット"; +"secrets_reset_warning_message" = "履歴とメッセージが消去され、信頼済の端末、信頼済のユーザーが取り消されます。"; +"secrets_reset_warning_title" = "全てをリセットすると"; +"secrets_reset_information" = "この端末を認証できる他の端末が全くない場合にのみ、続行してください。"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "全てリセット"; + + +"secrets_setup_recovery_passphrase_summary_title" = "セキュリティーフレーズを保存"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "パスフレーズを確認"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "確認"; +"secrets_setup_recovery_passphrase_confirm_information" = "確認のため、セキュリティーフレーズを再入力してください。"; +"secrets_setup_recovery_passphrase_additional_information" = "Matrixのアカウントパスワードと違うものにしてください。"; +"secrets_setup_recovery_passphrase_information" = "あなたしか知らないセキュリティーフレーズを入力してください。サーバーで機密情報を保護するために使用します。"; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "セキュリティーフレーズを設定"; +"secrets_setup_recovery_key_storage_alert_title" = "大切に保護しましょう"; +"secrets_setup_recovery_key_export_action" = "保存"; +"secrets_setup_recovery_key_loading" = "読み込んでいます…"; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "セキュリティーキーを保存"; +"secrets_recovery_with_key_invalid_recovery_key_title" = "機密ストレージにアクセスできません"; +"secrets_recovery_with_key_recover_action" = "鍵を使用"; +"secrets_recovery_with_key_recovery_key_placeholder" = "セキュリティーキーを入力"; +"secrets_recovery_with_key_recovery_key_title" = "入力"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "続行するにはセキュリティーキーを入力してください。"; + +// Recover with key + +"secrets_recovery_with_key_title" = "セキュリティーキー"; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "機密ストレージにアクセスできません"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "。"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "セキュリティーフレーズが分かりませんか?そんなときは "; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "セキュリティーフレーズを入力"; +"secrets_recovery_with_passphrase_passphrase_title" = "入力"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "セキュリティーフレーズ"; +"secrets_recovery_reset_action_part_2" = "全てリセット"; +"user_verification_session_details_verify_action_current_user_manually" = "テキストを使って手動で認証"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "この端末でスキャン"; +"emoji_picker_activity_category" = "アクティビティー"; +"device_verification_emoji_corn" = "とうもろこし"; +"device_verification_emoji_strawberry" = "いちご"; +"device_verification_emoji_apple" = "リンゴ"; +"device_verification_emoji_banana" = "バナナ"; +"device_verification_emoji_fire" = "炎"; +"device_verification_emoji_cloud" = "雲"; +"device_verification_emoji_moon" = "月"; +"device_verification_emoji_globe" = "地球"; +"device_verification_emoji_mushroom" = "きのこ"; +"device_verification_emoji_cactus" = "サボテン"; +"device_verification_emoji_tree" = "木"; +"device_verification_emoji_flower" = "花"; +"device_verification_emoji_butterfly" = "ちょうちょ"; +"device_verification_emoji_octopus" = "たこ"; +"device_verification_emoji_fish" = "魚"; +"device_verification_emoji_turtle" = "亀"; +"device_verification_emoji_penguin" = "ペンギン"; +"device_verification_emoji_rooster" = "ニワトリ"; +"device_verification_emoji_panda" = "パンダ"; +"device_verification_emoji_rabbit" = "うさぎ"; +"device_verification_emoji_elephant" = "ゾウ"; +"device_verification_emoji_pig" = "ブタ"; +"device_verification_emoji_unicorn" = "ユニコーン"; +"device_verification_emoji_horse" = "馬"; +"device_verification_emoji_lion" = "ライオン"; +"device_verification_emoji_cat" = "猫"; + +// MARK: Emoji +"device_verification_emoji_dog" = "犬"; + +// User + +"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; +"key_verification_verified_new_session_title" = "新しいセッションを認証しました!"; +"device_verification_verified_got_it_button" = "了解"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "認証されました!"; + +// Device + +"device_verification_verify_wait_partner" = "相手の承認を待機しています…"; +"key_verification_manually_verify_device_validate_action" = "認証"; +"key_verification_manually_verify_device_additional_information" = "一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。"; +"key_verification_manually_verify_device_key_title" = "セッションキー"; +"key_verification_manually_verify_device_id_title" = "セッションID"; +"key_verification_manually_verify_device_name_title" = "セッション名"; +"key_verification_manually_verify_device_instruction" = "他のセッションのユーザー設定で、以下を比較して承認してください:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "テキストを使って手動で認証"; +"key_verification_verify_sas_additional_information" = "セキュリティーを最大限に高めるには、対面で行うか、他の信頼できる通信手段を使用してください。"; +"key_verification_verify_sas_title_number" = "番号を比較"; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "既存のセッションにアクセスできない場合"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "セキュリティーフレーズまたはセキュリティーキーを使用"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "セキュリティーキーを使用"; +"device_verification_self_verify_wait_new_sign_in_title" = "このログインを認証"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "確認"; +"key_verification_alert_body" = "アカウントが安全かどうか確認してください。"; + +// Unverified sessions +"key_verification_alert_title" = "未認証のセッションがあります"; +"key_verification_self_verify_current_session_alert_validate_action" = "認証"; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "このセッションを認証"; +"device_verification_self_verify_start_waiting" = "待機しています…"; +"device_verification_self_verify_start_information" = "新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。"; +"device_verification_self_verify_start_verify_action" = "認証を開始"; +"device_verification_start_use_legacy_action" = "レガシー認証を使用"; +"device_verification_start_verify_button" = "認証を開始"; + +// MARK: Start +"device_verification_start_title" = "短い文字列を比較して認証"; +"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済としてマークされ、自分のセッションも相手に信頼済としてマークされます。"; +"device_verification_incoming_description_1" = "このセッションを認証して、信頼済としてマークします。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; + +// MARK: Incoming +"device_verification_incoming_title" = "認証のリクエストが届いています"; +"device_verification_error_cannot_load_device" = "セッションの情報を読み込めません。"; +"device_verification_cancelled_by_me" = "認証がキャンセルされました。理由:%@"; +"device_verification_cancelled" = "相手が認証をキャンセルしました。"; +"device_verification_security_advice_number" = "数字を比較して、同じ順番で現れているのを確認してください。"; +"key_verification_this_session_title" = "このセッションを認証"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "セッションを認証"; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "暗号化されたメッセージは不要です"; +"sign_out_key_backup_in_progress_alert_title" = "鍵をバックアップしています。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "暗号化されたメッセージは不要です"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "セキュアバックアップを使用開始"; +"sign_out_non_existing_key_backup_alert_title" = "今ここでサインアウトすると、あなたの暗号化されたメッセージにアクセスできなくなります"; +"sign_out_confirmation_message" = "サインアウトしてよろしいですか?"; + +// MARK: Sign out warning + +"sign_out" = "サインアウト"; + +// Success + +"key_backup_recover_success_info" = "バックアップを復元しました!"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "セキュリティーキーを無くしましたか? 設定で新しいセキュリティーキーを設定できます。"; +"key_backup_recover_from_recovery_key_recover_action" = "履歴のロックを解除"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "セキュリティーキーを入力"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "入力"; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "セキュリティーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます"; From 08dd5d0f2f5d8650e0af9bfc1c6c132946e4c7bf Mon Sep 17 00:00:00 2001 From: Vri Date: Mon, 30 Jan 2023 11:57:23 +0000 Subject: [PATCH 368/468] Translated using Weblate (German) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 7f1459e8b..c044c2ffd 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2720,9 +2720,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migriere Daten\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Krypto-SDK ist aktiviert. Zum Deaktivieren, bitte die App neu installieren"; -"settings_labs_confirm_crypto_sdk" = "Dies kann nicht rückgängig gemacht werden"; -"settings_labs_enable_crypto_sdk" = "Rust-basiertes Krypto-SDK aktivieren"; +"settings_labs_disable_crypto_sdk" = "Ende-zu-Ende-Verschlüsselung 2.0 (zum Deaktivieren abmelden)"; +"settings_labs_confirm_crypto_sdk" = "Diese Option wird eine neue, schnellere und zuverlässigere Ende-zu-Ende-Verschlüsselungs-Engine aktivieren, die in Rust geschrieben wurde. Einmal aktiviert, wirst du dich abmelden müssen, um sie zu deaktivieren. Möchtest du fortfahren?"; +"settings_labs_enable_crypto_sdk" = "Ende-zu-Ende-Verschlüsselung 2.0"; "poll_history_no_past_poll_period_text" = "Für die vergangenen %@ Tage sind keine beendeten Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; "poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; "poll_history_load_more" = "Weitere Umfragen laden"; From f3f63369b3b5dcb149da6adafd5935aea22c1679 Mon Sep 17 00:00:00 2001 From: Phl-Pro Date: Mon, 30 Jan 2023 15:40:54 +0000 Subject: [PATCH 369/468] Translated using Weblate (French) Currently translated at 95.4% (2262 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 4d9490190..0a846c5c1 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -103,7 +103,7 @@ "room_recents_no_conversation" = "Aucun salon"; "room_recents_low_priority_section" = "PRIORITÉ BASSE"; "room_recents_invites_section" = "INVITATIONS"; -"room_recents_start_chat_with" = "Commencer une discussion"; +"room_recents_start_chat_with" = "Nouveau message direct"; "room_recents_create_empty_room" = "Créer un salon"; "room_recents_join_room" = "Rejoindre le salon"; "room_recents_join_room_title" = "Rejoindre un salon"; @@ -113,7 +113,7 @@ "people_conversation_section" = "DISCUSSIONS"; "people_no_conversation" = "Aucune discussion"; // Rooms tab -"room_directory_no_public_room" = "Aucun salon public disponible"; +"room_directory_no_public_room" = "Aucun forum disponible"; // Groups tab "group_invite_section" = "INVITATIONS"; "group_section" = "COMMUNAUTÉS"; @@ -166,20 +166,20 @@ "room_participants_now" = "maintenant"; "room_participants_ago" = "d’inactivité"; "room_participants_action_section_admin_tools" = "Outils d’administration"; -"room_participants_action_section_direct_chats" = "Conversations privées"; +"room_participants_action_section_direct_chats" = "Messages directs"; "room_participants_action_section_devices" = "Sessions"; "room_participants_action_section_other" = "Options"; "room_participants_action_invite" = "Inviter"; "room_participants_action_leave" = "Quitter ce salon"; "room_participants_action_remove" = "Exclure de ce salon"; -"room_participants_action_ban" = "Bannir de ce salon"; +"room_participants_action_ban" = "Interdire l’accès au salon (définitif)"; "room_participants_action_unban" = "Révoquer le bannissement"; "room_participants_action_ignore" = "Masquer tous les messages de cet utilisateur"; "room_participants_action_unignore" = "Afficher tous les messages de cet utilisateur"; "room_participants_action_set_default_power_level" = "Rétrograder en utilisateur normal"; "room_participants_action_set_moderator" = "Nommer modérateur"; "room_participants_action_set_admin" = "Nommer administrateur"; -"room_participants_action_start_new_chat" = "Commencer une nouvelle discussion"; +"room_participants_action_start_new_chat" = "Nouveau message direct"; "room_participants_action_start_voice_call" = "Commencer un appel audio"; "room_participants_action_start_video_call" = "Commencer un appel vidéo"; "room_participants_action_mention" = "Mentionner"; @@ -399,7 +399,7 @@ "directory_server_picker_title" = "Sélectionner un répertoire"; "directory_server_all_rooms" = "Tous les salons sur le serveur %@"; "directory_server_all_native_rooms" = "Tous les salons Matrix natifs"; -"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses salons publics"; +"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses forums"; "directory_server_placeholder" = "matrix.org"; // Others "or" = "ou"; @@ -407,7 +407,7 @@ "today" = "Aujourd’hui"; "yesterday" = "Hier"; "network_offline_prompt" = "La connexion Internet semble être hors-ligne."; -"public_room_section_title" = "Salons publics (sur %@) :"; +"public_room_section_title" = "Forums (sur %@) :"; "bug_report_prompt" = "L’application s’est arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport d’anomalie ?"; "rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport d’anomalie ?"; "do_not_ask_again" = "Ne plus demander"; @@ -1211,8 +1211,8 @@ "create_room_section_header_address" = "ADRESSE"; "create_room_show_in_directory" = "Afficher le salon dans le répertoire"; "create_room_section_footer_type" = "Les personnes ne rejoignent un salon privé que sur invitation."; -"create_room_type_public" = "Salon public (tout le monde)"; -"create_room_type_private" = "Salon privé (seulement sur invitation)"; +"create_room_type_public" = "Forum (tout le monde)"; +"create_room_type_private" = "Salon (seulement sur invitation)"; "create_room_section_header_type" = "QUI PEUT Y ACCÉDER"; "create_room_section_footer_encryption" = "Le chiffrement ne peut pas être désactivé ensuite."; "create_room_enable_encryption" = "Activer le chiffrement"; @@ -1317,7 +1317,7 @@ "room_details_room_name_for_dm" = "Nom"; "room_details_photo_for_dm" = "Photo"; "room_details_title_for_dm" = "Détails"; -"settings_show_NSFW_public_rooms" = "Afficher les salons publics au contenu choquant"; +"settings_show_NSFW_public_rooms" = "Afficher les forums au contenu choquant"; "external_link_confirmation_message" = "Le lien %@ vous emmène vers un autre site : %@\n\nÊtes vous sûr de vouloir poursuivre ?"; "external_link_confirmation_title" = "Inspectez ce lien"; "room_open_dialpad" = "Pavé de numérotation"; @@ -1494,7 +1494,7 @@ "spaces_empty_space_title" = "Cet espace n’a pas (encore) de salon"; "space_tag" = "espace"; "spaces_suggested_room" = "Recommandé"; -"spaces_explore_rooms" = "Parcourir les salons"; +"spaces_explore_rooms" = "Rejoindre un forum"; "leave_space_and_all_rooms_action" = "Quitter tous les salons et espaces"; "leave_space_only_action" = "Ne quitter aucun salon"; "leave_space_message_admin_warning" = "Vous êtes administrateur de cet espace. Assurez-vous d’avoir transmis les droits d’administration à un autre membre avant de partir."; @@ -1723,7 +1723,7 @@ "set_default_power_level" = "Réinitialiser le rang"; "set_moderator" = "Nommer modérateur"; "set_admin" = "Nommer administrateur"; -"start_chat" = "Nouvelle conversation privée"; +"start_chat" = "Nouveau message direct"; "start_voice_call" = "Commencer un appel audio"; "start_video_call" = "Commencer un appel vidéo"; "mention" = "Mentionner"; @@ -1959,8 +1959,8 @@ "membership_ban" = "Banni"; "num_members_one" = "%@ utilisateur"; "num_members_other" = "%@ utilisateurs"; -"kick" = "Expulser"; -"ban" = "Bannir"; +"kick" = "Retirer du salon (réversible)"; +"ban" = "Interdire l’accès au salon (définitif)"; "unban" = "Révoquer le bannissement"; "message_unsaved_changes" = "Il y a des modifications non enregistrées. Quitter les annulera."; // Login Screen @@ -2465,7 +2465,7 @@ "room_recents_recently_viewed_section" = "Récemment vus"; "all_chats_nothing_found_placeholder_message" = "Essayez d’affiner votre recherche."; "all_chats_nothing_found_placeholder_title" = "Aucun résultat."; -"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non-lus s’afficheront lorsque vous en aurez."; +"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non lus s’afficheront lorsque vous en aurez."; "all_chats_empty_list_placeholder_title" = "Plus rien à voir."; "all_chats_empty_view_information" = "La messagerie sécurisée tout en un pour les équipes, les amis, et les organisations. Créez une discussion ou rejoignez un salon pour démarrer."; "all_chats_empty_space_information" = "Les espaces sont un nouveau moyen de grouper les salons et les gens. Ajoutez un salon, ou créez en un nouveau à l’aide du bouton en bas à droite."; @@ -2481,14 +2481,14 @@ "all_chats_edit_layout_add_filters_title" = "Filtrez vos messages"; "all_chats_edit_layout_add_section_message" = "Épinglez des sections à l’accueil pour y accéder plus rapidement"; "all_chats_edit_layout_add_section_title" = "Ajouter une section à l’accueil"; -"all_chats_edit_layout_unreads" = "Non-lus"; +"all_chats_edit_layout_unreads" = "Non lus"; "all_chats_edit_layout_recents" = "Récents"; "all_chats_edit_layout" = "Préférences d’agencement"; "all_chats_section_title" = "Discussions"; // Mark: - All Chats -"all_chats_title" = "Tous mes chats"; +"all_chats_title" = "Accueil"; "spaces_subspace_creation_visibility_message" = "L’espace créé sera ajouté à %@."; "spaces_subspace_creation_visibility_title" = "Quel type de sous-espace voulez-vous créer ?"; "spaces_explore_rooms_format" = "Parcourir %@"; From 55788c2145f263261691d08e6110a7039d1bfd51 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 18:07:54 +0000 Subject: [PATCH 370/468] Translated using Weblate (Japanese) Currently translated at 88.1% (2091 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 161 +++++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 15 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index ee907d40a..f1fffe2f0 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -32,7 +32,7 @@ "rename" = "名前変更"; "collapse" = "折りたたむ"; "send_to" = "%@さんへ送信"; -"sending" = "送信中"; +"sending" = "送信しています"; // Authentication "auth_login" = "ログイン"; "auth_register" = "利用者登録"; @@ -133,7 +133,7 @@ "search_in_progress" = "検索しています…"; // Directory "directory_cell_title" = "ルーム一覧を見る"; -"directory_cell_description" = "%tuつのルーム"; +"directory_cell_description" = "%tu個のルーム"; "directory_search_results_title" = "ルーム一覧の検索結果"; "directory_searching_title" = "ルーム一覧を検索しています…"; "directory_search_fail" = "一覧を取得できませんでした"; @@ -498,12 +498,12 @@ "group_home_one_member_format" = "1名のメンバー"; "group_home_multi_members_format" = "%tu名のメンバー"; "group_home_one_room_format" = "1つのルーム"; -"group_home_multi_rooms_format" = "%tuつのルーム"; +"group_home_multi_rooms_format" = "%tu個のルーム"; "group_invitation_format" = "%@がこのコミュニティーにあなたを招待しました"; // Group participants "group_participants_add_participant" = "参加者を追加"; -"group_participants_leave_prompt_title" = "グループを退出"; -"group_participants_leave_prompt_msg" = "グループを退出してよろしいですか?"; +"group_participants_leave_prompt_title" = "グループから退出"; +"group_participants_leave_prompt_msg" = "グループから退出してよろしいですか?"; "group_participants_remove_prompt_title" = "確認"; "group_participants_remove_prompt_msg" = "このグループから%@を削除してよろしいですか?"; "group_participants_invite_prompt_title" = "確認"; @@ -767,7 +767,7 @@ "auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; "callbar_return" = "かけ直す"; "callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; -"callbar_only_multiple_paused" = "一時停止した%@の通話"; +"callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "通話の一時停止"; "store_promotional_text" = "オープンネットワーク上でプライバシーを保護したチャットアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、サードパーティによるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; @@ -1504,7 +1504,7 @@ "ssl_only_accept" = "サーバー管理者が上記のものと一致する指紋を発行した場合にのみ、証明書を受け入れてください。"; "unignore" = "無視しない"; "notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化をオンにしました。"; -"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化をオンにしました(不明なアルゴリズム %2$@)。"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %@)をオンにしました。"; "device_details_rename_prompt_title" = "セッション名"; "account_error_push_not_allowed" = "通知は許可されていません"; "notice_room_third_party_revoked_invite" = "%@が%@のルームへの招待を取り消しました"; @@ -1627,12 +1627,12 @@ "onboarding_splash_page_3_message" = "エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; "onboarding_splash_page_3_title" = "安全なメッセージ。"; "onboarding_splash_page_2_message" = "会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。"; -"onboarding_splash_page_2_title" = "主導権はあなたにある。"; +"onboarding_splash_page_2_title" = "主導権を握るのは、あなたです。"; "onboarding_splash_page_1_message" = "オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。"; -"saving" = "保存中"; +"saving" = "保存しています"; // Activities -"loading" = "ロード中"; +"loading" = "読み込んでいます"; "confirm" = "確認"; "edit" = "編集"; "suggest" = "サジェスト"; @@ -1640,7 +1640,7 @@ "existing" = "既存"; "new_word" = "新規"; "stop" = "停止"; -"spaces_creation_post_process_creating_space_task" = "%@を作成中"; +"spaces_creation_post_process_creating_space_task" = "%@を作成しています"; "side_menu_coach_message" = "右にスワイプまたはタップで全てのルームが表示されます"; "spaces_creation_post_process_creating_space" = "スペースを作成中"; "spaces_creation_add_rooms_message" = "このスペースはあなた専用のため、他の人に通知されることはありません。この設定は後から変更できます。"; @@ -1695,7 +1695,7 @@ "home_context_menu_notifications" = "通知"; "home_context_menu_make_dm" = "連絡先に移動"; "home_context_menu_make_room" = "ルームに移動"; -"leave_space_title" = "%@ を退出"; +"leave_space_title" = "%@から退出"; "room_participants_leave_success" = "ルームから退出しました"; "room_participants_leave_processing" = "退出しています"; "event_formatter_group_call_leave" = "退出"; @@ -1703,7 +1703,7 @@ // Mark: Leave space -"leave_space_action" = "スペースを退出"; +"leave_space_action" = "スペースから退出"; "leave_space_selection_title" = "ルームを選択"; "create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; "create_room_suggest_room" = "スペースメンバーにおすすめ"; @@ -2152,7 +2152,7 @@ "wysiwyg_composer_link_action_edit_title" = "リンクを編集"; "deselect_all" = "全ての選択を解除"; "ignore_user" = "ユーザーを無視"; -"notice_room_name_removed_for_dm" = "%@が名前を削除しました"; +"notice_room_name_removed_for_dm" = "%@がルーム名を削除しました"; // New "notice_room_join_rule_invite" = "%@がこのルームを「招待者のみ参加可能」に設定しました。"; "notice_room_join_rule_invite_for_dm" = "%@がこれを「招待者のみ参加可能」に設定しました。"; @@ -2180,7 +2180,7 @@ "notice_room_third_party_registered_invite_by_you" = "%@の招待を受け入れました"; "notice_room_reject_by_you" = "招待を拒否しました"; "notice_room_withdraw_by_you" = "%@の招待を取り下げました"; -"notice_declined_video_call_by_you" = "通話を拒否しました"; +"notice_declined_video_call_by_you" = "着信を拒否しました"; "notice_room_history_visible_to_anyone_by_you" = "今後のルーム履歴を「誰でも」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_by_you" = "今後のルーム履歴を「メンバーのみ」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_by_you_for_dm" = "今後のメッセージを「メンバーのみ」閲覧可能に設定しました。"; @@ -2374,3 +2374,134 @@ // Recover from recovery key "key_backup_recover_from_recovery_key_info" = "セキュリティーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます"; +"call_video_with_user" = "%@とのビデオ通話"; +"call_more_actions_hold" = "保留"; +"notice_encryption_enabled_unknown_algorithm_by_you" = "エンドツーエンド暗号化(認識されていないアルゴリズム %@)をオンにしました。"; +"notice_room_name_removed_by_you_for_dm" = "ルーム名を削除しました"; +"notice_room_third_party_revoked_invite_by_you" = "%@のルームへの招待を取り消しました"; +"notice_declined_video_call" = "%@は着信を拒否しました"; +"attachment_size_prompt_message" = "これは設定から無効にできます。"; +"message_reply_to_sender_sent_their_location" = "位置情報を共有しました。"; +"message_reply_to_sender_sent_a_voice_message" = "音声メッセージを送信しました。"; +"wysiwyg_composer_start_action_text_formatting" = "テキストの装飾"; +"user_session_details_device_model" = "形式"; +"user_inactive_session_item_with_date" = "90日以上使用されていません(%@)"; +"user_inactive_session_item" = "90日以上使用されていません"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +"user_other_session_selected_count" = "%d件選択済"; +"user_session_inactive_session_description" = "非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。\n\n使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。"; +"user_session_permanently_unverified_session_description" = "このセッションは暗号化をサポートしていないため、認証できません。\n\nこのセッションでは、暗号化が有効になっているルームに参加することができません。\n\nセキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。"; +"user_sessions_overview_security_recommendations_unverified_info" = "未認証のセッションを認証するか、サインアウトしてください。"; +"location_sharing_live_list_item_time_left" = "残り%@"; +"location_sharing_live_viewer_title" = "位置情報"; +"location_sharing_live_map_callout_title" = "位置情報を共有"; +"location_sharing_pin_drop_share_title" = "この位置情報を送信"; +"location_sharing_static_share_title" = "現在の位置情報を送信"; +"location_sharing_map_loading_error" = "地図を読み込めません\nこのホームサーバーは地図を読み込むよう設定されていません"; +"location_sharing_allow_background_location_cancel_action" = "後で"; +"location_sharing_allow_background_location_validate_action" = "設定"; +"location_sharing_allow_background_location_title" = "アクセスを許可"; +"location_sharing_settings_header" = "位置情報の共有"; +"location_sharing_open_open_street_maps" = "OpenStreetMapで開く"; +"location_sharing_open_apple_maps" = "Appleマップで開く"; +"location_sharing_invalid_authorization_not_now" = "後で"; +"location_sharing_locating_user_error_title" = "%@は位置情報にアクセスできませんでした。後でもう一度やり直してください。"; +"location_sharing_post_failure_subtitle" = "%@は位置情報を送信できませんでした。後でもう一度やり直してください。"; +"location_sharing_post_failure_title" = "位置情報を送信できませんでした"; +"location_sharing_close_action" = "閉じる"; +"poll_timeline_ended_text" = "アンケートを終了しました"; +"poll_timeline_decryption_error" = "復号エラーにより、いくつかの投票はカウントできません"; +"poll_history_fetching_error" = "アンケートの取得中にエラーが発生しました。"; +"poll_history_no_past_poll_text" = "このルームに過去のアンケートはありません"; +"poll_history_no_active_poll_text" = "このルームに実施中のアンケートはありません"; +"poll_history_past_segment_title" = "過去のアンケート"; +"poll_history_active_segment_title" = "実施中のアンケート"; +"poll_history_loading_text" = "アンケートを表示しています"; + +// MARK: - Polls history + +"poll_history_title" = "アンケートの履歴"; +"space_detail_nav_title" = "スペースの詳細"; + +// MARK: - Room invites + +"room_invites_empty_view_title" = "新着はありません。"; +"all_chats_edit_menu_space_settings" = "スペースの設定"; +"all_chats_edit_menu_leave_space" = "%@から退出"; +"all_chats_user_menu_accessibility_label" = "ユーザーメニュー"; +"room_recents_recently_viewed_section" = "最近表示したルーム"; +"all_chats_empty_space_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。"; +"all_chats_edit_layout_sorting_options_title" = "メッセージを並び替える"; +"all_chats_edit_layout_add_filters_title" = "メッセージを絞り込む"; +"version_check_modal_action_title_supported" = "了解"; +"voice_broadcast_recorder_connection_error" = "接続エラー - 録音を停止しました"; +"voice_broadcast_connection_error_message" = "録音を開始できません。後でもう一度やり直してください。"; +"voice_broadcast_connection_error_title" = "接続エラー"; +"voice_broadcast_voip_cannot_start_description" = "ライブ配信を録音しているため、通話を開始できません。通話を開始するには、ライブ配信を終了してください。"; +"voice_broadcast_voip_cannot_start_title" = "通話を開始できません"; +"voice_broadcast_stop_alert_agree_button" = "はい、停止"; +"voice_broadcast_stop_alert_description" = "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。"; +"voice_broadcast_stop_alert_title" = "ライブ配信を停止しますか?"; +"voice_broadcast_buffering" = "バッファリングしています…"; +"voice_broadcast_time_left" = "残り%@"; +"voice_broadcast_tile" = "音声配信"; +"voice_broadcast_live" = "ライブ"; +"voice_broadcast_playback_lock_screen_placeholder" = "音声配信"; +"voice_broadcast_playback_loading_error" = "この音声配信を再生できません。"; +"voice_broadcast_already_in_progress_message" = "既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。"; +"voice_broadcast_blocked_by_someone_else_message" = "他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。"; +"voice_broadcast_permission_denied_message" = "このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。"; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "新しい音声配信を開始できません"; +"voice_message_broadcast_in_progress_message" = "ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください"; +"voice_message_broadcast_in_progress_title" = "音声メッセージを開始できません"; +"voice_message_lock_screen_placeholder" = "音声メッセージ"; +"voice_message_remaining_recording_time" = "残り%@"; + +// MARK: - Voice Messages + +"voice_message_release_to_send" = "押し続けて録音し、離すと送信"; +"side_menu_app_version" = "バージョン %@"; +"user_avatar_view_accessibility_hint" = "ユーザーのアバターを変更"; + +// MARK: - User avatar view + +"user_avatar_view_accessibility_label" = "アバター"; +"space_avatar_view_accessibility_hint" = "スペースのアバターを変更"; + +// MARK: Avatar + +"space_avatar_view_accessibility_label" = "アバター"; +"leave_space_selection_no_rooms" = "ルームを選択しない"; +"spaces_creation_post_process_creating_room" = "%@を作成しています"; +"spaces_creation_post_process_uploading_avatar" = "アバターをアップロードしています"; +"spaces_creation_invite_by_username_title" = "チームを招待"; +"spaces_creation_invite_by_username" = "ユーザー名で招待"; +"spaces_creation_sharing_type_title" = "誰と使いますか?"; +"spaces_creation_email_invites_email_title" = "電子メール"; +"spaces_creation_email_invites_title" = "チームを招待"; +"spaces_creation_new_rooms_support" = "サポート"; +"spaces_creation_new_rooms_random" = "ランダム"; +"spaces_creation_new_rooms_general" = "一般"; +"spaces_creation_new_rooms_room_name_title" = "ルーム名"; +"spaces_creation_private_space_title" = "あなたの非公開のスペース"; +"spaces_creation_public_space_title" = "あなたの公開スペース"; +"spaces_subspace_creation_visibility_title" = "作成するサブスペースの種類を選択してください"; + +// MARK: - Space Creation + +"spaces_creation_hint" = "スペースは、ルームや連絡先をグループ化する新しい方法です。"; +"spaces_add_space" = "スペースを追加"; +"spaces_add_room" = "ルームを追加"; +"spaces_invite_people" = "連絡先を招待"; +"space_public_join_rule" = "公開スペース"; +"space_private_join_rule" = "非公開のスペース"; +"spaces_no_result_found_title" = "検索結果がありません"; +"space_tag" = "スペース"; +"spaces_explore_rooms_one_room" = "1つのルーム"; +"spaces_explore_rooms_room_number" = "%@個のルーム"; +"leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; +"leave_space_only_action" = "どのルームからも退出しない"; From 60228c5acd64d94af794d590f33663f4139541bd Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Mon, 30 Jan 2023 15:20:40 +0000 Subject: [PATCH 371/468] Translated using Weblate (Japanese) Currently translated at 88.1% (2091 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index f1fffe2f0..7f280e315 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -768,7 +768,7 @@ "callbar_return" = "かけ直す"; "callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; -"callbar_only_single_paused" = "通話の一時停止"; +"callbar_only_single_paused" = "一時停止した通話"; "store_promotional_text" = "オープンネットワーク上でプライバシーを保護したチャットアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、サードパーティによるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; From 23b69cd2dd2f259bb0fc97dddc7f9bdac527a746 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 30 Jan 2023 16:06:41 +0000 Subject: [PATCH 372/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index b0adc41fc..d395f5b0d 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2911,9 +2911,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Перенесення даних\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Crypto SDK увімкнено. Щоб вимкнути, перевстановіть застосунок"; -"settings_labs_confirm_crypto_sdk" = "Дію не можна скасувати"; -"settings_labs_enable_crypto_sdk" = "Увімкнути новий заснований на rust Crypto SDK"; +"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування 2.0 (вийдіть, щоб вимкнути)"; +"settings_labs_confirm_crypto_sdk" = "Ця опція увімкне новий, швидший і надійніший механізм наскрізного шифрування, написаний на Rust. Після увімкнення вам потрібно буде вийти з системи, щоб вимкнути її. Бажаєте продовжити?"; +"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування 2.0"; "poll_history_load_more" = "Завантажити більше опитувань"; "poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; "poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; From 2dbaefbf5a422f7aa56af7911b19107be9b6866d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 30 Jan 2023 13:33:32 +0000 Subject: [PATCH 373/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index d16c075d9..dc7b4ce31 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2658,9 +2658,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Uus Crypto SDK on kasutusel. Tema väljalülitamiseks palun paigalda rakendus uuesti"; -"settings_labs_confirm_crypto_sdk" = "Seda toimingut ei saa tagasi pöörata"; -"settings_labs_enable_crypto_sdk" = "Võta kasutusele uus Rust-keelel põhinev Crypto SDK"; +"settings_labs_disable_crypto_sdk" = "Läbiva krüptimise versioon 2.0 on kasutusel (väljalülitamiseks pead välja logima)"; +"settings_labs_confirm_crypto_sdk" = "Selle valikuga võtad kasutusele uue, kiirema ja töökindlama läbiva krüptimise lahenduse, mis on kirjutatud programmeerimiskeeles Rust. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Kas sa soovid jätkata?"; +"settings_labs_enable_crypto_sdk" = "Võta kasutusele läbiva krüptimise versioon 2.0"; "poll_history_load_more" = "Laadi veel küsitlusi"; "poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; "poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; From 6afcbdb93dbfcfff013e54da8989f233ee0cbdcf Mon Sep 17 00:00:00 2001 From: Linerly Date: Mon, 30 Jan 2023 12:03:56 +0000 Subject: [PATCH 374/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 9687859bf..355012c46 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2913,9 +2913,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Memigrasikan data\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "SDK Kripto diaktifkan. Untuk menonaktifkan, mohon memasang ulang aplikasi"; -"settings_labs_confirm_crypto_sdk" = "Tindakan ini tidak dapat diurungkan"; -"settings_labs_enable_crypto_sdk" = "Aktifkan SDK Kripto baru berbasis Rust"; +"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung 2,0 (keluar dari akun untuk menonaktifkan)"; +"settings_labs_confirm_crypto_sdk" = "Opsi ini akan mengaktifkan mesin ditulis dalam Rust baru yang lebih cepat dan lebih andal untuk enkripsi ujung ke ujung. Setelah diaktifkan, Anda harus keluar dari akun untuk menonaktifkannya. Apakah Anda ingin melanjutkan?"; +"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung 2,0"; "poll_history_load_more" = "Muat lebih banyak pemungutan suara"; "poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya"; "poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; From 697b4fd28b6cbb7329c32dba81d7f4f63efdc99c Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 30 Jan 2023 14:05:38 +0000 Subject: [PATCH 375/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2371 of 2371 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index cb3f411de..ecb7e105e 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2909,9 +2909,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migrácia údajov\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Crypto SDK je povolené. Ak to chcete vypnúť, preinštalujte prosím aplikáciu"; -"settings_labs_confirm_crypto_sdk" = "Túto akciu nemožno vrátiť späť"; -"settings_labs_enable_crypto_sdk" = "Zapnúť nové Crypto SDK využívajúce Rust"; +"settings_labs_disable_crypto_sdk" = "End-to-end šifrovanie 2.0 (odhláste sa, aby ste ho vypli)"; +"settings_labs_confirm_crypto_sdk" = "Táto možnosť umožní použitie nového, rýchlejšieho a spoľahlivejšieho nástroja na end-to-end šifrovanie napísaného v jazyku Rust. Po jeho zapnutí sa budete musieť odhlásiť, aby ste ho mohli vypnúť. Chcete pokračovať?"; +"settings_labs_enable_crypto_sdk" = "End-to-end šifrovanie 2.0"; "poll_history_load_more" = "Načítať ďalšie ankety"; "poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; "poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; From f4f26b514de648c1e295900b996ab76d4c465c39 Mon Sep 17 00:00:00 2001 From: Vri Date: Mon, 30 Jan 2023 18:34:45 +0000 Subject: [PATCH 376/468] Translated using Weblate (German) Currently translated at 100.0% (2372 of 2372 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index c044c2ffd..03bf34ed0 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2728,3 +2728,4 @@ "poll_history_load_more" = "Weitere Umfragen laden"; "poll_history_loading_text" = "Zeige Umfragen an"; "poll_history_fetching_error" = "Fehler beim Laden der Umfragen."; +"key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen"; From 0c849c42ead58fe9e405ae2053c90ffba4938086 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 30 Jan 2023 18:38:42 +0000 Subject: [PATCH 377/468] Translated using Weblate (Japanese) Currently translated at 88.8% (2107 of 2372 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 7f280e315..f58f53f22 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2505,3 +2505,20 @@ "spaces_explore_rooms_room_number" = "%@個のルーム"; "leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; "leave_space_only_action" = "どのルームからも退出しない"; +"threads_discourage_information_2" = "\n\nスレッドを有効にしてよろしいですか?"; +"room_no_privileges_to_create_group_call" = "通話を開始するには管理者あるいはモデレーターである必要があります。"; +"contacts_address_book_permission_denied_alert_message" = "連絡先を有効にするには、端末の設定画面を開いてください。"; +"contacts_address_book_permission_denied_alert_title" = "連絡先が無効です"; +"password_policy_weak_pwd_error" = "パスワードが弱すぎます。8文字以上で、大文字、小文字、数字。特殊文字をそれぞれ1つずつ含めてください。"; +"authentication_qr_login_loading_signed_in" = "他の端末でサインインしました。"; +"authentication_qr_login_display_step1" = "他の端末でElementを開いてください"; +"authentication_qr_login_start_display_qr" = "この端末でQRコードを表示"; +"authentication_qr_login_start_need_alternative" = "別の方法が必要ですか?"; +"authentication_qr_login_start_step1" = "他の端末でElementを開いてください"; +"authentication_qr_login_start_subtitle" = "この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください:"; +"authentication_choose_password_not_verified_title" = "電子メールは認証されていません"; +"authentication_server_selection_generic_error" = "このURLでサーバーを発見できません。URLが正しいことを確認してください。"; +"authentication_server_selection_register_title" = "あなたのホームサーバーを選択"; +"authentication_server_selection_login_message" = "ホームサーバーのアドレスを入力してください"; +"authentication_server_selection_login_title" = "ホームサーバーに接続"; +"authentication_login_forgot_password" = "パスワードを忘れた場合"; From 6514525eaa043de6870c951fb67529abebc7cacc Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 30 Jan 2023 19:01:09 +0000 Subject: [PATCH 378/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2372 of 2372 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index d395f5b0d..e520d8584 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2919,3 +2919,4 @@ "poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; "poll_history_loading_text" = "Показ опитувань"; "poll_history_fetching_error" = "Помилка отримання опитувань."; +"key_backup_recover_from_private_key_progress" = "%@%% виконано"; From 664c1bf94a5b73700d9417f6a99e31a59b372f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 30 Jan 2023 20:46:57 +0000 Subject: [PATCH 379/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2372 of 2372 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index dc7b4ce31..d3174cd33 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2666,3 +2666,4 @@ "poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; "poll_history_loading_text" = "Küsitluste kuvamise ootel"; "poll_history_fetching_error" = "Viga küsitluste laadimisel."; +"key_backup_recover_from_private_key_progress" = "%@%% tehtud"; From 957e5a3d12dd2c75c7a45c39a0adefe5dc7ccdfd Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 31 Jan 2023 08:09:25 +0000 Subject: [PATCH 380/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2372 of 2372 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 355012c46..178dd15b3 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2921,3 +2921,4 @@ "poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; "poll_history_loading_text" = "Menampilkan pemungutan suara"; "poll_history_fetching_error" = "Terjadi kesalahan mendapatkan pemungutan suara."; +"key_backup_recover_from_private_key_progress" = "%@%% Selesai"; From 75b61b95dbca7b91582babbf8c615102b7f3c84c Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 31 Jan 2023 10:10:11 +0000 Subject: [PATCH 381/468] Translated using Weblate (German) Currently translated at 100.0% (2373 of 2373 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 03bf34ed0..9c60ecb4a 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2729,3 +2729,4 @@ "poll_history_loading_text" = "Zeige Umfragen an"; "poll_history_fetching_error" = "Fehler beim Laden der Umfragen."; "key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen"; +"voice_broadcast_playback_unable_to_decrypt" = "Entschlüsseln der Sprachübertragung nicht möglich."; From bdfd761db5ad0562850dd7f521edf6466ed8270a Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 31 Jan 2023 10:16:15 +0000 Subject: [PATCH 382/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2373 of 2373 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index e520d8584..5d68314b3 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2920,3 +2920,4 @@ "poll_history_loading_text" = "Показ опитувань"; "poll_history_fetching_error" = "Помилка отримання опитувань."; "key_backup_recover_from_private_key_progress" = "%@%% виконано"; +"voice_broadcast_playback_unable_to_decrypt" = "Неможливо розшифрувати цю голосову трансляцію."; From fb9fff4cd7f1dc916388a388a0cff66827f496a3 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 31 Jan 2023 12:06:53 +0000 Subject: [PATCH 383/468] Translated using Weblate (German) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 9c60ecb4a..285190f85 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2730,3 +2730,4 @@ "poll_history_fetching_error" = "Fehler beim Laden der Umfragen."; "key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen"; "voice_broadcast_playback_unable_to_decrypt" = "Entschlüsseln der Sprachübertragung nicht möglich."; +"home_context_menu_mark_as_unread" = "Als ungelesen markieren"; From 4c6dc3b5525a9e1ef7cdf3572fc9ddd448b9cac0 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 31 Jan 2023 11:42:15 +0000 Subject: [PATCH 384/468] Translated using Weblate (Hungarian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index c5a89d6c2..a10998eb7 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2710,6 +2710,10 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Adatok migrálása\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Titkosítási SDK engedélyezve. A kikapcsolásához az alkalmazást újra kell telepíteni"; -"settings_labs_confirm_crypto_sdk" = "Ezt a műveletet nem lehet visszavonni"; +"settings_labs_disable_crypto_sdk" = "Végpontok közötti titkosítás 2.0 (kikapcsoláshoz kijelentkezés szükséges)"; +"settings_labs_confirm_crypto_sdk" = "Ezzel az opcióval egy gyorsabb és megbízhatóbb végponttól végponting titkosító motor kerül engedélyezésre ami Rustban lett megírva. Bekapcsolás után a kikapcsolásához ki kell jelentkezni. Folytatod?"; "settings_labs_enable_crypto_sdk" = "Az új Rust alapú Titkosítási SDK engedélyezése"; +"home_context_menu_mark_as_unread" = "Olvasatlannak jelöl"; +"poll_history_fetching_error" = "Szavazás betöltési hiba."; +"voice_broadcast_playback_unable_to_decrypt" = "A hang közvetítés nem fejthető vissza."; +"key_backup_recover_from_private_key_progress" = "%@%% kész"; From 6abb0904cfa4559ff1c1eb79891f26ac4eb51a8c Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 31 Jan 2023 12:27:38 +0000 Subject: [PATCH 385/468] Translated using Weblate (Albanian) Currently translated at 99.6% (2365 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index eaa376f3a..a7a3038ae 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2679,3 +2679,31 @@ "voice_broadcast_voip_cannot_start_title" = "S’niset dot një thirrje"; "voice_message_broadcast_in_progress_message" = "S’mund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor"; "voice_message_broadcast_in_progress_title" = "S’niset dot mesazh zanor"; +"wysiwyg_composer_format_action_quote" = "Shfaq/fshih citim"; +"wysiwyg_composer_format_action_code_block" = "Shfaq/fshih bllok kodi"; +"wysiwyg_composer_format_action_ordered_list" = "Shfaq/fshih listë të numërtuar"; +"wysiwyg_composer_format_action_unordered_list" = "Shfaq/fshih listë me toptha"; +"poll_timeline_reply_ended_poll" = "Pyetësor i përfunduar"; +"poll_history_fetching_error" = "Gabim në sjelle pyetësorë."; +"poll_history_load_more" = "Ngarko më tepër pyetësorë"; +"poll_history_no_past_poll_period_text" = "S’ka pyetësorë të kaluar për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë"; +"poll_history_no_active_poll_period_text" = "S’ka pyetësorë aktivë për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë"; +"poll_history_loading_text" = "Shfaqje pyetësorësh"; + +// MARK: - Polls history + +"poll_history_title" = "Historik pyetësorësh"; +"voice_broadcast_playback_unable_to_decrypt" = "S’arrihet të shfshehtëzohet ky transmetim zanor."; +"voice_broadcast_recorder_connection_error" = "Gabim lidhjeje - Incizimi u ndal"; +"voice_broadcast_connection_error_message" = "Mjerisht, s’jemi në gjendje të nisim një incizim mu tani. Ju lutemi, riprovoni më vonë."; +"voice_broadcast_connection_error_title" = "Gabim lidhjeje"; +"home_context_menu_mark_as_unread" = "Vëri shenjë si i palexuar"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%"; +"key_backup_recover_from_private_key_progress" = "Plotësuar %@%%"; +"room_details_polls" = "Historik pyetësorësh"; +"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj 2.0 (që ta çaktivizoni, dilni)"; +"settings_labs_confirm_crypto_sdk" = "Kjo mundësi do të aktivizojë për fshhehtëzim skaj-më-skaj një motor të ri, më të shpejtë dhe më të qëndrueshëm, të shkruar në Rust. Pasi të aktivizohet, do t’ju duhet të bëni daljen nga llogaria, që ta çaktivizoni. Doni të vazhdohet?"; +"settings_labs_enable_crypto_sdk" = "Fshehtëzim skaj-më-skaj 2.0"; From 2eb5f96969c7611456f2f9f765e20369436e2b43 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 31 Jan 2023 13:57:47 +0000 Subject: [PATCH 386/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 5d68314b3..9bce65821 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2921,3 +2921,4 @@ "poll_history_fetching_error" = "Помилка отримання опитувань."; "key_backup_recover_from_private_key_progress" = "%@%% виконано"; "voice_broadcast_playback_unable_to_decrypt" = "Неможливо розшифрувати цю голосову трансляцію."; +"home_context_menu_mark_as_unread" = "Позначити непрочитаним"; From e46349f9219fea02811c2292cfa03e7ee4e36b8d Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 31 Jan 2023 11:40:25 +0000 Subject: [PATCH 387/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 178dd15b3..33d239790 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2922,3 +2922,5 @@ "poll_history_loading_text" = "Menampilkan pemungutan suara"; "poll_history_fetching_error" = "Terjadi kesalahan mendapatkan pemungutan suara."; "key_backup_recover_from_private_key_progress" = "%@%% Selesai"; +"voice_broadcast_playback_unable_to_decrypt" = "Tidak dapat mendekripsi siaran suara ini."; +"home_context_menu_mark_as_unread" = "Tandai sebagai belum dibaca"; From b361761cbc8a3a18f4021648b2854b18f5367509 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 31 Jan 2023 14:26:09 +0000 Subject: [PATCH 388/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index ecb7e105e..4b3a2078e 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2917,3 +2917,6 @@ "poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; "poll_history_loading_text" = "Zobrazenie ankiet"; "poll_history_fetching_error" = "Chyba pri načítavaní ankiet."; +"voice_broadcast_playback_unable_to_decrypt" = "Toto hlasové vysielanie sa nedá dešifrovať."; +"home_context_menu_mark_as_unread" = "Označiť ako neprečítané"; +"key_backup_recover_from_private_key_progress" = "%@%% Dokončené"; From 835f2ea84d48ab6f9e730b5000cdbfe60d93fa92 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 07:38:04 +0000 Subject: [PATCH 389/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index f58f53f22..efbd26c0f 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -720,7 +720,7 @@ "external_link_confirmation_message" = "リンク %@ は別のサイトに移動します:%@\n\n続行してよろしいですか?"; "room_event_action_delete_confirmation_title" = "未送信メッセージを削除"; "room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; -"room_unsent_messages_cancel_title" = "未送信メッセージを削除"; +"room_unsent_messages_cancel_title" = "未送信のメッセージを削除"; "room_message_replying_to" = "%@に返信中"; "room_message_editing" = "編集中"; "room_accessiblity_scroll_to_bottom" = "いちばん下までスクロール"; @@ -769,7 +769,7 @@ "callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "一時停止した通話"; -"store_promotional_text" = "オープンネットワーク上でプライバシーを保護したチャットアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、サードパーティによるアクセスはありません。"; +"store_promotional_text" = "オープンネットワーク上でプライバシーを保護してチャットできるアプリ。非中央集権型のため、あなた自身で制御できます。データ収集、バックドア、第三者によるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; "auth_softlogout_reason" = "ホームサーバー(%1$@)の管理者が%2$@(%3$@)からサインアウトさせました。"; From 019a4f93d210ddf5aa4b943484125d8dd5a4fc8e Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 07:35:24 +0000 Subject: [PATCH 390/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 128 +++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 20 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index efbd26c0f..aa23d1b85 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -193,8 +193,8 @@ "room_two_users_are_typing" = "%@さん、%@さんが入力しています…"; "room_many_users_are_typing" = "%@さん、%@さん他が入力しています…"; "room_message_placeholder" = "返信を送る(未暗号化)…"; -"encrypted_room_message_placeholder" = "暗号文を送信…"; -"room_message_short_placeholder" = "ここに送信文を入力…"; +"encrypted_room_message_placeholder" = "暗号化されたメッセージを送信…"; +"room_message_short_placeholder" = "メッセージを送信…"; "room_offline_notification" = "サーバーとの接続が失われました。"; "room_unsent_messages_notification" = "メッセージを送信できませんでした。"; "room_unsent_messages_unknown_devices_notification" = "未知のセッションが存在するために文章が送信されませんでした。"; @@ -203,13 +203,13 @@ "room_ongoing_conference_call_close" = "閉じる"; "room_conference_call_no_power" = "このルームで会議通話を管理する権限が必要です"; "room_prompt_resend" = "全て再送信"; -"room_prompt_cancel" = "全て中止"; -"room_resend_unsent_messages" = "未送信の文を再送信"; -"room_delete_unsent_messages" = "未送信の文を削除"; +"room_prompt_cancel" = "全てキャンセル"; +"room_resend_unsent_messages" = "未送信のメッセージを再送信"; +"room_delete_unsent_messages" = "未送信のメッセージを削除"; "room_event_action_copy" = "コピー"; "room_event_action_quote" = "引用"; "room_event_action_redact" = "削除"; -"room_event_action_more" = "さらに"; +"room_event_action_more" = "その他"; "room_event_action_share" = "共有"; "room_event_action_permalink" = "メッセージへのリンクをコピー"; "room_event_action_view_source" = "ソースを表示"; @@ -262,12 +262,12 @@ "settings_contacts" = "端末の電話帳"; "settings_advanced" = "拡張設定"; "settings_other" = "その他"; -"settings_labs" = "実験的"; +"settings_labs" = "ラボ"; "settings_devices" = "セッション"; "settings_cryptography" = "暗号化"; "settings_sign_out" = "サインアウト"; "settings_sign_out_confirmation" = "よろしいですか?"; -"settings_sign_out_e2e_warn" = "あなたはエンドツーエンド暗号鍵を失ってしまいます。この端末で暗号化されたルームの昔の発言を読むことができなくなります。"; +"settings_sign_out_e2e_warn" = "エンドツーエンド暗号鍵が消去されます。この端末では、暗号化されたルームの過去の発言を読むことができなくなってしまいます。"; "settings_profile_picture" = "プロフィール画像"; "settings_display_name" = "表示名"; "settings_first_name" = "名"; @@ -464,7 +464,7 @@ "group_section" = "コミュニティー"; "room_message_reply_to_placeholder" = "返信を送る(暗号化されていない)…"; "room_do_not_have_permission_to_post" = "このルームに投稿する権限がありません"; -"encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送信…"; +"encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送る…"; "room_message_reply_to_short_placeholder" = "返信を送る…"; "room_event_action_view_decrypted_source" = "復号化されたソースを見る"; "room_event_action_kick_prompt_reason" = "このユーザーを追放する理由"; @@ -648,8 +648,8 @@ "photo_library_access_not_granted" = "%@はフォトライブラリにアクセスする権限がありません"; "camera_unavailable" = "お使いの端末ではカメラを利用できません"; "event_formatter_widget_removed_by_you" = "ウィジェットを削除しました:%@"; -"event_formatter_jitsi_widget_removed_by_you" = "VoIPカンファレンスを削除しました"; -"event_formatter_jitsi_widget_added_by_you" = "VoIPカンファレンスを追加しました"; +"event_formatter_jitsi_widget_removed_by_you" = "VoIP会議を削除しました"; +"event_formatter_jitsi_widget_added_by_you" = "VoIP会議を追加しました"; // Events formatter with you "event_formatter_widget_added_by_you" = "ウィジェットを追加しました:%@"; @@ -1127,7 +1127,7 @@ "home_empty_view_title" = "%@へようこそ、\n%@"; "threads_empty_tip" = "ヒント:メッセージをタップして「スレッド」を選択し、開始。"; "threads_empty_info_all" = "スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。"; -"threads_empty_title" = "スレッドでディスカッションを整理して管理"; +"threads_empty_title" = "スレッド機能を使って、会話をまとめましょう"; "secure_key_backup_setup_intro_use_security_key_title" = "セキュリティーキーを使用"; // MARK: Secure backup setup @@ -1338,13 +1338,13 @@ "message_reply_to_message_to_reply_to_prefix" = "返信先"; // Room members "room_member_ignore_prompt" = "このユーザーからの全てのメッセージを非表示にしますか?"; -"room_member_power_level_prompt" = "この変更を元に戻すことはできません。ユーザーが自分と同じレベルの権限を持つように促しますが、よろしいですか?"; +"room_member_power_level_prompt" = "このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。\nよろしいですか?"; // Attachment -"attachment_size_prompt" = "次のように送信しますか:"; -"attachment_original" = "実際のサイズ: %@"; -"attachment_small" = "小: %@"; -"attachment_medium" = "中: %@"; -"attachment_large" = "大: %@"; +"attachment_size_prompt" = "次のように送信しますか:"; +"attachment_original" = "実際のサイズ:%@"; +"attachment_small" = "小:%@"; +"attachment_medium" = "中:%@"; +"attachment_large" = "大:%@"; "attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; "attachment_cancel_upload" = "アップロードをキャンセルしますか?"; "attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; @@ -1847,7 +1847,7 @@ "settings_presence_offline_mode" = "オフラインモード"; "settings_enable_room_message_bubbles" = "吹き出しでメッセージを表示"; "settings_discovery_accept_terms" = "IDサーバーの利用規約を承諾"; -"settings_labs_confirm_crypto_sdk" = "この操作は取り消せません"; +"settings_labs_confirm_crypto_sdk" = "このオプションは、新しく、より高速で安定性のあるエンドツーエンド暗号化(Rustで記述)を有効にします。無効にするにはログアウトが必要となります。続行してよろしいですか?"; "settings_labs_enable_voice_broadcast" = "音声配信"; "settings_labs_enable_new_app_layout" = "アプリケーションの新しいレイアウト"; "settings_labs_enable_new_client_info_feature" = "クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定"; @@ -2518,7 +2518,95 @@ "authentication_qr_login_start_subtitle" = "この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください:"; "authentication_choose_password_not_verified_title" = "電子メールは認証されていません"; "authentication_server_selection_generic_error" = "このURLでサーバーを発見できません。URLが正しいことを確認してください。"; -"authentication_server_selection_register_title" = "あなたのホームサーバーを選択"; +"authentication_server_selection_register_title" = "あなたのホームサーバーを選択してください"; "authentication_server_selection_login_message" = "ホームサーバーのアドレスを入力してください"; "authentication_server_selection_login_title" = "ホームサーバーに接続"; "authentication_login_forgot_password" = "パスワードを忘れた場合"; +"event_formatter_call_answer" = "出る"; +"event_formatter_call_incoming_video" = "着信中のビデオ通話"; +"event_formatter_call_incoming_voice" = "着信中の音声通話"; +"event_formatter_call_has_ended_with_time" = "通話を終了しました・%@"; +"room_access_space_chooser_other_spaces_section" = "その他のスペースまたはルーム"; +"room_access_settings_screen_setting_room_access" = "ルームのアクセスの設定"; +"settings_labs_enable_wysiwyg_composer" = "リッチテキストエディターを試してみる"; +"settings_labs_enable_ringing_for_group_calls" = "グループ通話で呼び出す"; +"settings_notifications_disabled_alert_message" = "通知を有効にするには、端末の設定画面を開いてください。"; +"room_accessibility_record_voice_message_hint" = "2回続けてタップし長押しすると録音。"; +"room_preview_decline_invitation_options" = "招待を拒否するか、このユーザーを無視しますか?"; +"threads_beta_information" = "スレッド機能を使って、会話をまとめましょう。\n\nスレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 "; +"threads_notice_information" = "実験期間中に作成されたスレッドは通常の返信として表示されます

スレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。"; +"threads_empty_info_my" = "既存のスレッドに返信するか、メッセージをタップし「スレッド」から新しいスレッドを開始。"; +"room_accessibility_thread_more" = "その他"; +"room_first_message_placeholder" = "最初のメッセージを送信…"; + +// MARK: - Chat + +"room_slide_to_end_group_call" = "スライドすると全員の通話を終了"; +"authentication_qr_login_failure_request_timed_out" = "時間内にリンクが完了しませんでした。"; +"authentication_qr_login_failure_title" = "リンクに失敗しました"; +"authentication_qr_login_start_step2" = "設定から「セキュリティーとプライバシー」を開いてください"; +"authentication_qr_login_confirm_alert" = "このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに制限なくアクセスできるようになります。"; +"authentication_qr_login_scan_subtitle" = "QRコードを以下の四角に合わせてください"; +"authentication_qr_login_display_step2" = "「QRコードでサインイン」を選択してください"; +"authentication_qr_login_start_step4" = "「この端末でQRコードを表示」を選択してください"; +"authentication_qr_login_start_step3" = "「端末をリンク」を選択してください"; +"authentication_qr_login_display_title" = "端末をリンク"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "他の人は %@ であなたを見つけることができます"; +"authentication_server_selection_register_message" = "あなたのサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます"; +"authentication_server_info_title_login" = "会話が実施される場所"; +"authentication_server_info_title" = "会話が実施される場所"; +"onboarding_avatar_message" = "表示名にプロフィール画像を追加しましょう"; +"all_chats_edit_layout_add_filters_message" = "自動的にメッセージをあなたが選択したカテゴリーにフィルタリング"; +"all_chats_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。"; +"home_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。以下の+ボタンを押すと、連絡先とルームを追加できます。"; +"all_chats_empty_view_title" = "%@\nは空です。"; +"all_chats_nothing_found_placeholder_message" = "検索を調整してみてください。"; +"all_chats_nothing_found_placeholder_title" = "何も見つかりませんでした。"; +"all_chats_edit_layout_pin_spaces_title" = "スペースをピン止め"; +"all_chats_edit_layout_add_section_message" = "セクションをホームにピン止めすると簡単にアクセスできます"; +"all_chats_edit_layout_add_section_title" = "セクションをホームに追加"; +"version_check_banner_subtitle_deprecated" = "%@のサポートはiOS %@で終了しました。%@の使用を継続する場合は、iOSのバージョンをアップグレードしてください。"; +"version_check_banner_subtitle_supported" = "%@のサポートはiOS %@で近日中に終了します。%@の使用を継続する場合は、iOSのバージョンをアップグレードしてください。"; +"version_check_modal_action_title_deprecated" = "方法を確認"; +"version_check_modal_title_supported" = "iOS %@のサポートは近日中に終了します"; + +// MARK: - Version check + +"version_check_banner_title_supported" = "iOS %@のサポートは近日中に終了します"; +"version_check_banner_title_deprecated" = "iOS %@のサポートは終了しました"; +"version_check_modal_title_deprecated" = "iOS %@のサポートは終了しました"; +"attachment_size_prompt_title" = "送信するサイズを確認"; +"attachment_small_with_resolution" = "小:%@(~%@)"; +"attachment_medium_with_resolution" = "中:%@(~%@)"; +"attachment_large_with_resolution" = "大:%@(~%@)"; +"e2e_passphrase_too_short" = "パスフレーズが短すぎます(%d文字以上にしてください)"; +"notice_room_third_party_revoked_invite_for_dm" = "%@が%@の招待を取り消しました"; +"notice_room_third_party_revoked_invite_by_you_for_dm" = "%@の招待を取り消しました"; +"notice_room_name_changed_by_you_for_dm" = "名前を%@に変更しました。"; +"call_remote_holded" = "%@が通話を保留しました"; +"call_holded" = "通話を保留しました"; +"call_more_actions_unhold" = "再開"; +"user_session_rename_session_description" = "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。\n\n相手はあなたとやり取りしていることを確かめることができますが、あなたがここに入力するセッション名は相手に対して表示されます。"; +"user_session_unverified_session_description" = "未認証のセッションは、アカウント情報でログインされていますが、クロス認証されていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。"; +"user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。"; +"user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。"; +"launch_loading_server_syncing" = "サーバーと同期しています"; +"launch_loading_processing_response" = "データを処理しています\n%@ %%"; +"wysiwyg_composer_format_action_link" = "リンクの装飾を適用"; +"wysiwyg_composer_format_action_inline_code" = "インラインコードの装飾を適用"; +"wysiwyg_composer_format_action_unordered_list" = "箇条書きリストの表示を切り替える"; +"wysiwyg_composer_format_action_ordered_list" = "番号付きリストの表示を切り替える"; +"wysiwyg_composer_format_action_code_block" = "コードブロックの表示を切り替える"; +"wysiwyg_composer_format_action_quote" = "引用の表示を切り替える"; +"poll_timeline_reply_ended_poll" = "終了したアンケート"; +"settings_labs_enable_crypto_sdk" = "エンドツーエンド暗号化2.0"; +"settings_labs_disable_crypto_sdk" = "エンドツーエンド暗号化2.0(無効にするにはログアウトしてください)"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "データを移行しています\n%@ %%"; +"poll_history_load_more" = "他のアンケートを読み込む"; +"key_backup_recover_from_private_key_progress" = "%@%%完了"; +"voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; +"home_context_menu_mark_as_unread" = "未読にする"; From 2166f033da40d2aa3779dd309a88920d6632d34d Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 07:41:21 +0000 Subject: [PATCH 391/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index aa23d1b85..5f9647ac7 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -765,7 +765,7 @@ "auth_softlogout_clear_data_button" = "全てのデータをクリア"; "auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; "auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; -"callbar_return" = "かけ直す"; +"callbar_return" = "折り返し"; "callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "一時停止した通話"; From aa4d58dc2e48bfeb3f24790f95a26c43f3774e92 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 07:41:09 +0000 Subject: [PATCH 392/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 5f9647ac7..dcb9609d7 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -766,10 +766,10 @@ "auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; "auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; "callbar_return" = "折り返し"; -"callbar_active_and_multiple_paused" = "アクティブな通話(%@)· %@の一時停止された通話"; +"callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "一時停止した通話"; -"store_promotional_text" = "オープンネットワーク上でプライバシーを保護してチャットできるアプリ。非中央集権型のため、あなた自身で制御できます。データ収集、バックドア、第三者によるアクセスはありません。"; +"store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者によるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; "auth_softlogout_reason" = "ホームサーバー(%1$@)の管理者が%2$@(%3$@)からサインアウトさせました。"; From 0038fc7f681aec327a3ec17ad64b03f03e094861 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 07:50:57 +0000 Subject: [PATCH 393/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index dcb9609d7..d95aaa968 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -772,7 +772,7 @@ "store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者によるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; -"auth_softlogout_reason" = "ホームサーバー(%1$@)の管理者が%2$@(%3$@)からサインアウトさせました。"; +"auth_softlogout_reason" = "あなたのホームサーバー (%1$@) 管理者があなたをアカウント %2$@ (%3$@) からサインアウトさせました。"; "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; "auth_autodiscover_invalid_response" = "無効なホームサーバー発見レスポンス"; From 877e2d8b0283f35b240877676cdd2f2cbb8e6a9c Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 07:50:04 +0000 Subject: [PATCH 394/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index d95aaa968..a62ea1929 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -765,7 +765,7 @@ "auth_softlogout_clear_data_button" = "全てのデータをクリア"; "auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; "auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; -"callbar_return" = "折り返し"; +"callbar_return" = "折り返す"; "callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "一時停止した通話"; @@ -775,12 +775,12 @@ "auth_softlogout_reason" = "あなたのホームサーバー (%1$@) 管理者があなたをアカウント %2$@ (%3$@) からサインアウトさせました。"; "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; -"auth_autodiscover_invalid_response" = "無効なホームサーバー発見レスポンス"; -"auth_accept_policies" = "このホームサーバーのポリシーを確認して同意してください:"; -"auth_reset_password_error_is_required" = "IDサーバーが設定されていません:パスワードをリセットするためにサーバーオプションに追加してください。"; -"auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードをリセットするためにIDサーバーを追加してください。"; -"auth_phone_is_required" = "IDサーバーが設定されていないので、パスワードをリセットするために電話番号を追加することはできません。"; -"auth_email_is_required" = "IDサーバーが設定されていないので、パスワードをリセットするためにメールアドレスを追加することはできません。"; +"auth_autodiscover_invalid_response" = "ホームサーバーのディスカバリー(発見)に関する不正な応答です"; +"auth_accept_policies" = "このホームサーバーの運営方針を確認して承諾してください:"; +"auth_reset_password_error_is_required" = "IDサーバーが設定されていません:Matrixのアカウントのパスワードを再設定するためにサーバーオプションに追加してください。"; +"auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードを再設定するためにIDサーバーを追加してください。"; +"auth_phone_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用する電話番号を追加することができません。"; +"auth_email_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用するメールアドレスを追加することができません。"; "auth_add_email_phone_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションでメールアドレスや電話番号を使用して知人に見つけてもらえるようにできます。"; "auth_add_phone_message_2" = "電話番号を設定します。後からオプションで知人に見つけてもらえるようにできます。"; "auth_add_email_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。"; From 703e5e1e914f368eee6dbca02320e6ff0c76fbe1 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:05:57 +0000 Subject: [PATCH 395/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index a62ea1929..d529c9df8 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -753,7 +753,7 @@ "error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようですね。サインアウトしますか?"; "social_login_button_title_sign_up" = "%@でサインアップ"; "social_login_button_title_sign_in" = "%@でサインイン"; -"social_login_button_title_continue" = "続きはこちら%@"; +"social_login_button_title_continue" = "%@ で続ける"; "social_login_list_title_sign_up" = "もしくは"; "social_login_list_title_sign_in" = "もしくは"; From 7f5648add4e3e991bff9bce938e48da3ef0f6d3e Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:02:41 +0000 Subject: [PATCH 396/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index d529c9df8..d15b907e2 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -759,12 +759,12 @@ // Social login -"social_login_list_title_continue" = "続きはこちら"; -"auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?再びサインインするとアカウントデータやメッセージにアクセスできます。"; +"social_login_list_title_continue" = "次で続行"; +"auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?アカウントのデータやメッセージにアクセスするには、再びサインインしてください。"; "auth_softlogout_clear_data_sign_out_title" = "よろしいですか?"; -"auth_softlogout_clear_data_button" = "全てのデータをクリア"; -"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; -"auth_softlogout_clear_data_message_1" = "警告:個人データ(暗号鍵を含む)がこの端末にまだ保存されています。"; +"auth_softlogout_clear_data_button" = "全てのデータを消去"; +"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する、または別のアカウントにサインインする場合は、消去してください。"; +"auth_softlogout_clear_data_message_1" = "警告:あなたの個人データ(暗号鍵を含む)が、この端末にまだ保存されています。"; "callbar_return" = "折り返す"; "callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; @@ -772,7 +772,7 @@ "store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者によるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; -"auth_softlogout_reason" = "あなたのホームサーバー (%1$@) 管理者があなたをアカウント %2$@ (%3$@) からサインアウトさせました。"; +"auth_softlogout_reason" = "あなたのホームサーバー(%1$@)の管理者が、あなたをアカウント %2$@ (%3$@)からサインアウトさせました。"; "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; "auth_autodiscover_invalid_response" = "ホームサーバーのディスカバリー(発見)に関する不正な応答です"; From ec04b56b0256d43e43ad86ee56c0da810ba81e63 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:13:39 +0000 Subject: [PATCH 397/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index d15b907e2..dfd56f853 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -737,7 +737,7 @@ "room_participants_action_security_status_verify" = "認証"; "room_participants_action_security_status_verified" = "認証済"; "room_participants_action_section_security" = "セキュリティー"; -"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使って連絡先とチャットを開始することができません。"; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使用して連絡先とチャットを開始することはできません。"; "room_participants_filter_room_members_for_dm" = "メンバーを検索"; "room_participants_remove_third_party_invite_prompt_msg" = "招待を取り消してよろしいですか?"; "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; From f9cd0ca0902fea47a70653528e0430536d2d84fa Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:11:37 +0000 Subject: [PATCH 398/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index dfd56f853..7e375bb7a 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -743,17 +743,17 @@ "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; "room_participants_leave_prompt_title_for_dm" = "退出"; "contacts_address_book_no_identity_server" = "IDサーバーが設定されていません"; -"rooms_empty_view_information" = "ルームは非公開でも公開でも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ったりすることができます。"; +"rooms_empty_view_information" = "ルームは非公開でも公開でも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ったりすることができます。"; "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると連絡先を追加できます。"; "people_empty_view_title" = "連絡先"; "room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールで参加者を追加することができません。"; // Errors -"error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようですね。サインアウトしますか?"; +"error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようです。サインアウトしますか?"; "social_login_button_title_sign_up" = "%@でサインアップ"; "social_login_button_title_sign_in" = "%@でサインイン"; -"social_login_button_title_continue" = "%@ で続ける"; +"social_login_button_title_continue" = "%@で続行"; "social_login_list_title_sign_up" = "もしくは"; "social_login_list_title_sign_in" = "もしくは"; @@ -2559,7 +2559,7 @@ "onboarding_avatar_message" = "表示名にプロフィール画像を追加しましょう"; "all_chats_edit_layout_add_filters_message" = "自動的にメッセージをあなたが選択したカテゴリーにフィルタリング"; "all_chats_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。"; -"home_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。以下の+ボタンを押すと、連絡先とルームを追加できます。"; +"home_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。以下の+ボタンを押すと、連絡先とルームを追加できます。"; "all_chats_empty_view_title" = "%@\nは空です。"; "all_chats_nothing_found_placeholder_message" = "検索を調整してみてください。"; "all_chats_nothing_found_placeholder_title" = "何も見つかりませんでした。"; From 230f100c602e3aec75dcdf4022f4601bad2a2616 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:17:00 +0000 Subject: [PATCH 399/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 7e375bb7a..0b06409ad 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -242,7 +242,7 @@ // Room Preview "room_preview_invitation_format" = "あなたは%@さんに呼ばれてこのルームへ参加しました"; "room_preview_subtitle" = "現在表示しているのはルームのプレビューです。メッセージの送信などは行えません。"; -"room_preview_unlinked_email_warning" = "このアカウントに関連付けられていない%@宛に招待が送信されました。別のアカウントでログインするか、メールアドレスをこのアカウントに追加することができます。"; +"room_preview_unlinked_email_warning" = "この招待は、このアカウントに関連付けられていない %@ に送信されました。別のアカウントでログインするか、このメールアドレスを自分のアカウントに追加してください。"; "room_preview_try_join_an_unknown_room" = "%@ に参加しますか?"; "room_preview_try_join_an_unknown_room_default" = "ルーム"; // Settings @@ -718,7 +718,7 @@ "settings_discovery_settings" = "ディスカバリー"; "room_multiple_typing_notification" = "%@とその他のユーザーが入力中です"; "external_link_confirmation_message" = "リンク %@ は別のサイトに移動します:%@\n\n続行してよろしいですか?"; -"room_event_action_delete_confirmation_title" = "未送信メッセージを削除"; +"room_event_action_delete_confirmation_title" = "未送信のメッセージを削除"; "room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; "room_unsent_messages_cancel_title" = "未送信のメッセージを削除"; "room_message_replying_to" = "%@に返信中"; From 184377e87a8d3a404b145a79d78e219e7804f2d1 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:21:43 +0000 Subject: [PATCH 400/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 0b06409ad..839575c44 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -242,7 +242,7 @@ // Room Preview "room_preview_invitation_format" = "あなたは%@さんに呼ばれてこのルームへ参加しました"; "room_preview_subtitle" = "現在表示しているのはルームのプレビューです。メッセージの送信などは行えません。"; -"room_preview_unlinked_email_warning" = "この招待は、このアカウントに関連付けられていない %@ に送信されました。別のアカウントでログインするか、このメールアドレスを自分のアカウントに追加してください。"; +"room_preview_unlinked_email_warning" = "この招待は、このアカウントに関連付けられていない%@に送信されました。別のアカウントでログインするか、このメールアドレスを自分のアカウントに追加してください。"; "room_preview_try_join_an_unknown_room" = "%@ に参加しますか?"; "room_preview_try_join_an_unknown_room_default" = "ルーム"; // Settings @@ -319,7 +319,7 @@ "settings_crypto_device_id" = "\nセッションID: "; "settings_crypto_device_key" = "\nセッションキー:\n"; "settings_crypto_export" = "鍵をエクスポート"; -"settings_crypto_blacklist_unverified_devices" = "認証されたセッションのみで暗号化"; +"settings_crypto_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; // Room Details "room_details_title" = "ルームの詳細"; "room_details_people" = "メンバー"; @@ -359,7 +359,7 @@ "room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告: 有効後にこれを無効にすることはできません!)"; "room_details_advanced_e2e_encryption_enabled" = "このルームの発言は暗号化されています"; "room_details_advanced_e2e_encryption_disabled" = "このルームの発言は暗号化されていません。"; -"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証されたセッションのみで暗号化"; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; "room_details_fail_to_update_avatar" = "ルームのアイコン画像の更新に失敗"; "room_details_fail_to_update_room_name" = "ルーム名の更新に失敗"; "room_details_fail_to_update_topic" = "ルームの説明の更新に失敗"; @@ -622,7 +622,7 @@ "widget_picker_manage_integrations" = "インテグレーションを管理する…"; // Widget Picker -"widget_picker_title" = "インテグレーションマネージャー"; +"widget_picker_title" = "インテグレーション"; "widget_integration_manager_disabled" = "設定でインテグレーションマネージャーを有効にする必要があります"; "widget_menu_remove" = "全員から削除"; "widget_menu_revoke_permission" = "アクセスを取り消す"; @@ -668,7 +668,7 @@ // Media picker "media_picker_title" = "メディアライブラリ"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "ここは暗号化が有効ではありません。"; -"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここは暗号化が有効です"; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効です"; "room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; @@ -1002,7 +1002,7 @@ "room_event_action_delete_confirmation_message" = "この未送信メッセージを削除してよろしいですか?"; "room_accessibility_video_call" = "ビデオ通話"; "room_accessibility_call" = "通話"; -"room_accessibility_integrations" = "統合"; +"room_accessibility_integrations" = "インテグレーション"; "room_accessibility_search" = "検索"; "room_accessibility_upload" = "アップロード"; "room_message_edits_history_title" = "メッセージを編集"; From 0be85492ffbf2c462aaeaeb77f0a6aff63da018f Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:21:23 +0000 Subject: [PATCH 401/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 839575c44..0ff6df199 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -675,7 +675,7 @@ "room_details_access_section_anyone_apart_from_guest_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_for_dm" = "これにアクセスできる人は?"; -"room_details_photo_for_dm" = "写真"; +"room_details_photo_for_dm" = "画像"; "room_details_integrations" = "インテグレーション"; "room_details_search" = "ルーム内検索"; "room_details_title_for_dm" = "詳細"; From b6cc14691b35d7e5b7587fd96c564dfc81930ea6 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:23:09 +0000 Subject: [PATCH 402/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 0ff6df199..30cad93ce 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -522,7 +522,7 @@ "widget_sticker_picker_no_stickerpacks_alert_add_now" = "今すぐ追加しますか?"; // Room key request dialog "e2e_room_key_request_title" = "暗号鍵の要求"; -"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しい端末 '%@' を追加しました。"; +"e2e_room_key_request_message_new_device" = "暗号化キーを要求している新しいセッション'%@'を追加しました。"; "e2e_room_key_request_message" = "認証されていない端末 '%@' が暗号鍵を要求しています。"; "e2e_room_key_request_start_verification" = "認証を始めます…"; "e2e_room_key_request_share_without_verifying" = "認証せずに共有"; @@ -668,7 +668,7 @@ // Media picker "media_picker_title" = "メディアライブラリ"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "ここは暗号化が有効ではありません。"; -"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効です"; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効になっています"; "room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; From 12ef800beec2e50cb9261839a50ef95a628a7ea0 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:22:20 +0000 Subject: [PATCH 403/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2187 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 30cad93ce..8e3f37bea 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -667,7 +667,7 @@ // Media picker "media_picker_title" = "メディアライブラリ"; -"room_details_advanced_e2e_encryption_disabled_for_dm" = "ここは暗号化が有効ではありません。"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "ここでは暗号化が有効ではありません。"; "room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効になっています"; "room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; From c1b002afff7c671d430a2d2092176219620bc90b Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:24:24 +0000 Subject: [PATCH 404/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2188 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 8e3f37bea..07e9be2b9 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2610,3 +2610,4 @@ "key_backup_recover_from_private_key_progress" = "%@%%完了"; "voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; "home_context_menu_mark_as_unread" = "未読にする"; +"key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; From e24588c4aa6a0563ca7cf0c186b2412732cd4c92 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:23:56 +0000 Subject: [PATCH 405/468] Translated using Weblate (Japanese) Currently translated at 92.1% (2188 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 07e9be2b9..42ffa10e0 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -522,8 +522,8 @@ "widget_sticker_picker_no_stickerpacks_alert_add_now" = "今すぐ追加しますか?"; // Room key request dialog "e2e_room_key_request_title" = "暗号鍵の要求"; -"e2e_room_key_request_message_new_device" = "暗号化キーを要求している新しいセッション'%@'を追加しました。"; -"e2e_room_key_request_message" = "認証されていない端末 '%@' が暗号鍵を要求しています。"; +"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; +"e2e_room_key_request_message" = "未認証のセッション '%@' が暗号鍵を要求しています。"; "e2e_room_key_request_start_verification" = "認証を始めます…"; "e2e_room_key_request_share_without_verifying" = "認証せずに共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; From d64df8ffb1b38941c1f4f73b706a7d510502dade Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:24:39 +0000 Subject: [PATCH 406/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2189 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 42ffa10e0..bd495986e 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2611,3 +2611,4 @@ "voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; "home_context_menu_mark_as_unread" = "未読にする"; "key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; +"key_backup_setup_passphrase_passphrase_invalid" = "ワードを追加してみる"; From da581b33e1d5f5d3eff5ced4df809e55ebf5fba9 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:24:31 +0000 Subject: [PATCH 407/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2189 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index bd495986e..bc4e18a63 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2610,5 +2610,5 @@ "key_backup_recover_from_private_key_progress" = "%@%%完了"; "voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; "home_context_menu_mark_as_unread" = "未読にする"; -"key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; +"key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; "key_backup_setup_passphrase_passphrase_invalid" = "ワードを追加してみる"; From 83e0070eceb075a07618644677552cd6a536a13e Mon Sep 17 00:00:00 2001 From: Kaede Date: Wed, 1 Feb 2023 08:26:29 +0000 Subject: [PATCH 408/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index bc4e18a63..d0cd79205 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2612,3 +2612,4 @@ "home_context_menu_mark_as_unread" = "未読にする"; "key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; "key_backup_setup_passphrase_passphrase_invalid" = "ワードを追加してみる"; +"biometrics_cant_unlocked_alert_title" = "アプリのロック解除に失敗しました"; From 10326450d6fbeb2ee9a812bb7b1693b132212f4b Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:25:01 +0000 Subject: [PATCH 409/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index d0cd79205..13a1073f9 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2611,5 +2611,6 @@ "voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。"; "home_context_menu_mark_as_unread" = "未読にする"; "key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; -"key_backup_setup_passphrase_passphrase_invalid" = "ワードを追加してみる"; +"key_backup_setup_passphrase_passphrase_invalid" = "語を追加してみる"; "biometrics_cant_unlocked_alert_title" = "アプリのロック解除に失敗しました"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; From ac0ed82f5a7f869d5b6471d87b6278466dcd80ed Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:26:59 +0000 Subject: [PATCH 410/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 13a1073f9..fbbb82465 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1679,7 +1679,7 @@ // MARK: - Invite friends -"invite_friends_action" = "友だちを %@ に招待"; +"invite_friends_action" = "友達を %@ に招待する"; "call_transfer_error_title" = "エラー"; "home_context_menu_mark_as_read" = "既読にする"; "home_context_menu_normal_priority" = "通常優先度"; From 5bc45f9e32da78564879ae3f7d2f5baf5c478c61 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:26:39 +0000 Subject: [PATCH 411/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index fbbb82465..c76a3a311 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2612,5 +2612,5 @@ "home_context_menu_mark_as_unread" = "未読にする"; "key_backup_setup_passphrase_passphrase_valid" = "いいですね!"; "key_backup_setup_passphrase_passphrase_invalid" = "語を追加してみる"; -"biometrics_cant_unlocked_alert_title" = "アプリのロック解除に失敗しました"; +"biometrics_cant_unlocked_alert_title" = "アプリのロックを解除できません"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; From f07a5de2c59fcddaded909182a270c732f7bcc2d Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:27:25 +0000 Subject: [PATCH 412/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index c76a3a311..4c012ff4e 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1586,7 +1586,7 @@ "biometrics_cant_unlocked_alert_message_retry" = "再試行"; "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; -"invite_friends_share_text" = "%@ での連絡先: %@"; +"invite_friends_share_text" = "%@で連絡してください:%@"; "side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; @@ -1679,7 +1679,7 @@ // MARK: - Invite friends -"invite_friends_action" = "友達を %@ に招待する"; +"invite_friends_action" = "友達を%@に招待"; "call_transfer_error_title" = "エラー"; "home_context_menu_mark_as_read" = "既読にする"; "home_context_menu_normal_priority" = "通常優先度"; From 7a266ffe4fe8eb695b919335c3ba4cf4051b3a52 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:27:41 +0000 Subject: [PATCH 413/468] Translated using Weblate (Japanese) Currently translated at 92.2% (2191 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 4c012ff4e..6fbea6a19 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1586,7 +1586,7 @@ "biometrics_cant_unlocked_alert_message_retry" = "再試行"; "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; -"invite_friends_share_text" = "%@で連絡してください:%@"; +"invite_friends_share_text" = "こちらでお話ししましょう %@: %@"; "side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; From 710af0c2efa764f06d374b98c7289cfeb7baef01 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:28:00 +0000 Subject: [PATCH 414/468] Translated using Weblate (Japanese) Currently translated at 92.3% (2192 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 6fbea6a19..6dbefc6db 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2614,3 +2614,4 @@ "key_backup_setup_passphrase_passphrase_invalid" = "語を追加してみる"; "biometrics_cant_unlocked_alert_title" = "アプリのロックを解除できません"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; +"room_avatar_view_accessibility_hint" = "部屋のアバターを変更する"; From 10420849ca885dc56fef744e2977f3e3f87d4471 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:27:54 +0000 Subject: [PATCH 415/468] Translated using Weblate (Japanese) Currently translated at 92.3% (2192 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 6dbefc6db..130d43ba3 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1586,7 +1586,7 @@ "biometrics_cant_unlocked_alert_message_retry" = "再試行"; "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; -"invite_friends_share_text" = "こちらでお話ししましょう %@: %@"; +"invite_friends_share_text" = "%@でお話ししましょう:%@"; "side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; From 228a189d0c435f266822b68f3a18a3dfdc40cc81 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:34:41 +0000 Subject: [PATCH 416/468] Translated using Weblate (Japanese) Currently translated at 92.3% (2193 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 130d43ba3..9689fe451 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2615,3 +2615,4 @@ "biometrics_cant_unlocked_alert_title" = "アプリのロックを解除できません"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; "room_avatar_view_accessibility_hint" = "部屋のアバターを変更する"; +"room_intro_cell_information_dm_sentence2" = "この会話はお二人だけで、他の人は参加できません。"; From 332f4252e3638df1a8522bab97f112889ff0b1c7 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:28:08 +0000 Subject: [PATCH 417/468] Translated using Weblate (Japanese) Currently translated at 92.3% (2193 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 9689fe451..dd013e1ac 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2614,5 +2614,5 @@ "key_backup_setup_passphrase_passphrase_invalid" = "語を追加してみる"; "biometrics_cant_unlocked_alert_title" = "アプリのロックを解除できません"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; -"room_avatar_view_accessibility_hint" = "部屋のアバターを変更する"; +"room_avatar_view_accessibility_hint" = "ルームのアバターを変更"; "room_intro_cell_information_dm_sentence2" = "この会話はお二人だけで、他の人は参加できません。"; From a12f0a5f35b50743ce3e5baa0365b7c80fbe9bb8 Mon Sep 17 00:00:00 2001 From: oksya8and8 Date: Wed, 1 Feb 2023 08:36:18 +0000 Subject: [PATCH 418/468] Translated using Weblate (Japanese) Currently translated at 92.4% (2195 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index dd013e1ac..bd55ee0ad 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2616,3 +2616,5 @@ "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; "room_avatar_view_accessibility_hint" = "ルームのアバターを変更"; "room_intro_cell_information_dm_sentence2" = "この会話はお二人だけで、他の人は参加できません。"; +"favourites_empty_view_information" = "お気に入り登録にはいくつかの方法がありますが、一番手っ取り早いのは、長押しすることです。星マークをタップすると、自動的にここに表示され、大切に保管されます。"; +"room_intro_cell_information_multiple_dm_sentence2" = "誰かを招待しない限り、この会話に参加しているのはあなただけです。"; From 6bcb9ac381a304dfa79551c8def6034322fb4431 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 08:39:18 +0000 Subject: [PATCH 419/468] Translated using Weblate (Japanese) Currently translated at 92.6% (2199 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index bd55ee0ad..adbd9c78b 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -2616,5 +2616,11 @@ "key_backup_setup_passphrase_confirm_passphrase_valid" = "いいですね!"; "room_avatar_view_accessibility_hint" = "ルームのアバターを変更"; "room_intro_cell_information_dm_sentence2" = "この会話はお二人だけで、他の人は参加できません。"; -"favourites_empty_view_information" = "お気に入り登録にはいくつかの方法がありますが、一番手っ取り早いのは、長押しすることです。星マークをタップすると、自動的にここに表示され、大切に保管されます。"; +"favourites_empty_view_information" = "お気に入り登録にはいくつかの方法がありますが、一番手っ取り早いのは、長押しすることです。星マークをタップすると、自動的にここに表示され、保管されます。"; "room_intro_cell_information_multiple_dm_sentence2" = "誰かを招待しない限り、この会話に参加しているのはあなただけです。"; +"analytics_prompt_message_new_user" = "%@の改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。"; + +// Analytics +"analytics_prompt_title" = "%@の改善を手伝う"; +"event_formatter_call_active_video" = "実行中のビデオ通話"; +"event_formatter_call_active_voice" = "実行中の音声通話"; From 8d00b479c598dc6e9ce17aa72b7aad3a0547c9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 31 Jan 2023 19:47:22 +0000 Subject: [PATCH 420/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2374 of 2374 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index d3174cd33..7eb797ad1 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2667,3 +2667,5 @@ "poll_history_loading_text" = "Küsitluste kuvamise ootel"; "poll_history_fetching_error" = "Viga küsitluste laadimisel."; "key_backup_recover_from_private_key_progress" = "%@%% tehtud"; +"voice_broadcast_playback_unable_to_decrypt" = "Selle ringhäälingukõne dekrüptimine ei õnnestu."; +"home_context_menu_mark_as_unread" = "Märgi mitteloetuks"; From 9c944b4a21e7fae4e7387954e5b17cc08a3f47ed Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 1 Feb 2023 10:46:04 +0000 Subject: [PATCH 421/468] Translated using Weblate (German) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 285190f85..00f370eed 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2731,3 +2731,5 @@ "key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen"; "voice_broadcast_playback_unable_to_decrypt" = "Entschlüsseln der Sprachübertragung nicht möglich."; "home_context_menu_mark_as_unread" = "Als ungelesen markieren"; +"wysiwyg_composer_format_action_un_indent" = "Einrückung verringern"; +"wysiwyg_composer_format_action_indent" = "Einrückung erhöhen"; From ba20603d8d60d7cd70b6a0ed3fd26d0a343d9ff5 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 1 Feb 2023 11:20:49 +0000 Subject: [PATCH 422/468] Translated using Weblate (Japanese) Currently translated at 96.5% (2294 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 171 +++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 30 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index adbd9c78b..0bc575f22 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -338,7 +338,7 @@ "room_details_access_section_anyone" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_no_address_warning" = "このルームへのリンクを作成するには、ルームのアドレスが必要です"; "room_details_access_section_directory_toggle" = "ルーム一覧へ公開"; -"room_details_history_section" = "発言履歴を閲覧できる人"; +"room_details_history_section" = "履歴を閲覧できる人"; "room_details_history_section_anyone" = "誰でも"; "room_details_history_section_members_only" = "メンバーのみ (この設定を選択した時点から)"; "room_details_history_section_members_only_since_invited" = "メンバーのみ(招待を送った時点から)"; @@ -524,7 +524,7 @@ "e2e_room_key_request_title" = "暗号鍵の要求"; "e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; "e2e_room_key_request_message" = "未認証のセッション '%@' が暗号鍵を要求しています。"; -"e2e_room_key_request_start_verification" = "認証を始めます…"; +"e2e_room_key_request_start_verification" = "認証を開始…"; "e2e_room_key_request_share_without_verifying" = "認証せずに共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR @@ -563,18 +563,18 @@ // MARK: - Room Info -"room_info_list_one_member" = "1名のメンバー"; +"room_info_list_one_member" = "1人のメンバー"; "create_room_placeholder_address" = "#testroom:matrix.org"; "create_room_section_header_address" = "アドレス"; "create_room_show_in_directory" = "ルーム一覧に掲載"; "create_room_section_footer_type" = "非公開のルームは、ルームに招待された人のみ参加できます。"; -"create_room_type_public" = "公開ルーム (誰でも参加可能)"; +"create_room_type_public" = "公開ルーム(誰でも参加可能)"; "create_room_type_private" = "非公開ルーム (招待者のみ参加可能)"; "create_room_section_header_type" = "アクセスできる人"; "create_room_section_footer_encryption" = "暗号化はあとから無効にすることはできません。"; -"create_room_section_header_encryption" = "ルームの暗号化"; -"create_room_placeholder_topic" = "トピック"; -"create_room_section_header_topic" = "ルームのトピック(任意)"; +"create_room_section_header_encryption" = "暗号化"; +"create_room_placeholder_topic" = "ルームのトピックを入力してください"; +"create_room_section_header_topic" = "トピック(任意)"; "create_room_placeholder_name" = "名前"; "create_room_section_header_name" = "ルーム名"; @@ -864,9 +864,9 @@ "settings_key_backup_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。"; "settings_labs_message_reaction" = "絵文字でメッセージに反応"; "settings_security" = "セキュリティー"; -"settings_three_pids_management_information_part3" = ""; +"settings_three_pids_management_information_part3" = "。"; "settings_three_pids_management_information_part2" = "ディスカバリー"; -"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrixーーオープンな分散型通信の標準規格ーーで動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; +"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrix――オープンな分散型通信の標準規格――で動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; "user_verification_session_details_information_untrusted_current_user" = "このセッションを認証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; @@ -1039,9 +1039,9 @@ // MARK: - Biometrics Protection "biometrics_mode_touch_id" = "Touch ID"; -"pin_protection_settings_enable_pin" = "PINを有効にする"; -"pin_protection_settings_section_header_with_biometrics" = "PINと%@"; -"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_enable_pin" = "PINコードを有効にする"; +"pin_protection_settings_section_header_with_biometrics" = "PINコードと%@"; +"pin_protection_settings_section_header" = "PINコード"; "settings_mentions_and_keywords_encryption_notice" = "携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。"; "settings_new_keyword" = "キーワードを追加"; "settings_your_keywords" = "以下でキーワードを指定できます"; @@ -1435,15 +1435,15 @@ "delete" = "削除"; // actions "action_logout" = "ログアウト"; -"create_room" = "ルームを作る"; +"create_room" = "ルームを作成"; "login" = "ログイン"; "create_account" = "アカウントを作成"; "membership_invite" = "招待しました"; "membership_leave" = "退出しました"; "membership_ban" = "ブロックしました"; -"num_members_one" = "%@ ユーザー"; -"num_members_other" = "%@ ユーザー"; -"kick" = "キック"; +"num_members_one" = "%@人のユーザー"; +"num_members_other" = "%@人のユーザー"; +"kick" = "チャットから追放"; "ban" = "ブロック"; "unban" = "ブロック解除"; "message_unsaved_changes" = "保存されていない変更があります。 退出すると変更は取り消されます。"; @@ -1452,7 +1452,7 @@ "login_error_must_start_http" = "URLは http[s]:// で始まる必要があります"; // room details dialog screen // contacts list screen -"invitation_message" = "私はmatrixであなたとチャットしたい。 詳細はウェブサイトhttp://matrix.orgをお尋ねください。"; +"invitation_message" = "matrixでチャットしましょう。 ウェブサイト http://matrix.org を開いてください。"; // Settings screen "settings_title_config" = "構成"; "settings_title_notifications" = "通知"; @@ -1464,7 +1464,7 @@ "notification_settings_per_word_notifications" = "単語単位の通知"; "notification_settings_per_word_info" = "単語は大文字と小文字を区別せずに一致させ、*ワイルドカードを含めることができます。 従って:\nfooは、区切り文字で囲まれた文字列foo(例 句読点や空白、行の開始/終了)と一致します。\nfoo*は、fooで始まる単語に一致します。\n*foo*は、3文字のfooを含む単語に一致します。"; "notification_settings_always_notify" = "常に通知"; -"notification_settings_never_notify" = "決して通知しない"; +"notification_settings_never_notify" = "通知しない"; "notification_settings_word_to_match" = "一致する単語"; "notification_settings_highlight" = "ハイライト"; "notification_settings_custom_sound" = "カスタムサウンド"; @@ -1473,10 +1473,10 @@ "notification_settings_sender_hint" = "@user:domain.com"; "notification_settings_select_room" = "ルームを選択"; "notification_settings_other_alerts" = "その他のアラート"; -"notification_settings_contain_my_user_name" = "私のユーザー名を含むメッセージについて音で私に通知してください"; -"notification_settings_contain_my_display_name" = "私の表示名が含まれているメッセージが届いた際に音で通知"; -"notification_settings_just_sent_to_me" = "私に送られたメッセージについての音で私に知らせる"; -"notification_settings_invite_to_a_new_room" = "私が新しいルームに招待されたときに知らせる"; +"notification_settings_contain_my_user_name" = "私のユーザー名を含むメッセージについて音で通知"; +"notification_settings_contain_my_display_name" = "私の表示名を含むメッセージについて音で通知"; +"notification_settings_just_sent_to_me" = "私にのみ送信されたメッセージについて音で通知"; +"notification_settings_invite_to_a_new_room" = "新しいルームに招待されたときに通知"; "notification_settings_people_join_leave_rooms" = "誰かがルームに参加もしくは退出したときに通知"; "notification_settings_receive_a_call" = "通話を受信したときに通知"; "notification_settings_suppress_from_bots" = "ボットからの通知を抑制"; @@ -1520,8 +1520,8 @@ "notice_display_name_set_by_you" = "表示名を%@に変更しました"; "notice_display_name_changed_from_by_you" = "表示名を%@から%@に変更しました"; "notice_display_name_removed_by_you" = "表示名を削除しました"; -"notice_topic_changed_by_you" = "トピックを変更しました: %@"; -"notice_room_name_changed_by_you" = "ルームの名前を変更しました: %@"; +"notice_topic_changed_by_you" = "トピックを「%@」に変更しました。"; +"notice_room_name_changed_by_you" = "ルームの名前を%@に変更しました。"; "notice_placed_voice_call_by_you" = "音声通話を開始しました"; "notice_placed_video_call_by_you" = "ビデオ通話を開始しました"; "notice_answered_video_call_by_you" = "電話に出ました"; @@ -1530,7 +1530,7 @@ "notice_room_name_removed_by_you" = "ルーム名を削除しました"; "notice_room_topic_removed_by_you" = "トピックを削除しました"; "notice_profile_change_redacted_by_you" = "プロフィール %@を更新しました"; -"notice_room_created_by_you" = "ルームを作成しました"; +"notice_room_created_by_you" = "ルームを作成し設定しました。"; "notice_encryption_enabled_ok_by_you" = "あなたはエンドツーエンド暗号化をオンにしました。"; "notice_redaction_by_you" = "イベントを編集しました (id: %@)"; "resume_call" = "再開"; @@ -1594,14 +1594,14 @@ // Onboarding "onboarding_splash_register_button_title" = "アカウントを作成"; -"notice_room_created_by_you_for_dm" = "参加しました"; +"notice_room_created_by_you_for_dm" = "参加しました。"; "notice_room_created_for_dm" = "%@が参加しました。"; "onboarding_use_case_existing_server_button" = "サーバーに接続"; "callbar_only_single_active_group" = "タップしてグループ通話に参加 (%@)"; "settings_confirm_media_size" = "送信時のサイズ確認"; "settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面が表示されます。"; "settings_contacts_enable_sync_description" = "IDサーバーを使用して連絡先を探すと同時に、連絡先があなたを探せるようにします。"; -"home_syncing" = "同期中"; +"home_syncing" = "同期しています"; "search_filter_placeholder" = "絞り込む"; // MARK: - Share invite link @@ -1652,9 +1652,9 @@ "spaces_creation_sharing_type_message" = "参加者を選択してください%@。この設定は後から変更できます。"; "spaces_creation_settings_message" = "詳細を入力してください。この設定は後から変更できます。"; "spaces_creation_address_default_message" = "スペースは以下のように表記されます\n%@"; -"space_settings_current_address_message" = "スペースは以下のように表記されます\n%@"; +"space_settings_current_address_message" = "スペースは以下で閲覧できます\n%@"; "space_topic" = "説明文"; -"spaces_creation_cancel_message" = "進捗状況は失われます。"; +"spaces_creation_cancel_message" = "これまでの設定は失われます。"; "spaces_creation_cancel_title" = "スペースの作成を停止しますか?"; "create_room_section_footer_type_private" = "招待した人のみが検索・参加できます。"; @@ -1708,7 +1708,7 @@ "create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; "create_room_suggest_room" = "スペースメンバーにおすすめ"; "create_room_show_in_directory_footer" = "他の人が検索・参加できるようになります。"; -"create_room_promotion_header" = "PR"; +"create_room_promotion_header" = "プロモート"; "searchable_directory_search_placeholder" = "名前または ID"; "room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; "room_suggestion_settings_screen_message" = "おすすめのルームは、スペースメンバーに参加を推奨するものとして PR されます。"; @@ -2624,3 +2624,114 @@ "analytics_prompt_title" = "%@の改善を手伝う"; "event_formatter_call_active_video" = "実行中のビデオ通話"; "event_formatter_call_active_voice" = "実行中の音声通話"; +"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@ 試行)"; +"create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; +"create_room_section_footer_type_public" = "スペース名にあるだけでなく、招待された連絡先のみが検索し、参加できます。"; +"searchable_directory_x_network" = "%@ネットワーク"; +"pin_protection_explanatory" = "PINコードを設定すると、メッセージや連絡先などのデータを保護できます。アプリの開始時にPINコードを入力するよう要求します。"; +"secrets_recovery_with_key_information_default" = "セキュリティーキーを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; +"secrets_recovery_with_passphrase_information_default" = "セキュリティーフレーズを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; +"user_verification_session_details_verify_action_current_user" = "インタラクティブ認証"; +"share_extension_low_quality_video_message" = "%@をより高い品質で送信、あるいは、より低い品質で送信。"; +"room_access_space_chooser_other_spaces_section_info" = "これらは、%@の他の管理者がいるスペースまたはルームです。"; +"room_access_space_chooser_known_spaces_section" = "%@を含む、あなたが知っているスペース"; + +// MARK: - Side menu + +"side_menu_reveal_action_accessibility_label" = "左のパネル"; +"leave_space_selection_all_rooms" = "全てのルームを選択"; +"spaces_add_room_missing_permission_message" = "このスペースにルームを追加する権限がありません。"; +"spaces_creation_invite_by_username_message" = "後から招待することもできます。"; +"spaces_creation_email_invites_message" = "後から招待することもできます。"; +"spaces_creation_address_invalid_characters" = "%@\nには不正な文字があります"; +"spaces_creation_address_already_exists" = "%@\nは既に存在します"; +"spaces_creation_empty_room_name_error" = "名称が必要です"; +"space_settings_update_failed_message" = "スペースの設定の更新に失敗しました。再試行しますか?"; +"spaces_coming_soon_title" = "近日公開"; +"spaces_explore_rooms_format" = "%@を探索"; +"spaces_create_subspace_title" = "サブスペースを作成"; +"space_beta_announce_title" = "スペースは近日公開"; +"space_beta_announce_badge" = "ベータ版"; + +// MARK: - Spaces + +"space_feature_unavailable_title" = "スペースはまだありません"; +"room_invite_to_room_option_title" = "このルームのみ"; +"share_invite_link_room_text" = "こんにちは。%@ からこのルームに参加してください。"; +"share_invite_link_space_text" = "こんにちは。%@ からこのスペースに参加してください。"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "メッセージを保護"; +"secure_key_backup_setup_existing_backup_error_info" = "ロックを解除してセキュアバックアップで再利用するか、削除してセキュアバックアップでメッセージの新しいバックアップを作成。"; +"room_access_settings_screen_restricted_message" = "スペースを誰でも検索し、参加できるようにする。\n対象のスペースを確認してください。"; +"room_details_promote_room_title" = "ルームをプロモート"; +"settings_about" = "概要"; +"call_transfer_error_message" = "通話の転送に失敗しました"; +"call_transfer_contacts_all" = "全て"; +"call_transfer_contacts_recent" = "履歴"; + +// MARK: - Dial Pad +"dialpad_title" = "ダイヤルパッド"; +"create_room_type_restricted" = "スペースの参加者"; +"biometrics_cant_unlocked_alert_message_login" = "再ログイン"; +"biometrics_cant_unlocked_alert_message_x" = "ロックを解除するには、%@を使用するか、再ログインして%@を有効にしてください"; +"biometrics_setup_subtitle" = "時間を節約"; +"biometrics_desetup_disable_button_title_x" = "%@を無効にする"; +"biometrics_desetup_title_x" = "%@を無効にする"; +"pin_protection_kick_user_alert_message" = "多数のエラーが発生したため、ログアウトしました"; +"pin_protection_not_allowed_pin" = "セキュリティー上の理由で、このPINコードは利用できません。他のPINコードを試してください"; +"pin_protection_settings_change_pin" = "PINコードを変更"; +"pin_protection_settings_enabled_forced" = "PINコードが有効です"; +"pin_protection_settings_section_footer" = "PINコードを再設定するには、再ログインして新しいコードを作成してください。"; +"pin_protection_mismatch_too_many_times_error_message" = "PINコードを覚えていない場合は「PINコードを忘れました」をタップしてください。"; +"pin_protection_mismatch_error_message" = "もう一度やり直してください"; +"pin_protection_mismatch_error_title" = "PINコードが一致しません"; +"pin_protection_reset_alert_message" = "PINコードを再設定するには、再ログインして新しいコードを作成してください"; +"pin_protection_reset_alert_title" = "PINコードを再設定"; +"pin_protection_forgot_pin" = "PINコードを忘れました"; +"pin_protection_enter_pin" = "PINコードを入力してください"; +"pin_protection_confirm_pin_to_change" = "PINコードを変更するには、PINコードを確認してください"; +"pin_protection_confirm_pin_to_disable" = "PINコードを無効にするには、PINコードを確認してください"; +"major_update_information" = "アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。"; + +// MARK: - Major update + +"major_update_title" = "Riotは%@になりました"; +"secrets_reset_authentication_message" = "承認するにはMatrixのアカウントのパスワードを入力してください"; +"secrets_setup_recovery_passphrase_summary_information" = "セキュリティーフレーズを記憶。セキュリティーフレーズを使うと、暗号化したメッセージやデータのロックを解除することができます。"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ 印刷して安全な場所で保管\n✓ USBキーやバックアップ用ドライブに保存\n✓ 個人用のクラウドストレージにコピー"; +"secrets_setup_recovery_key_information" = "セキュリティーキーは安全な場所で保管してください。セキュリティーキーを使うと、暗号化したメッセージやデータのロックを解除することができます。"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "正しいセキュリティーキーを入力したことを確かめてください。"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "続行するにはセキュリティーフレーズを入力してください。"; +"secrets_recovery_with_key_information_verify_device" = "セキュリティーキーを使用して、この端末を認証してください。"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "正しいセキュリティーフレーズを入力したことを確かめてください。"; +"secrets_recovery_with_passphrase_recover_action" = "セキュリティーフレーズを使用"; +"secrets_recovery_with_passphrase_information_verify_device" = "セキュリティーフレーズを使用して、この端末を認証してください。"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "全ての復旧用の手段を忘れたか、無くしましたか? "; +"user_verification_session_details_additional_information_untrusted_current_user" = "このセッションにサインインしなかった場合、あなたのアカウントの安全性が損なわれている可能性があります。"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "他の認証方法を確認しています…"; +"device_verification_self_verify_wait_additional_information" = "これは%@と、クロス署名に対応した他のMatrixのクライアントで機能します。"; +"device_verification_self_verify_wait_information" = "暗号化されたメッセージにアクセスするには、あなたの他のセッションからこのセッションを認証する必要があります。\n\n他の端末で最新の%@を使用してください:"; +"key_verification_self_verify_current_session_alert_message" = "他のユーザーは信頼しないかもしれません。"; +"device_verification_start_use_legacy" = "何も表示されますか?まだ全てのクライアントはインタラクティブな認証をサポートしていません。レガシー認証を使用してください。"; +"device_verification_start_wait_partner" = "相手の承諾を待機しています…"; +"key_verification_user_title" = "認証"; +"key_verification_new_session_title" = "新しいセッションを認証"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "待機します"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "暗号化されたメッセージを失います"; +"secure_key_backup_setup_existing_backup_error_title" = "メッセージのバックアップは既に存在します"; +"service_terms_modal_policy_checkbox_accessibility_hint" = "チェックして%@を承諾してください"; +"service_terms_modal_information_description_integration_manager" = "インテグレーションマネージャーを使うと、第三者による機能を追加することができます。"; +"service_terms_modal_information_description_identity_server" = "IDサーバーを使うと、電話番号やメールアドレスを検索して、連絡先が既にアカウントをもっているかどうか確認することができます。"; +"service_terms_modal_description_integration_manager" = "ボット、ブリッジ、ウィジェット、ステッカーパックの使用を許可します。"; +"share_extension_low_quality_video_title" = "動画を低品質で送信します"; +"analytics_prompt_yes" = "はい、大丈夫です"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "規約を%@で確認してください。よろしいですか?"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "規約は%@で確認できます。"; +"analytics_prompt_message_upgrade" = "あなたは以前、利用状況に関する匿名データの共有に同意しました。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。"; From 53d8440af3065a0d78395a63f98e3913722b6674 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 1 Feb 2023 09:00:37 +0000 Subject: [PATCH 423/468] Translated using Weblate (Italian) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index b26bb2ec2..a88a64ded 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2690,6 +2690,12 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migrazione dati\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Crypto SDK attivato. Per disattivarlo devi reinstallare l'app"; -"settings_labs_confirm_crypto_sdk" = "Quest'azione è irreversibile"; -"settings_labs_enable_crypto_sdk" = "Attiva il nuovo Crypto SDK basato su rust"; +"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end 2.0 (disconnettiti per disattivarla)"; +"settings_labs_confirm_crypto_sdk" = "Questa opzione attiverà un motore nuovo, più veloce e affidabile per la crittografia end-to-end scritto in Rust. Una volta attivato, dovrai disconnetterti per disattivarlo. Vuoi procedere?"; +"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end 2.0"; +"wysiwyg_composer_format_action_un_indent" = "Diminuisci indentazione"; +"wysiwyg_composer_format_action_indent" = "Aumenta indentazione"; +"poll_history_fetching_error" = "Errore di recupero dei sondaggi."; +"voice_broadcast_playback_unable_to_decrypt" = "Impossibile decifrare questa trasmissione vocale."; +"home_context_menu_mark_as_unread" = "Segna come non letto"; +"key_backup_recover_from_private_key_progress" = "%@%% Completato"; From 7dc15540d7e54c30d92454443ddd98f58834606f Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 1 Feb 2023 09:33:33 +0000 Subject: [PATCH 424/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 9bce65821..93b8b58c1 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2922,3 +2922,5 @@ "key_backup_recover_from_private_key_progress" = "%@%% виконано"; "voice_broadcast_playback_unable_to_decrypt" = "Неможливо розшифрувати цю голосову трансляцію."; "home_context_menu_mark_as_unread" = "Позначити непрочитаним"; +"wysiwyg_composer_format_action_un_indent" = "Зменшити відступ"; +"wysiwyg_composer_format_action_indent" = "Збільшити відступ"; From b23d5adca11d109886dd17f2f07683080ead5258 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 1 Feb 2023 10:48:56 +0000 Subject: [PATCH 425/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 4b3a2078e..709eec675 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2920,3 +2920,5 @@ "voice_broadcast_playback_unable_to_decrypt" = "Toto hlasové vysielanie sa nedá dešifrovať."; "home_context_menu_mark_as_unread" = "Označiť ako neprečítané"; "key_backup_recover_from_private_key_progress" = "%@%% Dokončené"; +"wysiwyg_composer_format_action_indent" = "Zväčšenie odsadenia"; +"wysiwyg_composer_format_action_un_indent" = "Zmenšenie odsadenia"; From bc98a4c03eea72cac45fca967e3c6eac40b976b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 1 Feb 2023 12:38:32 +0000 Subject: [PATCH 426/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 7eb797ad1..d67d5f79c 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2669,3 +2669,5 @@ "key_backup_recover_from_private_key_progress" = "%@%% tehtud"; "voice_broadcast_playback_unable_to_decrypt" = "Selle ringhäälingukõne dekrüptimine ei õnnestu."; "home_context_menu_mark_as_unread" = "Märgi mitteloetuks"; +"wysiwyg_composer_format_action_un_indent" = "Vähenda taandrida"; +"wysiwyg_composer_format_action_indent" = "Suurenda taandrida"; From 6fe3fa8136f92c0172d6390dd7b5dae9ef001dc4 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 1 Feb 2023 12:52:23 +0000 Subject: [PATCH 427/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2376 of 2376 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 33d239790..21ff49f35 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2924,3 +2924,5 @@ "key_backup_recover_from_private_key_progress" = "%@%% Selesai"; "voice_broadcast_playback_unable_to_decrypt" = "Tidak dapat mendekripsi siaran suara ini."; "home_context_menu_mark_as_unread" = "Tandai sebagai belum dibaca"; +"wysiwyg_composer_format_action_un_indent" = "Kurangi indentasi"; +"wysiwyg_composer_format_action_indent" = "Tambahkan indentasi"; From fceef3ec9d5853867be3c5d5ab48859d8108a3f5 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 1 Feb 2023 14:07:29 +0000 Subject: [PATCH 428/468] Translated using Weblate (German) Currently translated at 100.0% (2377 of 2377 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 00f370eed..17c6b42be 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2733,3 +2733,4 @@ "home_context_menu_mark_as_unread" = "Als ungelesen markieren"; "wysiwyg_composer_format_action_un_indent" = "Einrückung verringern"; "wysiwyg_composer_format_action_indent" = "Einrückung erhöhen"; +"settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten."; From f5257687b804b7f9606fe755c5ec72b8b637e215 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 1 Feb 2023 14:34:06 +0000 Subject: [PATCH 429/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2377 of 2377 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 93b8b58c1..3ddfd1586 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2924,3 +2924,4 @@ "home_context_menu_mark_as_unread" = "Позначити непрочитаним"; "wysiwyg_composer_format_action_un_indent" = "Зменшити відступ"; "wysiwyg_composer_format_action_indent" = "Збільшити відступ"; +"settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз."; From 8626eec60ea1ea52bcf0208a6a4025073889a07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 1 Feb 2023 14:33:02 +0000 Subject: [PATCH 430/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2377 of 2377 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index d67d5f79c..866a2c1ac 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2671,3 +2671,4 @@ "home_context_menu_mark_as_unread" = "Märgi mitteloetuks"; "wysiwyg_composer_format_action_un_indent" = "Vähenda taandrida"; "wysiwyg_composer_format_action_indent" = "Suurenda taandrida"; +"settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada."; From 664692a190d14b07e38cc42478d47ba9e31e0fb2 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 1 Feb 2023 15:04:52 +0000 Subject: [PATCH 431/468] Translated using Weblate (German) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 17c6b42be..c9ee49d16 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2734,3 +2734,4 @@ "wysiwyg_composer_format_action_un_indent" = "Einrückung verringern"; "wysiwyg_composer_format_action_indent" = "Einrückung erhöhen"; "settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten."; +"poll_history_detail_view_in_timeline" = "Umfrage in Verlauf anzeigen"; From aa19b1ca54305936ecf305395f396278f39fbbdd Mon Sep 17 00:00:00 2001 From: bluelullaby6 Date: Wed, 1 Feb 2023 23:05:32 +0000 Subject: [PATCH 432/468] Translated using Weblate (French) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 135 +++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 0a846c5c1..4ea0610d6 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -2529,7 +2529,7 @@ "device_name_desktop" = "%@ Bureau"; "user_inactive_session_item_with_date" = "Inactif depuis 90 jours ou plus (%@)"; "user_inactive_session_item" = "Inactif depuis 90 jours ou plus"; -"user_session_item_details" = "%@ · Dernière activité %@"; +"user_session_item_details" = "%1$@ · %2$@"; // First item is client name and second item is session display name "user_session_name" = "%@ : %@"; @@ -2606,3 +2606,136 @@ "manage_session_name_info" = "Gardez en tête que les noms des sessions sont aussi visibles par les personnes avec qui vous communiquez. %@"; "manage_session_name_hint" = "Personnaliser les noms des sessions peut vous aider à reconnaître vos appareils plus facilement."; "settings_labs_enable_wysiwyg_composer" = "Essayez le compositeur de messages visuel"; +"settings_labs_enable_voice_broadcast" = "Diffusion vocale"; +"wysiwyg_composer_format_action_un_indent" = "Diminuer le retrait"; +"wysiwyg_composer_format_action_indent" = "Augmenter le retrait"; +"wysiwyg_composer_format_action_code_block" = "Bloc de code"; +"wysiwyg_composer_start_action_stickers" = "Autocollants"; +"user_session_rename_session_title" = "Renommer les sessions"; +"user_session_verified_session_description" = "Les sessions vérifiées sont toutes celles où vous vous êtes connecté à Element grâce à vos identifiants ou celles pour lesquelles vous avez confirmé votre identité à l'aide d'une autre session.\n\nCela signifie que vous êtes en possession de toutes les clés requises pour déchiffrer vos messages et montrer aux autres utilisateurs que vous faites confiance à cette session."; +"poll_history_loading_text" = "Afficher les sondages"; +"voice_message_broadcast_in_progress_title" = "Impossible de démarrer l'enregistrement vocal"; +"home_context_menu_mark_as_unread" = "Marquer comme non lu"; +"launch_loading_processing_response" = "Traitement des données\n%@ %%"; +"notice_voice_broadcast_ended_by_you" = "Vous avez terminé une diffusion vocale."; +"notice_voice_broadcast_ended" = "%@ a terminé une diffusion vocale."; +"notice_voice_broadcast_live" = "Diffusion en direct"; +"deselect_all" = "Tout désélectionner"; +"wysiwyg_composer_link_action_edit_title" = "Modifier le lien"; +"wysiwyg_composer_link_action_create_title" = "Créer un lien"; +"wysiwyg_composer_link_action_link" = "Lien"; + +// Links +"wysiwyg_composer_link_action_text" = "Texte"; +"wysiwyg_composer_format_action_quote" = "Citation"; +"wysiwyg_composer_format_action_ordered_list" = "Liste numérique"; +"wysiwyg_composer_format_action_unordered_list" = "Liste à puces"; +"wysiwyg_composer_format_action_inline_code" = "Formater comme code informatique"; +"wysiwyg_composer_format_action_link" = "Formater comme lien"; +"wysiwyg_composer_format_action_strikethrough" = "Souligner"; +"wysiwyg_composer_format_action_underline" = "Barrer"; +"wysiwyg_composer_format_action_italic" = "Mettre en italique"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Mettre en caractères gras"; +"wysiwyg_composer_start_action_voice_broadcast" = "Diffusion vocale"; +"wysiwyg_composer_start_action_text_formatting" = "Formatage du texte"; +"wysiwyg_composer_start_action_camera" = "Appareil photo"; +"wysiwyg_composer_start_action_location" = "Position"; +"wysiwyg_composer_start_action_polls" = "Sondages"; +"wysiwyg_composer_start_action_attachments" = "Pièces jointes"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Galerie photo"; +"user_session_details_last_activity" = "Dernière activité"; +"user_session_item_details_last_activity" = "Dernière activité %@"; +"user_other_session_menu_sign_out_sessions" = "Déconnecter %@ sessions"; +"user_other_session_selected_count" = "%@ sélectionnées"; +"user_other_session_menu_select_sessions" = "Sélectionnez des sessions"; +"user_other_session_clear_filter" = "Effacer les filtres"; +"user_other_session_no_unverified_sessions" = "Aucune session non vérifiée trouvée."; +"user_other_session_no_verified_sessions" = "Aucune session vérifiée trouvée."; +"user_other_session_no_inactive_sessions" = "Aucune session inactive trouvée."; +"user_other_session_filter_menu_inactive" = "Inactives"; +"user_other_session_filter_menu_unverified" = "Non vérifiées"; +"user_other_session_filter_menu_verified" = "Vérifiées"; +"user_other_session_filter_menu_all" = "Toutes les sessions"; +"user_other_session_filter" = "Filtrer"; +"user_other_session_verified_sessions_header_subtitle" = "Pour augmenter la sécurité, veuillez déconnecter toutes les sessions qui vous semblent inconnues ou que vous n'utilisez plus."; +"user_other_session_current_session_details" = "Votre session actuelle"; +"user_other_session_security_recommendation_title" = "Autres sessions"; +"user_session_rename_session_description" = "D'autres utilisateurs des conversations et salons que vous rejoignez peuvent consulter la liste complète de vos session.\n\nCela leur permet de confirmer qu'ils communiquent bien avec vous, mais cela signifie également qu'ils verront le nom que vous donnez à vos sessions."; +"user_session_inactive_session_description" = "Les sessions inactives sont celles qui n'ont pas été utilisées depuis un certain temps, mais qui continuent de recevoir des clés de chiffrement.\n\nÉliminer ces sessions inactives augmente la sécurité et les performances, et facilite l'identification de nouvelles connexions suspectes."; +"user_session_inactive_session_title" = "Sessions inactives"; +"user_session_permanently_unverified_session_description" = "Cette session de prend pas en charge le chiffrement et ne peut donc être vérifiée.\n\nVous ne pourrez pas intervenir dans les salons où le chiffrement est activé en utilisant cette session.\n\nPour une sécurité et confidentialité optimale, il est recommandé d'utiliser des clients Matrix qui prennent en charge le chiffrement."; +"user_session_unverified_session_description" = "Les sessions non vérifiez sont celles qui sont connectées avec vos identifiants, mais qui n'ont pas passé les vérifications croisées.\n\nVous devriez passer en revue ces sessions car elles pourraient témoigner d'un usage malicieux de votre compte."; +"user_session_unverified_session_title" = "Session non vérifiée"; +"user_session_verified_session_title" = "Sessions vérifiées"; +"user_session_got_it" = "Entendu"; +"user_other_session_verified_additional_info" = "Cette session est prête à l'échange de messages."; +"user_other_session_permanently_unverified_additional_info" = "Cette session ne prend pas en charge le chiffrement et ne peut donc être vérifiée."; +"user_other_session_unverified_additional_info" = "Vérifier ou déconnecter cette session pour une sécurité et une fiabilité accrue."; +"user_session_verification_unknown_additional_info" = "Vérifier la session actuelle pour révéler l'état de vérification de cette session."; +"user_session_verification_unknown_short" = "Inconnu"; +"user_session_verification_unknown" = "État de vérification inconnu"; +"user_sessions_hide_location_info" = "Masquer l'adresse IP"; +"user_sessions_show_location_info" = "Montrer l'adresse IP"; +"poll_timeline_reply_ended_poll" = "Sondage terminé"; +"poll_timeline_ended_text" = "Sondage clos"; +"poll_timeline_decryption_error" = "Des erreurs de déchiffrement pourrait empêcher certains votes d'être comptabilisés"; +"poll_history_fetching_error" = "Erreur au cours de la récupération des sondages."; +"poll_history_load_more" = "Charger plus de sondages"; +"poll_history_no_past_poll_period_text" = "Il n'y a pas eu de sondages les %@ derniers jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs"; +"poll_history_no_active_poll_period_text" = "Il n'y a pas eu de sondages depuis %@ jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs"; +"poll_history_detail_view_in_timeline" = "Consulter la chronologie des sondages"; +"poll_history_no_past_poll_text" = "Il n'y a pas de sondage précédent dans ce salon"; +"poll_history_no_active_poll_text" = "Il n'y a aucun sondage en cours dans ce salon"; +"poll_history_past_segment_title" = "Sondages précédents"; +"poll_history_active_segment_title" = "Sondages en cours"; + +// MARK: - Polls history + +"poll_history_title" = "Historique des sondages"; +"voice_broadcast_playback_unable_to_decrypt" = "Impossible de déchiffrer cette diffusion vocale."; +"voice_broadcast_recorder_connection_error" = "Erreur de connexion - Enregistrement interrompu"; +"voice_broadcast_connection_error_message" = "Nous sommes malheureusement dans l'impossibilité de démarrer un enregistrement maintenant. Veuillez réessayer plus tard."; +"voice_broadcast_connection_error_title" = "Erreur de connexion"; +"voice_broadcast_voip_cannot_start_description" = "Vous ne pouvez pas démarrer d'appel car vous enregistrez déjà une diffusion en direct. Veuillez interrompre votre diffusion pour lancer un appel."; +"voice_broadcast_voip_cannot_start_title" = "Impossible de démarrer l'appel"; +"voice_broadcast_stop_alert_agree_button" = "Oui, terminer"; +"voice_broadcast_stop_alert_description" = "Êtes vous sûr de vouloir interrompre votre diffusion vocale ? Cela mettra fin à la diffusion et rendra l'enregistrement disponible dans le salon."; +"voice_broadcast_stop_alert_title" = "Arrêter la diffusion vocale ?"; +"voice_broadcast_buffering" = "Mise en mémoire tampon..."; +"voice_broadcast_time_left" = "%@ restant"; +"voice_broadcast_tile" = "Diffusion vocale"; +"voice_broadcast_live" = "En direct"; +"voice_broadcast_playback_lock_screen_placeholder" = "Diffusion vocale"; +"voice_broadcast_playback_loading_error" = "Impossible de lire cette diffusion vocale."; +"voice_broadcast_blocked_by_someone_else_message" = "Quelqu'un d'autre est déjà en train d'enregistrer une diffusion vocale. Veuillez attendre la fin de la leur pour en démarrer une nouvelle."; +"voice_broadcast_already_in_progress_message" = "Vous êtes déjà en train d'enregistrer une diffusion vocale. Veuillez y mettre fin avant d'en démarrer une nouvelle."; +"voice_broadcast_permission_denied_message" = "Vous n'avez pas les autorisations nécessaires pour démarrer une diffusion vocal dans ce salon. Contactez un administrateur pour qu'il vous octroie la permission."; + +// MARK: - Voice Broadcast +"voice_broadcast_unauthorized_title" = "Impossible de démarrer une nouvelle diffusion vocale"; +"voice_message_broadcast_in_progress_message" = "Vous ne pouvez pas démarrer d'enregistrement vocal car vous diffusez en direct. Veuillez interrompre votre diffusion pour démarrer l'enregistrement vocal"; +"launch_loading_server_syncing_nth_attempt" = "Synchronisation avec le serveur\n(%@ tentatives)"; +"launch_loading_server_syncing" = "Synchronisation avec le serveur"; + +// MARK: - Launch loading + +"launch_loading_migrating_data" = "Migration des données\n%@ %%"; +"key_backup_recover_from_private_key_progress" = "%@%% Fini"; +"room_details_polls" = "Historique des sondages"; +"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout 2.0 (se déconnecter pour désactiver)"; +"settings_labs_confirm_crypto_sdk" = "Cette option activera un nouvel engin pour le chiffrement de bout en bout, plus rapide et plus fiable, écrit en Rust. Une fois activé vous devrez vous déconnecter pour le désactiver. Voulez-vous continuer?"; +"settings_labs_enable_crypto_sdk" = "Chiffrement de bout en bout 2.0"; +"settings_push_rules_error" = "Nous avons rencontré une erreur lors de la mise à jours de vos préférences de notification. Veuillez réactiver l'option."; +"password_policy_pwd_in_dict_error" = "Ce mot de passe a été trouvé dans un dictionnaire, et son usage n'est donc pas autorisé."; +"password_policy_weak_pwd_error" = "Ce mot de passe est trop faible. Il doit contenir au moins 8 caractères, dont au moins une majuscule, une minuscule, un chiffre et un caractère spécial."; + +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "Mot de passe trop court"; +"accessibility_selected" = "sélectionné"; From 2d1de7e84f00abe4f4f9beb7efb2092afebd68f2 Mon Sep 17 00:00:00 2001 From: J H Date: Thu, 2 Feb 2023 06:21:14 +0000 Subject: [PATCH 433/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 83.0% (1975 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 5ad8c44fa..a503d920b 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -2278,3 +2278,10 @@ "authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的二维码。"; "room_invite_to_space_option_detail" = "他们可以探索 %@,但不会成为 %@ 的成员。"; "analytics_prompt_message_new_user" = "通过分享匿名的使用数据,帮助我们识别问题并改进 %@ 。为了了解人们如何使用多个设备,我们将生成一个随机的标识符,由你的设备共享。"; +"threads_notice_done" = "知道了"; +"message_from_a_thread" = "来自消息列"; +"threads_empty_info_all" = "消息列帮助你的对话不离题且易于跟踪。"; +"accessibility_selected" = "已选中"; +"deselect_all" = "取消全选"; +"notice_voice_broadcast_ended" = "%@结束了一个语音广播。"; +"notice_voice_broadcast_ended_by_you" = "你结束了一个语音广播。"; From a1964b8426551dca9177568f707bfbec93c9c8cf Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Feb 2023 10:49:11 +0000 Subject: [PATCH 434/468] Translated using Weblate (Japanese) Currently translated at 99.9% (2377 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 577 ++++++++++++++++------------ 1 file changed, 333 insertions(+), 244 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 0bc575f22..987aefa1f 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -8,7 +8,7 @@ "view" = "表示"; "next" = "次へ"; "back" = "戻る"; -"continue" = "続ける"; +"continue" = "続行"; "create" = "作成"; "start" = "開始"; "leave" = "退出"; @@ -20,27 +20,27 @@ "cancel" = "キャンセル"; "save" = "保存"; "join" = "参加"; -"decline" = "断る"; +"decline" = "拒否"; "accept" = "受諾"; "preview" = "プレビュー"; "camera" = "カメラ"; "voice" = "音声"; "video" = "動画"; -"active_call" = "通話開始"; -"active_call_details" = "通話開始(%@)"; +"active_call" = "実施中の通話"; +"active_call_details" = "実施中の通話(%@)"; "later" = "後で"; "rename" = "名前変更"; "collapse" = "折りたたむ"; -"send_to" = "%@さんへ送信"; +"send_to" = "%@へ送信"; "sending" = "送信しています"; // Authentication "auth_login" = "ログイン"; -"auth_register" = "利用者登録"; +"auth_register" = "登録"; "auth_submit" = "受諾"; "auth_skip" = "スキップ"; -"auth_send_reset_email" = "初期化メール送信"; -"auth_return_to_login" = "ログイン画面へ戻る"; -"auth_user_id_placeholder" = "ユーザー名または電子メール"; +"auth_send_reset_email" = "リセット用メールを送信"; +"auth_return_to_login" = "ログイン画面に戻る"; +"auth_user_id_placeholder" = "電子メールまたはユーザー名"; "auth_password_placeholder" = "パスワード"; "auth_new_password_placeholder" = "新しいパスワード"; "auth_user_name_placeholder" = "ユーザー名"; @@ -48,15 +48,15 @@ "auth_email_placeholder" = "メールアドレス"; "auth_optional_phone_placeholder" = "電話番号(任意)"; "auth_phone_placeholder" = "電話番号"; -"auth_repeat_password_placeholder" = "パスワード再確認"; -"auth_repeat_new_password_placeholder" = "新しいパスワードを再確認"; +"auth_repeat_password_placeholder" = "パスワードを再確認"; +"auth_repeat_new_password_placeholder" = "Matrixアカウントの新しいパスワードを確認"; "auth_home_server_placeholder" = "URL (例 https://matrix.org)"; "auth_identity_server_placeholder" = "URL (例 https://vector.im)"; -"auth_invalid_login_param" = "ユーザー名かパスワードが正しくありません"; -"auth_invalid_user_name" = "ユーザー名は半角英数字、ドット、ハイフン、アンダスコアのみで記して下さい"; -"auth_invalid_password" = "パスワードが短すぎます(最小6文字)"; -"auth_invalid_email" = "メールアドレスの形式が正しくありません"; -"auth_invalid_phone" = "正しくない電話番号のようです"; +"auth_invalid_login_param" = "ユーザー名とパスワードの一方あるいは両方が正しくありません"; +"auth_invalid_user_name" = "ユーザー名には半角英数字、ドット、ハイフン、アンダースコアのみを使用してください"; +"auth_invalid_password" = "パスワードが短すぎます(最小6文字)"; +"auth_invalid_email" = "メールアドレスの形式が正しくないようです"; +"auth_invalid_phone" = "電話番号の形式が正しくないようです"; "auth_missing_password" = "パスワードが入力されていません"; "auth_add_email_message" = "電子メールアドレスを登録すると, 誰かがあなたを検索をしたり, パスワード紛失時に初期化のメールを送ることができます."; "auth_add_phone_message" = "電話番号を登録すると, 誰かがあなたを電話番号で検索できるようになります."; @@ -67,38 +67,38 @@ "auth_missing_email_or_phone" = "メールアドレスまたは電話番号が入力されていません"; "auth_email_in_use" = "このメールアドレスは既に使用されています"; "auth_phone_in_use" = "この電話番号は既に使用されています"; -"auth_untrusted_id_server" = "この認証サーバーは信用されていません"; +"auth_untrusted_id_server" = "この認証サーバーは信頼されていません"; "auth_password_dont_match" = "パスワードが一致しません"; "auth_username_in_use" = "ユーザー名は既に使用されています"; -"auth_forgot_password" = "パスワードを忘れましたか?"; +"auth_forgot_password" = "Matrixのアカウントのパスワードを忘れましたか?"; "auth_email_not_found" = "電子メールの送信に失敗しました:メールアドレスが見つかりません"; -"auth_use_server_options" = "接続先サーバーを指定する(追加設定)"; -"auth_email_validation_message" = "登録を続行するには電子メールを確認して下さい"; -"auth_msisdn_validation_title" = "認証を確認中"; -"auth_msisdn_validation_message" = "SMSで認証番号を送りました。以下にその番号を入力してください。"; +"auth_use_server_options" = "接続先サーバーを指定(高度)"; +"auth_email_validation_message" = "登録を続行するには電子メールを確認してください"; +"auth_msisdn_validation_title" = "認証の保留中"; +"auth_msisdn_validation_message" = "SMSで認証コードを送りました。以下にコードを入力してください。"; "auth_msisdn_validation_error" = "電話番号を認証できません。"; "auth_recaptcha_message" = "このホームサーバーは、あなたがロボットではないことの確認を求めています"; -"auth_reset_password_message" = "Matrixのアカウントのパスワードを初期化するには、アカウントに登録されているメールアドレスを入力してください:"; +"auth_reset_password_message" = "Matrixのアカウントのパスワードを再設定するには、アカウントに登録されているメールアドレスを入力してください:"; "auth_reset_password_missing_email" = "あなたのアカウントに登録されたメールアドレスの入力が必要です。"; "auth_reset_password_missing_password" = "新しいパスワードの入力が必要です。"; -"auth_reset_password_email_validation_message" = "%@ へ電子メールが送信されました。リンクをたどったら以下をクリックしてください。"; -"auth_reset_password_next_step_button" = "メールアドレスを認証しました"; -"auth_reset_password_error_unauthorized" = "メールアドレスの確認に失敗しました:電子メールのリンクをクリックしたことを確認してください"; -"auth_reset_password_error_not_found" = "あなたのメールアドレスは、接続先サーバー上のMatrix IDと関連付けられていないようです。"; -"auth_reset_password_success_message" = "あなたのパスワードは初期化されました。\n\nあなたは全てのセッションから切断しており、プッシュ通知を受け取ることはありません。通知を再度有効にするには、各端末に再度ログインします。"; -"auth_add_email_and_phone_warning" = "電子メールと電話番号の同時登録は、まだシステムが対応できません。電話番号だけの登録は可能です。お手数おかけしますが、後ほど個人情報設定からメールアドレスを登録してください。"; +"auth_reset_password_email_validation_message" = "%@ へ電子メールを送信しました。電子メール内のリンクを開いた後、以下をクリックしてください。"; +"auth_reset_password_next_step_button" = "メールアドレスを確認しました"; +"auth_reset_password_error_unauthorized" = "メールアドレスの認証に失敗しました:電子メール内のリンクを開いたことを確認してください"; +"auth_reset_password_error_not_found" = "あなたのメールアドレスは、このホームサーバー上のMatrix IDと関連付けられていないようです。"; +"auth_reset_password_success_message" = "あなたのMatrixのアカウントのパスワードは初期化されました。\n\n全てのセッションからログアウトしたため、プッシュ通知は送信されません。通知を再度有効にするには、各端末で再度ログインしてください。"; +"auth_add_email_and_phone_warning" = "電子メールと電話番号の両方による登録は、まだサポートしていません。電話番号のみでの登録を受け付けています。メールアドレスは、設定内のプロフィールから後ほど追加できます。"; // Chat creation "room_creation_title" = "チャットを開始"; "room_creation_account" = "アカウント"; "room_creation_appearance" = "外観"; "room_creation_appearance_name" = "名前"; "room_creation_appearance_picture" = "チャット画像(任意)"; -"room_creation_privacy" = "個人情報保護"; +"room_creation_privacy" = "プライバシー"; "room_creation_private_room" = "この会話は非公開です"; "room_creation_public_room" = "この会話は公開されています"; "room_creation_make_public" = "公開"; "room_creation_make_public_prompt_title" = "このチャットを公開しますか?"; -"room_creation_make_public_prompt_msg" = "このチャットを公開してもよろしいですか?誰でもあなたのメッセージを読んでチャットに参加できます。"; +"room_creation_make_public_prompt_msg" = "このチャットを公開してよろしいですか?誰でもあなたのメッセージを読み、チャットに参加できます。"; "room_creation_keep_private" = "非公開に保つ"; "room_creation_make_private" = "非公開にする"; "room_creation_wait_for_creation" = "ルームは既に作成されています。お待ちください。"; @@ -113,8 +113,8 @@ "room_recents_invites_section" = "招待中"; "room_recents_start_chat_with" = "チャットを開始"; "room_recents_create_empty_room" = "ルームを作成"; -"room_recents_join_room" = "ルームへ参加"; -"room_recents_join_room_title" = "ルームへ参加"; +"room_recents_join_room" = "ルームに参加"; +"room_recents_join_room_title" = "ルームに参加"; "room_recents_join_room_prompt" = "ルームIDまたはルームのエイリアスを入力"; // People tab "people_invites_section" = "招待中"; @@ -126,10 +126,10 @@ "search_rooms" = "ルーム"; "search_messages" = "メッセージ"; "search_people" = "連絡先"; -"search_files" = "添付ファイル"; +"search_files" = "ファイル"; "search_default_placeholder" = "検索"; "search_people_placeholder" = "ユーザーID、表示名、電子メールで検索"; -"search_no_result" = "結果なし"; +"search_no_result" = "結果がありません"; "search_in_progress" = "検索しています…"; // Directory "directory_cell_title" = "ルーム一覧を見る"; @@ -139,11 +139,11 @@ "directory_search_fail" = "一覧を取得できませんでした"; // Contacts "contacts_address_book_section" = "端末の電話帳"; -"contacts_address_book_matrix_users_toggle" = "Matrix利用者のみ"; -"contacts_address_book_no_contact" = "端末内電話帳に連絡先がありません"; -"contacts_address_book_permission_required" = "端末内電話帳へのアクセス権限が必要です"; +"contacts_address_book_matrix_users_toggle" = "Matrixのユーザーのみ"; +"contacts_address_book_no_contact" = "端末の電話帳に連絡先がありません"; +"contacts_address_book_permission_required" = "端末の電話帳へのアクセス権限が必要です"; "contacts_user_directory_section" = "ユーザー一覧"; -"contacts_user_directory_offline_section" = "ユーザー一覧 (オフライン)"; +"contacts_user_directory_offline_section" = "ユーザー一覧(オフライン)"; // Chat participants "room_participants_title" = "参加者"; "room_participants_add_participant" = "参加者を追加"; @@ -152,14 +152,14 @@ "room_participants_leave_prompt_title" = "ルームから退出"; "room_participants_leave_prompt_msg" = "ルームから退出してよろしいですか?"; "room_participants_remove_prompt_title" = "確認"; -"room_participants_remove_prompt_msg" = "本当に%@をチャットから退去させますか?"; +"room_participants_remove_prompt_msg" = "%@をチャットから追放してよろしいですか?"; "room_participants_remove_third_party_invite_msg" = "サードパーティの招待を削除することは、APIが存在するまでサポートされていません"; "room_participants_invite_prompt_title" = "確認"; -"room_participants_invite_prompt_msg" = "%@をチャットに招待してよろしいですか?"; -"room_participants_filter_room_members" = "ルームメンバーを検索"; +"room_participants_invite_prompt_msg" = "%@をこのチャットに招待してよろしいですか?"; +"room_participants_filter_room_members" = "ルームのメンバーを検索"; "room_participants_invite_another_user" = "ユーザーID、名前、電子メールで検索、招待"; "room_participants_invite_malformed_id_title" = "招待エラー"; -"room_participants_invite_malformed_id" = "不正なIDです。メールアドレスを用いるか、'@localpart:domain'のようなMatrix IDを使用してください"; +"room_participants_invite_malformed_id" = "不正なIDです。メールアドレスを用いるか、'@localpart:domain'の形式のMatrix IDを使用してください"; "room_participants_invited_section" = "招待中"; "room_participants_online" = "オンライン"; "room_participants_offline" = "オフライン"; @@ -167,37 +167,37 @@ "room_participants_idle" = "アイドル"; "room_participants_now" = "現在"; "room_participants_ago" = "前"; -"room_participants_action_section_admin_tools" = "管理者権限操作"; -"room_participants_action_section_direct_chats" = "非公開のチャット"; -"room_participants_action_section_devices" = "セッション一覧"; +"room_participants_action_section_admin_tools" = "管理者ツール"; +"room_participants_action_section_direct_chats" = "ダイレクトメッセージ"; +"room_participants_action_section_devices" = "セッション"; "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; "room_participants_action_leave" = "このルームから退出"; "room_participants_action_remove" = "このルームから削除"; "room_participants_action_ban" = "このルームからブロック"; "room_participants_action_unban" = "ブロックを解除"; -"room_participants_action_ignore" = "このユーザーの発言を全て非表示にする"; -"room_participants_action_unignore" = "このユーザーの発言を全て表示"; +"room_participants_action_ignore" = "このユーザーのメッセージを全て非表示にする"; +"room_participants_action_unignore" = "このユーザーのメッセージを全て表示"; "room_participants_action_set_default_power_level" = "権限を一般ユーザーへ変更"; "room_participants_action_set_moderator" = "権限をモデレーターへ変更"; "room_participants_action_set_admin" = "権限を管理者へ変更"; "room_participants_action_start_new_chat" = "チャットを開始"; "room_participants_action_start_voice_call" = "音声通話を開始"; -"room_participants_action_start_video_call" = "映像付き音声通話を開始"; +"room_participants_action_start_video_call" = "ビデオ通話を開始"; "room_participants_action_mention" = "メンション"; // Chat -"room_jump_to_first_unread" = "最初の未読位置へ移動"; +"room_jump_to_first_unread" = "最新の未読へ移動"; "room_new_message_notification" = "%d件の新しいメッセージ"; "room_new_messages_notification" = "%d件の新しいメッセージ"; -"room_one_user_is_typing" = "%@さんが入力しています…"; -"room_two_users_are_typing" = "%@さん、%@さんが入力しています…"; -"room_many_users_are_typing" = "%@さん、%@さん他が入力しています…"; -"room_message_placeholder" = "返信を送る(未暗号化)…"; +"room_one_user_is_typing" = "%@が入力しています…"; +"room_two_users_are_typing" = "%@と%@が入力しています…"; +"room_many_users_are_typing" = "%@、%@他が入力しています…"; +"room_message_placeholder" = "メッセージを送信(暗号化されていません)…"; "encrypted_room_message_placeholder" = "暗号化されたメッセージを送信…"; "room_message_short_placeholder" = "メッセージを送信…"; "room_offline_notification" = "サーバーとの接続が失われました。"; "room_unsent_messages_notification" = "メッセージを送信できませんでした。"; -"room_unsent_messages_unknown_devices_notification" = "未知のセッションが存在するために文章が送信されませんでした。"; +"room_unsent_messages_unknown_devices_notification" = "不明なセッションが存在するため、メッセージの送信に失敗しました。"; "room_ongoing_conference_call" = "会議通話実施中。%@または%@で参加してください。"; "room_ongoing_conference_call_with_close" = "会議通話実施中。%@または%@で参加してください。%@。"; "room_ongoing_conference_call_close" = "閉じる"; @@ -213,61 +213,61 @@ "room_event_action_share" = "共有"; "room_event_action_permalink" = "メッセージへのリンクをコピー"; "room_event_action_view_source" = "ソースを表示"; -"room_event_action_report" = "発言を報告"; -"room_event_action_report_prompt_reason" = "この発言を報告する理由"; +"room_event_action_report" = "内容を報告"; +"room_event_action_report_prompt_reason" = "この内容を報告する理由"; "room_event_action_report_prompt_ignore_user" = "このユーザーからの全ての発言を非表示にしますか?"; "room_event_action_save" = "保存"; "room_event_action_resend" = "再送信"; "room_event_action_delete" = "削除"; -"room_event_action_cancel_send" = "送信中止"; -"room_event_action_cancel_download" = "ダウンロード中止"; -"room_event_action_view_encryption" = "暗号についての情報"; -"room_warning_about_encryption" = "エンドツーエンド暗号化はベータ版であり、信頼性が低い場合があります。\n\n発言を保護するためにはまだ信用すべきではありません。\n\n端末が参加するより前の発言履歴を復号化することはまだできません。\n\n暗号化された発言は、まだ暗号化を実装していないクライアントでは表示されません。"; -"room_event_failed_to_send" = "送信失敗"; +"room_event_action_cancel_send" = "送信をキャンセル"; +"room_event_action_cancel_download" = "ダウンロードをキャンセル"; +"room_event_action_view_encryption" = "暗号化についての情報"; +"room_warning_about_encryption" = "エンドツーエンド暗号化はベータ版のため、信頼性が低い場合があります。\n\nデータを保護するためにはまだ信用すべきではありません。\n\n端末が参加する以前の履歴を復号化することはまだできません。\n\n暗号化されたメッセージは、暗号化を実装していないクライアントでは表示できません。"; +"room_event_failed_to_send" = "送信に失敗しました"; // Unknown devices -"unknown_devices_alert_title" = "ルームに未知のセッションが存在します"; -"unknown_devices_alert" = "このルームには、確認されていない未知のセッションが含まれています。\nすなわち、セッションがをユーザー本人が所有しているという保証はありません。\n続ける前に各セッションの確認を行うことをおすすめしますが、確認することなく発言を再送信することができます。"; -"unknown_devices_send_anyway" = "無視して送信"; -"unknown_devices_call_anyway" = "無視して通話"; -"unknown_devices_answer_anyway" = "無視して応答"; -"unknown_devices_verify" = "確認…"; -"unknown_devices_title" = "未知のセッション"; +"unknown_devices_alert_title" = "ルームに不明なセッションが存在します"; +"unknown_devices_alert" = "このルームには、未認証のセッションが含まれています。\nセッションをユーザー本人が所有しているという保証はありません。\n続行する前に各セッションの認証を行うことを推奨しますが、認証せずメッセージを再送信することもできます。"; +"unknown_devices_send_anyway" = "送信"; +"unknown_devices_call_anyway" = "通話"; +"unknown_devices_answer_anyway" = "応答"; +"unknown_devices_verify" = "認証…"; +"unknown_devices_title" = "不明なセッション"; // Room Title "room_title_new_room" = "新しいルーム"; "room_title_multiple_active_members" = "全%@人中%@人が回線接続"; "room_title_one_active_member" = "全%@人中%@人が回線接続"; -"room_title_invite_members" = "招待中"; -"room_title_members" = "%@名のメンバー"; -"room_title_one_member" = "1名のメンバー"; +"room_title_invite_members" = "メンバーを招待"; +"room_title_members" = "%@人のメンバー"; +"room_title_one_member" = "1人のメンバー"; // Room Preview -"room_preview_invitation_format" = "あなたは%@さんに呼ばれてこのルームへ参加しました"; +"room_preview_invitation_format" = "%@があなたをこのルームに招待しました。"; "room_preview_subtitle" = "現在表示しているのはルームのプレビューです。メッセージの送信などは行えません。"; "room_preview_unlinked_email_warning" = "この招待は、このアカウントに関連付けられていない%@に送信されました。別のアカウントでログインするか、このメールアドレスを自分のアカウントに追加してください。"; -"room_preview_try_join_an_unknown_room" = "%@ に参加しますか?"; +"room_preview_try_join_an_unknown_room" = "%@にアクセスしようとしています。この会話に参加しますか?"; "room_preview_try_join_an_unknown_room_default" = "ルーム"; // Settings "settings_title" = "設定"; -"account_logout_all" = "全てのアカウントを回線切断"; +"account_logout_all" = "全てのアカウントをログアウト"; "settings_config_no_build_info" = "ビルド情報がありません"; -"settings_mark_all_as_read" = "全ての発言を既読にする"; +"settings_mark_all_as_read" = "全てのメッセージを既読にする"; "settings_report_bug" = "バグレポート"; -"settings_config_home_server" = "接続先サーバーは %@"; +"settings_config_home_server" = "ホームサーバーは %@ です"; "settings_config_identity_server" = "IDサーバー:%@"; "settings_config_user_id" = "%@でログインしています"; -"settings_user_settings" = "利用者設定"; +"settings_user_settings" = "ユーザー設定"; "settings_notifications_settings" = "通知設定"; "settings_calls_settings" = "通話"; "settings_user_interface" = "端末操作表示"; -"settings_ignored_users" = "無視する相手"; -"settings_contacts" = "端末の電話帳"; -"settings_advanced" = "拡張設定"; +"settings_ignored_users" = "無視しているユーザー"; +"settings_contacts" = "端末の連絡先"; +"settings_advanced" = "高度な設定"; "settings_other" = "その他"; "settings_labs" = "ラボ"; "settings_devices" = "セッション"; "settings_cryptography" = "暗号化"; "settings_sign_out" = "サインアウト"; "settings_sign_out_confirmation" = "よろしいですか?"; -"settings_sign_out_e2e_warn" = "エンドツーエンド暗号鍵が消去されます。この端末では、暗号化されたルームの過去の発言を読むことができなくなってしまいます。"; +"settings_sign_out_e2e_warn" = "エンドツーエンド暗号鍵が消去されます。この端末では、暗号化されたルームの過去のメッセージを読むことができなくなってしまいます。"; "settings_profile_picture" = "プロフィール画像"; "settings_display_name" = "表示名"; "settings_first_name" = "名"; @@ -281,11 +281,11 @@ "settings_phone_number" = "電話番号"; "settings_add_phone_number" = "電話番号を追加"; "settings_night_mode" = "夜間おやすみモード"; -"settings_fail_to_update_profile" = "自己紹介設定の更新に失敗しました"; +"settings_fail_to_update_profile" = "プロフィールの更新に失敗しました"; "settings_enable_push_notif" = "この端末での通知"; -"settings_show_decrypted_content" = "復号化された文章を表示"; -"settings_global_settings_info" = "あなたの%@ webクライアント上で、全体の通知設定が可能です"; -"settings_pin_rooms_with_missed_notif" = "逃した通知があるルームを固定"; +"settings_show_decrypted_content" = "復号化された内容を表示"; +"settings_global_settings_info" = "全体の通知設定は %@ webクライアントで行えます"; +"settings_pin_rooms_with_missed_notif" = "逃した通知があるルームをピン止め"; "settings_ui_language" = "言語"; "settings_ui_theme" = "外観"; "settings_ui_theme_auto" = "自動"; @@ -293,31 +293,31 @@ "settings_ui_theme_dark" = "ダーク"; "settings_ui_theme_picker_title" = "外観を選択"; "settings_ui_theme_picker_message" = "色反転設定の端末では、「自動」を使ってください"; -"settings_unignore_user" = "%@さんからのメッセージを見ますか?"; +"settings_unignore_user" = "%@さんからのメッセージを表示しますか?"; "settings_contacts_discover_matrix_users" = "電子メールと電話番号をユーザの検索に使用"; "settings_contacts_phonebook_country" = "電話帳の国番号"; "settings_labs_e2e_encryption" = "エンドツーエンド暗号化"; -"settings_labs_e2e_encryption_prompt_message" = "暗号化の設定を完了するためには再度ログインしてください。"; +"settings_labs_e2e_encryption_prompt_message" = "暗号化の設定を完了するには、再度ログインしてください。"; "settings_labs_matrix_apps" = "Matrixアプリ"; -"settings_labs_create_conference_with_jitsi" = "jitsiの会議通話を作成"; +"settings_labs_create_conference_with_jitsi" = "Jitsiで会議通話を作成"; "settings_version" = "バージョン %@"; -"settings_olm_version" = "Olmバージョン %@"; +"settings_olm_version" = "Olmのバージョン %@"; "settings_copyright" = "著作権"; "settings_term_conditions" = "利用規約"; -"settings_privacy_policy" = "個人情報保護方針"; -"settings_third_party_notices" = "外部ライブラリの規約"; +"settings_privacy_policy" = "プライバシーポリシー"; +"settings_third_party_notices" = "外部ライブラリーの規約"; "settings_send_crash_report" = "匿名利用状況と誤動作情報を送信"; -"settings_enable_rageshake" = "バグレポートのため端末を振る"; -"settings_clear_cache" = "一時保存を消去"; -"settings_change_password" = "パスワード変更"; -"settings_old_password" = "今までのパスワード"; +"settings_enable_rageshake" = "端末を振って不具合を報告"; +"settings_clear_cache" = "キャッシュを消去"; +"settings_change_password" = "パスワードを変更"; +"settings_old_password" = "以前のパスワード"; "settings_new_password" = "新しいパスワード"; -"settings_confirm_password" = "パスワード確認"; -"settings_fail_to_update_password" = "パスワードの更新に失敗しました"; -"settings_password_updated" = "あなたのパスワードは更新されました"; -"settings_crypto_device_name" = "セッション名: "; -"settings_crypto_device_id" = "\nセッションID: "; -"settings_crypto_device_key" = "\nセッションキー:\n"; +"settings_confirm_password" = "パスワードを確認"; +"settings_fail_to_update_password" = "Matrixのアカウントのパスワードの更新に失敗しました"; +"settings_password_updated" = "Matrixのアカウントのパスワードを更新しました"; +"settings_crypto_device_name" = "セッション名: "; +"settings_crypto_device_id" = "\nセッションID: "; +"settings_crypto_device_key" = "\nセッションキー:\n"; "settings_crypto_export" = "鍵をエクスポート"; "settings_crypto_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; // Room Details @@ -331,7 +331,7 @@ "room_details_favourite_tag" = "お気に入り"; "room_details_low_priority_tag" = "低優先度"; "room_details_mute_notifs" = "通知をミュート"; -"room_details_direct_chat" = "対話"; +"room_details_direct_chat" = "ダイレクトメッセージ"; "room_details_access_section" = "このルームにアクセスできる人は?"; "room_details_access_section_invited_only" = "招待された人のみ"; "room_details_access_section_anyone_apart_from_guest" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを除く)"; @@ -357,12 +357,12 @@ "room_details_advanced_section" = "高度な設定"; "room_details_advanced_room_id" = "ルームID:"; "room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告: 有効後にこれを無効にすることはできません!)"; -"room_details_advanced_e2e_encryption_enabled" = "このルームの発言は暗号化されています"; -"room_details_advanced_e2e_encryption_disabled" = "このルームの発言は暗号化されていません。"; +"room_details_advanced_e2e_encryption_enabled" = "このルームでは暗号化が有効になっています"; +"room_details_advanced_e2e_encryption_disabled" = "このルームでは暗号化が有効ではありません。"; "room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; "room_details_fail_to_update_avatar" = "ルームのアイコン画像の更新に失敗"; "room_details_fail_to_update_room_name" = "ルーム名の更新に失敗"; -"room_details_fail_to_update_topic" = "ルームの説明の更新に失敗"; +"room_details_fail_to_update_topic" = "トピックの更新に失敗"; "room_details_fail_to_update_room_guest_access" = "ゲストによるルームへのアクセスの設定更新に失敗"; "room_details_fail_to_update_room_join_rule" = "参加ルールの更新に失敗"; "room_details_fail_to_update_room_directory_visibility" = "ルーム一覧の可視設定の更新に失敗"; @@ -370,12 +370,12 @@ "room_details_fail_to_add_room_aliases" = "新しいルームアドレスの追加に失敗"; "room_details_fail_to_remove_room_aliases" = "ルームアドレスの削除に失敗"; "room_details_fail_to_update_room_canonical_alias" = "メインアドレスの更新に失敗"; -"room_details_fail_to_update_room_direct" = "ルームの対話タグの変更に失敗"; -"room_details_fail_to_enable_encryption" = "ルームの暗号化の開始に失敗"; +"room_details_fail_to_update_room_direct" = "このルームのダイレクトフラグのアップデートに失敗"; +"room_details_fail_to_enable_encryption" = "このルームの暗号化の有効化に失敗"; "room_details_save_changes_prompt" = "変更を保存しますか?"; -"room_details_set_main_address" = "メインアドレスを設定"; +"room_details_set_main_address" = "メインアドレスに設定"; "room_details_unset_main_address" = "メインアドレスの設定を解除"; -"room_details_copy_room_id" = "ルーム固有IDをコピー"; +"room_details_copy_room_id" = "ルームIDをコピー"; "room_details_copy_room_address" = "ルームのアドレスをコピー"; "room_details_copy_room_url" = "ルームのURLをコピー"; // Read Receipts @@ -411,7 +411,7 @@ "large_badge_value_k_format" = "%.1fK"; // room display name "room_displayname_room_invite" = "招待"; -"room_displayname_two_members" = "%@ と %@"; +"room_displayname_two_members" = "%@と%@"; "room_displayname_no_title" = "だれもいない部屋"; // Call "call_incoming_voice_prompt" = "%@ さんから通話の着信中"; @@ -453,37 +453,37 @@ "widget_integration_room_not_visible" = "ルーム %@ は見えません。"; // Share extension "share_extension_auth_prompt" = "メインのアプリにログインしてコンテンツを共有"; -"share_extension_failed_to_encrypt" = "送信に失敗しました。このルームの暗号設定をメインの端末で確認して下さい"; +"share_extension_failed_to_encrypt" = "送信に失敗しました。このルームの暗号設定をメインの端末で確認してください"; "room_details_advanced_e2e_encryption_prompt_message" = "End-to-end暗号化は実験的なものであり、信頼性が低い場合があります。\n\n発言を保護するためにはまだそれを信用すべきではありません。\n\n端末は、まだ参加する前の発言履歴を復号化することはできません。\n\n部屋の暗号化が今から有効になったら、もう無効にすることはできません。\n\n暗号化された発言は、まだ暗号化を実装していないアプリでは表示されません。"; "settings_enable_callkit" = "呼び出しの統合"; -"settings_pin_rooms_with_unread" = "未読のあるルームを固定"; +"settings_pin_rooms_with_unread" = "未読メッセージがあるルームをピン止め"; "title_groups" = "コミュニティー"; "room_recents_server_notice_section" = "システムアラート"; // Groups tab "group_invite_section" = "招待"; "group_section" = "コミュニティー"; -"room_message_reply_to_placeholder" = "返信を送る(暗号化されていない)…"; +"room_message_reply_to_placeholder" = "返信を送る(暗号化されていません)…"; "room_do_not_have_permission_to_post" = "このルームに投稿する権限がありません"; "encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送る…"; "room_message_reply_to_short_placeholder" = "返信を送る…"; -"room_event_action_view_decrypted_source" = "復号化されたソースを見る"; +"room_event_action_view_decrypted_source" = "復号化されたソースを表示"; "room_event_action_kick_prompt_reason" = "このユーザーを追放する理由"; -"room_action_send_photo_or_video" = "写真か動画を送る"; -"room_action_send_sticker" = "スタンプ送信"; +"room_action_send_photo_or_video" = "写真または動画を送信"; +"room_action_send_sticker" = "ステッカーを送信"; "room_replacement_information" = "このルームは置き換えられており、アクティブではありません。"; -"room_replacement_link" = "こちらから継続中の会話を確認する。"; +"room_replacement_link" = "こちらから継続中の会話を確認。"; "room_predecessor_information" = "このルームは別の会話の続きです。"; -"room_predecessor_link" = "以前のメッセージを見るには、ここをタップしてください。"; -"room_resource_limit_exceeded_message_contact_2_link" = "サービス管理者に連絡"; +"room_predecessor_link" = "以前のメッセージを表示するには、ここをタップしてください。"; +"room_resource_limit_exceeded_message_contact_2_link" = "サービス管理者に連絡してください"; "room_resource_limit_exceeded_message_contact_3" = " このサービスの使用を継続するには。"; -"room_resource_usage_limit_reached_message_1_default" = "このホームサーバーはリソース制限の1つを超えています "; -"room_resource_usage_limit_reached_message_1_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数制限を超えています "; +"room_resource_usage_limit_reached_message_1_default" = "このホームサーバーはリソースの上限に達しました "; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数の上限に達しました "; "room_resource_usage_limit_reached_message_2" = "一部のユーザーはログインできなくなります。"; "room_resource_usage_limit_reached_message_contact_3" = " この制限を増やすには。"; -"settings_deactivate_account" = "無効化したアカウント"; +"settings_deactivate_account" = "アカウントの無効化"; "settings_labs_room_members_lazy_loading" = "遅延ロードルームのメンバー"; "settings_labs_room_members_lazy_loading_error_message" = "あなたのホームサーバーはまだルームメンバーの遅延ロードをサポートしていません。 後で試してください。"; -"settings_deactivate_my_account" = "アカウントを無効にします"; +"settings_deactivate_my_account" = "アカウントを永久に無効にする"; "room_details_flair_section" = "コミュニティーの特色を表示"; "room_details_new_flair_placeholder" = "新しいコミュニティーIDを追加(例 +foo%@)"; "room_details_flair_invalid_id_prompt_title" = "無効な形式"; @@ -525,7 +525,7 @@ "e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; "e2e_room_key_request_message" = "未認証のセッション '%@' が暗号鍵を要求しています。"; "e2e_room_key_request_start_verification" = "認証を開始…"; -"e2e_room_key_request_share_without_verifying" = "認証せずに共有"; +"e2e_room_key_request_share_without_verifying" = "認証せず共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR "gdpr_consent_not_given_alert_message" = "%@ホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; @@ -556,8 +556,8 @@ "accessibility_checkbox_label" = "チェックボックス"; "auth_login_single_sign_on" = "サインイン"; "auth_softlogout_clear_data_sign_out" = "サインアウト"; -"room_message_unable_open_link_error_message" = "リンクを開くことができません。"; -"user_verification_session_details_verify_action_other_user" = "手動で確認"; +"room_message_unable_open_link_error_message" = "リンクを開けません。"; +"user_verification_session_details_verify_action_other_user" = "手動で認証"; "room_info_list_section_other" = "その他"; "room_info_list_several_members" = "%@人のメンバー"; @@ -576,21 +576,21 @@ "create_room_placeholder_topic" = "ルームのトピックを入力してください"; "create_room_section_header_topic" = "トピック(任意)"; "create_room_placeholder_name" = "名前"; -"create_room_section_header_name" = "ルーム名"; +"create_room_section_header_name" = "名前"; // MARK: - Create Room "create_room_title" = "新しいルーム"; "create_room_enable_encryption" = "暗号化を有効にする"; "room_details_room_name_for_dm" = "名前"; -"room_participants_security_information_room_encrypted_for_dm" = "ここで送受信されるメッセージはエンドツーエンド暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; -"room_participants_security_information_room_not_encrypted_for_dm" = "ここでのメッセージはエンドツーエンド暗号化されていません。"; +"room_participants_security_information_room_encrypted_for_dm" = "ここで送受信されるメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; +"room_participants_security_information_room_not_encrypted_for_dm" = "ここでのメッセージはエンドツーエンドで暗号化されていません。"; // Mark: - Room creation introduction cell "room_intro_cell_add_participants_action" = "参加者を追加"; -"room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; -"room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されていません。"; +"room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; +"room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されていません。"; "room_intro_cell_information_dm_sentence1_part3" = "とのダイレクトメッセージの始まりです。 "; "callbar_active_and_single_paused" = "1つのアクティブな通話(%@)· 1つの一時停止された通話"; @@ -680,43 +680,43 @@ "room_details_search" = "ルーム内検索"; "room_details_title_for_dm" = "詳細"; "identity_server_settings_alert_error_invalid_identity_server" = "%@は有効なIDサーバーではありません。"; -"identity_server_settings_alert_error_terms_not_accepted" = "IDサーバーとして設定するには%@の条件を受け入れる必要があります。"; -"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して切断"; -"identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー%@で個人データを共有しています。\n\n切断する前にメールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; +"identity_server_settings_alert_error_terms_not_accepted" = "IDサーバーに設定するには、%@の利用規約を承諾する必要があります。"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して接続解除"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー %@ で個人データを共有しています。\n\n接続を解除する前に、メールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; "identity_server_settings_alert_disconnect_button" = "接続を解除"; -"identity_server_settings_alert_disconnect" = "IDサーバー%@を接続解除しますか?"; -"identity_server_settings_alert_disconnect_title" = "IDサーバーを接続解除"; -"identity_server_settings_alert_change" = "IDサーバー%1$@を切断し、代わりに%2$@に接続しますか?"; +"identity_server_settings_alert_disconnect" = "IDサーバー %@ との接続を解除しますか?"; +"identity_server_settings_alert_disconnect_title" = "IDサーバーから接続を解除"; +"identity_server_settings_alert_change" = "IDサーバー %1$@ を切断し、代わりに %2$@ に接続しますか?"; "identity_server_settings_alert_change_title" = "IDサーバーを変更"; "identity_server_settings_alert_no_terms" = "選択したIDサーバーには利用規約がありません。そのサーバーの所有者を信頼できる場合にのみ続行してください。"; "identity_server_settings_alert_no_terms_title" = "IDサーバーには利用規約がありません"; "identity_server_settings_disconnect" = "接続を解除"; -"identity_server_settings_disconnect_info" = "IDサーバーとの接続を解除すると、他のユーザーから発見されなくなり、メールや電話で他のユーザーを招待することができるようになります。"; +"identity_server_settings_disconnect_info" = "IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。"; "identity_server_settings_change" = "変更"; "identity_server_settings_add" = "追加"; "identity_server_settings_place_holder" = "IDサーバーを入力"; -"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以上でIDサーバーを追加してください。"; -"identity_server_settings_description" = "あなたは%@を使って、あなたの知り合いを発見し、また向こうから発見できるようにしています。"; +"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を見つけたり、その連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; +"identity_server_settings_description" = "現在 %@ を使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。"; "security_settings_complete_security_alert_title" = "セキュリティーを確認"; "security_settings_crosssigning_complete_security" = "セキュリティーを確認"; "security_settings_crosssigning_bootstrap" = "設定"; "settings_devices_description" = "セッションの公開名は、あなたとやり取りする人々に対して表示されます"; -"settings_key_backup_delete_confirmation_prompt_title" = "バックアップの削除"; +"settings_key_backup_delete_confirmation_prompt_title" = "バックアップを削除"; "settings_key_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "settings_key_backup_info_algorithm" = "アルゴリズム:%@"; "settings_key_backup_info_version" = "鍵のバックアップのバージョン:%@"; "settings_key_backup_info_none" = "あなたの鍵は、このセッションからバックアップされていません。"; "settings_key_backup_info_checking" = "確認しています…"; -"settings_add_3pid_password_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; -"settings_add_3pid_invalid_password_message" = "無効な認証情報"; +"settings_add_3pid_password_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; +"settings_add_3pid_invalid_password_message" = "認証情報が正しくありません"; "settings_add_3pid_password_title_email" = "メールアドレスを追加"; -"settings_integrations_allow_description" = "インテグレーションマネージャー(%@)を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。\n\n設定データを受け取り、お客様に代わってウィジェットの変更、ルーム招待の送信、権限の設定を行うことができます。"; +"settings_integrations_allow_description" = "インテグレーションマネージャー %@ を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理。\n\n設定データを受け取り、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。"; "settings_integrations_allow_button" = "インテグレーションを管理"; -"settings_calls_stun_server_fallback_button" = "フォールバックコールアシストサーバーを許可"; +"settings_calls_stun_server_fallback_button" = "フォールバック用の通話アシストサーバーを許可"; "settings_key_backup" = "鍵のバックアップ"; "settings_integrations" = "インテグレーション"; "settings_discovery_settings" = "ディスカバリー"; -"room_multiple_typing_notification" = "%@とその他のユーザーが入力中です"; +"room_multiple_typing_notification" = "%@とその他のメンバー"; "external_link_confirmation_message" = "リンク %@ は別のサイトに移動します:%@\n\n続行してよろしいですか?"; "room_event_action_delete_confirmation_title" = "未送信のメッセージを削除"; "room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; @@ -747,11 +747,11 @@ "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると連絡先を追加できます。"; "people_empty_view_title" = "連絡先"; -"room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールで参加者を追加することができません。"; +"room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールでは参加者を追加できません。"; // Errors "error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようです。サインアウトしますか?"; -"social_login_button_title_sign_up" = "%@でサインアップ"; +"social_login_button_title_sign_up" = "%@で登録"; "social_login_button_title_sign_in" = "%@でサインイン"; "social_login_button_title_continue" = "%@で続行"; "social_login_list_title_sign_up" = "もしくは"; @@ -763,7 +763,7 @@ "auth_softlogout_clear_data_sign_out_msg" = "この端末に現在保存されている全てのデータを消去してよろしいですか?アカウントのデータやメッセージにアクセスするには、再びサインインしてください。"; "auth_softlogout_clear_data_sign_out_title" = "よろしいですか?"; "auth_softlogout_clear_data_button" = "全てのデータを消去"; -"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する、または別のアカウントにサインインする場合は、消去してください。"; +"auth_softlogout_clear_data_message_2" = "この端末の使用を終了する、または別のアカウントにサインインする場合は、個人データを消去してください。"; "auth_softlogout_clear_data_message_1" = "警告:あなたの個人データ(暗号鍵を含む)が、この端末にまだ保存されています。"; "callbar_return" = "折り返す"; "callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; @@ -771,7 +771,7 @@ "callbar_only_single_paused" = "一時停止した通話"; "store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者によるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; -"auth_softlogout_recover_encryption_keys" = "暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。"; +"auth_softlogout_recover_encryption_keys" = "暗号鍵はこの端末にのみ保存されています。保護されたメッセージをどの端末でも読むには、その暗号鍵が必要になります。サインインして暗号鍵を復元してください。"; "auth_softlogout_reason" = "あなたのホームサーバー(%1$@)の管理者が、あなたをアカウント %2$@ (%3$@)からサインアウトさせました。"; "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; @@ -780,13 +780,13 @@ "auth_reset_password_error_is_required" = "IDサーバーが設定されていません:Matrixのアカウントのパスワードを再設定するためにサーバーオプションに追加してください。"; "auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードを再設定するためにIDサーバーを追加してください。"; "auth_phone_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用する電話番号を追加することができません。"; -"auth_email_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用するメールアドレスを追加することができません。"; +"auth_email_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードを再設定する際に使用するメールアドレスを追加することができません。"; "auth_add_email_phone_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションでメールアドレスや電話番号を使用して知人に見つけてもらえるようにできます。"; "auth_add_phone_message_2" = "電話番号を設定します。後からオプションで知人に見つけてもらえるようにできます。"; "auth_add_email_message_2" = "アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。"; "less" = "たたむ"; "more" = "もっと"; -"switch" = "切り替え"; +"switch" = "切り替える"; "joined" = "参加済"; "skip" = "スキップ"; @@ -795,7 +795,7 @@ // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "このアプリは、ホームサーバーの認証機構をサポートしていません。"; -"manage_session_sign_out" = "セッションからサインアウト"; +"manage_session_sign_out" = "このセッションからサインアウト"; "manage_session_not_trusted" = "信頼されていません"; "manage_session_trusted" = "信頼済"; "manage_session_name" = "セッション名"; @@ -803,52 +803,52 @@ // Manage session "manage_session_title" = "セッションを管理"; -"security_settings_user_password_description" = "アカウントのパスワードを入力して本人確認を行ってください"; +"security_settings_user_password_description" = "Matrixのアカウントのパスワードを入力して本人確認を行ってください"; "security_settings_complete_security_alert_message" = "現在のセッションのセキュリティーを完了させる必要があります。"; -"security_settings_blacklist_unverified_devices_description" = "全てのセッションを認証して、信頼できるものとしてマークしメッセージを送信します。"; +"security_settings_blacklist_unverified_devices_description" = "全てのセッションを認証し、信頼済としてマークしてメッセージを送信します。"; "security_settings_blacklist_unverified_devices" = "信頼していないセッションにはメッセージを送信しない"; -"security_settings_advanced" = "上級者向け"; +"security_settings_advanced" = "高度な設定"; "security_settings_export_keys_manually" = "手動で鍵をエクスポート"; -"security_settings_cryptography" = "暗号技術"; +"security_settings_cryptography" = "暗号化"; "security_settings_crosssigning_reset" = "クロス署名をリセット"; -"security_settings_crosssigning_info_ok" = "クロス署名が有効です。"; +"security_settings_crosssigning_info_ok" = "クロス署名を利用できます。"; "security_settings_crosssigning_info_trusted" = "クロス署名が有効になっています。クロス署名に基づいて他のユーザーや自分の他のセッションを信頼することはできますが、このセッションにはクロス署名用の秘密鍵がないため、このセッションからクロス署名を行うことはできません。このセッションのセキュリティーを完了してください。"; "security_settings_crosssigning_info_exists" = "アカウントにはクロス署名IDがありますが、このセッションはまだ信頼されていません。このセッションのセキュリティーを完了してください。"; -"security_settings_crosssigning_info_not_bootstrapped" = "クロス署名がまだ行われていません。"; +"security_settings_crosssigning_info_not_bootstrapped" = "クロス署名がまだ設定されていません。"; "security_settings_crosssigning" = "クロス署名"; "security_settings_backup" = "メッセージのバックアップ"; "security_settings_secure_backup_delete" = "バックアップの削除"; "security_settings_secure_backup_synchronise" = "同期"; "security_settings_secure_backup_setup" = "設定"; -"security_settings_secure_backup_description" = "セッションにアクセスできなくなる場合に備えて、アカウントデータと暗号鍵をバックアップします。鍵は一意のセキュリティーキーで保護されます。"; +"security_settings_secure_backup_description" = "セッションにアクセスできなくなる場合に備えて、アカウントデータと暗号鍵をバックアップしましょう。鍵は一意のセキュリティーキーで保護されます。"; "security_settings_secure_backup" = "安全なバックアップ"; -"security_settings_crypto_sessions_description_2" = "見覚えのないログインがある場合は、Matrixアカウントのパスワードを変更し、バックアップをリセットしてください。"; +"security_settings_crypto_sessions_description_2" = "見覚えのないログインがある場合は、Matrixのアカウントのパスワードを変更し、バックアップをリセットしてください。"; "security_settings_crypto_sessions_loading" = "セッションを読み込んでいます…"; "security_settings_crypto_sessions" = "セッション"; // Security settings "security_settings_title" = "セキュリティー"; "settings_show_NSFW_public_rooms" = "NSFWパブリックルームを表示"; -"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以上でIDサーバーを追加してください。"; +"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。自分の連絡先を見つけたり、連絡先から見つけてもらったりするには、以上にIDサーバーを追加してください。"; "settings_identity_server_no_is" = "IDサーバーが設定されていません"; -"settings_identity_server_description" = "上記で設定したIDサーバーを使って、自分の知り合いを発見したり、発見されたりすることができます。"; +"settings_identity_server_description" = "上記で設定したIDサーバーを使うと、自分の連絡先を見つけたり、連絡先から見つけてもらったりすることができます。"; "settings_discovery_three_pid_details_enter_sms_code_action" = "SMSアクティベーションコードを入力"; "settings_discovery_three_pid_details_cancel_email_validation_action" = "メールの認証をキャンセル"; -"settings_discovery_three_pid_details_revoke_action" = "取り消し"; +"settings_discovery_three_pid_details_revoke_action" = "取り消す"; "settings_discovery_three_pid_details_share_action" = "共有"; "settings_discovery_three_pid_details_title_email" = "メールアドレスを管理"; "settings_discovery_three_pid_details_title_phone_number" = "電話番号を管理"; -"settings_discovery_three_pid_details_information_phone_number" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用できる電話番号の設定を管理します。アカウントへ電話番号の追加や削除ができます。"; -"settings_discovery_three_pid_details_information_email" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用できるメールアドレスの設定を管理します。アカウントへメールアドレスの追加や削除ができます。"; +"settings_discovery_three_pid_details_information_phone_number" = "他のユーザーがあなたを発見したり、ルームに招待したりする際に使用できる電話番号の設定を管理します。アカウント画面で電話番号を追加、削除できます。"; +"settings_discovery_three_pid_details_information_email" = "他のユーザーがあなたを発見したり、ルームに招待したりする際に使用できるメールアドレスの設定を管理します。アカウント画面でメールアドレスを追加、削除できます。"; "settings_discovery_error_message" = "エラーが発生しました。再試行してください。"; "settings_discovery_three_pids_management_information_part3" = "。"; "settings_discovery_three_pids_management_information_part2" = "ユーザー設定"; -"settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストにメールアドレスや電話番号を追加したり、削除したりすることができます。 "; -"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー(%@)の利用規約への同意が必要です。"; -"settings_discovery_no_identity_server" = "現在、IDサーバーを使用していません。あなたの知っている連絡先から発見されるようにするには、IDサーバーを追加してください。"; -"settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを失うことがあります。"; +"settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストに、メールアドレスや電話番号を追加したり、削除したりすることができます。 "; +"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー %@ の利用規約への同意が必要です。"; +"settings_discovery_no_identity_server" = "現在IDサーバーを使用していません。あなたの知っている連絡先から見つけてもらえるようにするには、IDサーバーを追加してください。"; +"settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを読み取れなくなってしまいます。"; "settings_key_backup_button_connect" = "このセッションを鍵のバックアップに接続"; -"settings_key_backup_button_delete" = "バックアップの削除"; +"settings_key_backup_button_delete" = "バックアップを削除"; "settings_key_backup_button_restore" = "バックアップから復元"; "settings_key_backup_button_create" = "鍵のバックアップを使用開始"; "settings_key_backup_info_trust_signature_invalid_device_unverified" = "バックアップには%@による無効な署名があります"; @@ -856,22 +856,22 @@ "settings_key_backup_info_trust_signature_valid_device_unverified" = "バックアップには%@による署名があります"; "settings_key_backup_info_trust_signature_valid_device_verified" = "バックアップには%@による有効な署名があります"; "settings_key_backup_info_trust_signature_valid" = "バックアップにはこのセッションによる有効な署名があります"; -"settings_key_backup_info_trust_signature_unknown" = "バックアップにはID:%@によるセッションの署名があります"; -"settings_key_backup_info_progress_done" = "全ての鍵がバックアップされています"; +"settings_key_backup_info_trust_signature_unknown" = "バックアップには、ID:%@によるセッションの署名があります"; +"settings_key_backup_info_progress_done" = "全ての鍵をバックアップしました"; "settings_key_backup_info_progress" = "%@の鍵をバックアップしています…"; -"settings_key_backup_info_not_valid" = "このセッションでは鍵をバックアップしていませんが、復元に使用したり、今後鍵を追加したりできるバックアップを持っています。"; +"settings_key_backup_info_not_valid" = "このセッションでは鍵をバックアップしていませんが、復元に使用したり、鍵を今後追加したりできるバックアップを持っています。"; "settings_key_backup_info_signout_warning" = "鍵を失くさないよう、サインアウトする前にバックアップしてください。"; "settings_key_backup_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。"; "settings_labs_message_reaction" = "絵文字でメッセージに反応"; "settings_security" = "セキュリティー"; -"settings_three_pids_management_information_part3" = "。"; +"settings_three_pids_management_information_part3" = "で設定しましょう。"; "settings_three_pids_management_information_part2" = "ディスカバリー"; -"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrix――オープンな分散型通信の標準規格――で動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有: データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション: Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全: 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の真正性を確認するためのクロス署名を行います。\n\n包括的なコミュニケーション: メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても: 全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; +"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールできます。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化と、クロス署名による認証で、あなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrix――オープンな分散型通信の標準規格――で動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有:データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション:Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全:本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の端末を認証するためのクロス署名を行います。\n\n包括的なコミュニケーション:メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても:全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; -"user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; -"user_verification_session_details_information_untrusted_current_user" = "このセッションを認証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; -"user_verification_session_details_information_trusted_other_user_part2" = " 検証済み:"; -"user_verification_session_details_information_trusted_other_user_part1" = "このセッションは安全なものとして信頼されています。なぜなら "; +"user_verification_session_details_information_untrusted_other_user" = " が新しいセッションを使ってサインインしました:"; +"user_verification_session_details_information_untrusted_current_user" = "このセッションを認証して信頼済としてマークし、暗号化されたメッセージへのアクセスを許可。"; +"user_verification_session_details_information_trusted_other_user_part2" = " が検証しました:"; +"user_verification_session_details_information_trusted_other_user_part1" = "このセッションは安全なものとして信頼されています。 "; "user_verification_session_details_information_trusted_current_user" = "このセッションは、認証されたため安全なものとして信頼されています。"; "user_verification_session_details_untrusted_title" = "信頼されていません"; @@ -880,7 +880,7 @@ "user_verification_session_details_trusted_title" = "信頼済"; "user_verification_sessions_list_session_untrusted" = "信頼されていません"; "user_verification_sessions_list_session_trusted" = "信頼済"; -"user_verification_sessions_list_table_title" = "セッション一覧"; +"user_verification_sessions_list_table_title" = "セッション"; "user_verification_sessions_list_information" = "このルームにいるこのユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; "user_verification_sessions_list_user_trust_level_unknown_title" = "不明"; "user_verification_sessions_list_user_trust_level_warning_title" = "警告"; @@ -888,9 +888,9 @@ // Sessions list "user_verification_sessions_list_user_trust_level_trusted_title" = "信頼済"; -"user_verification_start_additional_information" = "安心してご利用いただくために、直接お会いするか、別の方法でご連絡ください。"; -"user_verification_start_waiting_partner" = "%@を待っています…"; -"user_verification_start_information_part2" = " 両方の端末でワンタイムコードを確認します。"; +"user_verification_start_additional_information" = "セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。"; +"user_verification_start_waiting_partner" = "%@を待機しています…"; +"user_verification_start_information_part2" = " 両方の端末でワンタイムコードを確認し、認証してください。"; "user_verification_start_information_part1" = "セキュリティーを高めるために "; // MARK: - User verification @@ -903,22 +903,22 @@ // Scanned "key_verification_scan_confirmation_scanned_title" = "まもなくです!"; -"key_verification_scan_confirmation_scanning_device_waiting_other" = "他の端末を待っています…"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "他の端末を待機しています…"; // MARK: Scan confirmation // Scanning "key_verification_scan_confirmation_scanning_title" = "もう少しです。確認を待っています…"; -"key_verification_scan_confirmation_scanning_user_waiting_other" = "%@を待っています…"; -"key_verification_verify_qr_code_scan_other_code_success_message" = "QRコードの認証に成功しました。"; -"key_verification_verify_qr_code_scan_other_code_success_title" = "コードが有効になりました!"; -"key_verification_verify_qr_code_other_scan_my_code_title" = "相手がQRコードを読み取ってくれましたか?"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "%@を待機しています…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QRコードを正常に検証しました。"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "コードを検証しました!"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "相手がQRコードを正常に読み取りましたか?"; "key_verification_verify_qr_code_start_emoji_action" = "絵文字による認証"; "key_verification_verify_qr_code_cannot_scan_action" = "スキャンできませんか?"; "key_verification_verify_qr_code_scan_code_action" = "コードをスキャン"; "key_verification_verify_qr_code_emoji_information" = "絵文字の並びを比較して認証。"; -"key_verification_verify_qr_code_information_other_device" = "以下のコードをスキャンして確認してください:"; -"key_verification_verify_qr_code_information" = "コードをスキャンして、お互いをしっかりと確認します。"; +"key_verification_verify_qr_code_information_other_device" = "以下のコードをスキャンして認証してください:"; +"key_verification_verify_qr_code_information" = "コードをスキャンして、お互いを安全に認証しましょう。"; // MARK: QR code @@ -987,7 +987,7 @@ "device_verification_emoji_hammer" = "ハンマー"; "device_verification_emoji_key" = "鍵"; "device_verification_emoji_lock" = "錠"; -"settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理します。誰があなたのことを発見できるかを管理する "; +"settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理。あなたを見つけられる人を "; "settings_identity_server_settings" = "IDサーバー"; "external_link_confirmation_title" = "このリンクを再確認してください"; "media_type_accessibility_sticker" = "ステッカー"; @@ -997,19 +997,19 @@ "media_type_accessibility_audio" = "音声"; "media_type_accessibility_image" = "画像"; "room_open_dialpad" = "ダイヤルパッド"; -"room_place_voice_call" = "ビデオ通話"; -"room_accessibility_hangup" = "通話を切る"; -"room_event_action_delete_confirmation_message" = "この未送信メッセージを削除してよろしいですか?"; +"room_place_voice_call" = "音声通話"; +"room_accessibility_hangup" = "電話を切る"; +"room_event_action_delete_confirmation_message" = "この未送信のメッセージを削除してよろしいですか?"; "room_accessibility_video_call" = "ビデオ通話"; "room_accessibility_call" = "通話"; -"room_accessibility_integrations" = "インテグレーション"; +"room_accessibility_integrations" = "インテグレーション(統合)"; "room_accessibility_search" = "検索"; "room_accessibility_upload" = "アップロード"; -"room_message_edits_history_title" = "メッセージを編集"; +"room_message_edits_history_title" = "メッセージの編集履歴"; "room_action_reply" = "返信"; "room_action_send_file" = "ファイルを送信"; -"room_action_camera" = "写真やビデオの撮影"; -"room_event_action_reaction_history" = "反応の履歴"; +"room_action_camera" = "写真または動画を撮影"; +"room_event_action_reaction_history" = "リアクションの履歴"; "room_event_action_reaction_show_less" = "表示しない"; "room_event_action_reaction_show_all" = "全てを見る"; "room_event_action_edit" = "編集"; @@ -1030,7 +1030,7 @@ "device_verification_self_verify_alert_title" = "ログインしましたか?"; "room_recents_suggested_rooms_section" = "おすすめのルーム"; "settings_show_url_previews_description" = "プレビューは暗号化されていないルームでのみ表示されます。"; -"settings_show_url_previews" = "ウェブサイトプレビューを表示"; +"settings_show_url_previews" = "ウェブサイトのプレビューを表示"; "biometrics_setup_enable_button_title_x" = "%@を有効にする"; "biometrics_setup_title_x" = "%@を有効にする"; "biometrics_settings_enable_x" = "%@を有効にする"; @@ -1044,7 +1044,7 @@ "pin_protection_settings_section_header" = "PINコード"; "settings_mentions_and_keywords_encryption_notice" = "携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。"; "settings_new_keyword" = "キーワードを追加"; -"settings_your_keywords" = "以下でキーワードを指定できます"; +"settings_your_keywords" = "キーワード"; "settings_mentions_and_keywords" = "メンションとキーワード"; "settings_messages_containing_keywords" = "キーワード"; "settings_messages_containing_at_room" = "@room"; @@ -1126,7 +1126,7 @@ "home_empty_view_title" = "%@へようこそ、\n%@"; "threads_empty_tip" = "ヒント:メッセージをタップして「スレッド」を選択し、開始。"; -"threads_empty_info_all" = "スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。"; +"threads_empty_info_all" = "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。"; "threads_empty_title" = "スレッド機能を使って、会話をまとめましょう"; "secure_key_backup_setup_intro_use_security_key_title" = "セキュリティーキーを使用"; @@ -1186,8 +1186,8 @@ "login_tablet_device" = "タブレット"; "login_desktop_device" = "デスクトップ"; "login_error_resource_limit_exceeded_title" = "リソース制限を超えました"; -"login_error_resource_limit_exceeded_message_default" = "このホームサーバーは、リソース制限の1つを超えています。"; -"login_error_resource_limit_exceeded_message_monthly_active_user" = "このホームサーバーは、月間アクティブユーザー制限を超えています。"; +"login_error_resource_limit_exceeded_message_default" = "このホームサーバーはリソースの上限に達しました。"; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数の上限に達しました 。"; "login_error_resource_limit_exceeded_message_contact" = "\n\nこのサービスを続行するには、サービス管理者に連絡してください。"; "login_error_resource_limit_exceeded_contact_button" = "管理者に連絡"; "abort" = "中断"; @@ -1278,20 +1278,20 @@ "room_event_encryption_info_event_unencrypted" = "暗号化されていません"; "room_event_encryption_info_event_none" = "なし"; "room_event_encryption_info_device" = "\n送信者セッション情報\n"; -"room_event_encryption_info_device_unknown" = "未知のセッション\n"; +"room_event_encryption_info_device_unknown" = "不明なセッション\n"; "room_event_encryption_info_device_name" = "名前\n"; "room_event_encryption_info_device_id" = "ID\n"; "room_event_encryption_info_device_verification" = "認証\n"; "room_event_encryption_info_device_fingerprint" = "Ed25519 fingerprint\n"; "room_event_encryption_info_device_verified" = "認証済"; "room_event_encryption_info_device_not_verified" = "認証されていません"; -"room_event_encryption_info_device_blocked" = "ブラックリストに載せた"; -"room_event_encryption_info_verify" = "認証しています…"; -"room_event_encryption_info_unverify" = "未認証"; -"room_event_encryption_info_block" = "ブラックリスト"; -"room_event_encryption_info_unblock" = "ブラックでないリスト"; +"room_event_encryption_info_device_blocked" = "ブラックリストに追加済"; +"room_event_encryption_info_verify" = "認証…"; +"room_event_encryption_info_unverify" = "認証を取り消す"; +"room_event_encryption_info_block" = "ブラックリストに追加"; +"room_event_encryption_info_unblock" = "ブラックリストから除外"; "room_event_encryption_verify_title" = "セッションを認証\n\n"; -"room_event_encryption_verify_message" = "このセッションが信頼できることを確認するには、他の方法(対面や電話など)で所有者に連絡し、セッションのユーザー設定で表示される鍵が以下の鍵と一致するかどうかを訪ねてください。\n\nセッション名: %@\nセッションID: %@\nセッションキー: %@\n\n一致する場合は、下の確認ボタンを押します。 それ以外の人がこのセッションを傍受している場合は、代わりにブラックリストボタンを押してください。\n\n将来この認証プロセスはより洗練されたものになります。"; +"room_event_encryption_verify_message" = "このセッションが信頼できることを確認するには、他の方法(対面や電話など)で所有者に連絡し、セッションのユーザー設定で表示される鍵が以下の鍵と一致するかどうかを訪ねてください。\n\nセッション名:%@\nセッションID:%@\nセッションキー:%@\n\n一致する場合は、下の確認ボタンを押します。 それ以外の人がこのセッションを傍受している場合は、代わりにブラックリストボタンを押してください。\n\n将来この認証プロセスはより洗練されたものになります。"; "room_event_encryption_verify_ok" = "認証"; // Account "account_save_changes" = "変更を保存"; @@ -1302,7 +1302,7 @@ "account_email_validation_error" = "メールアドレスを認証できません。メールを確認して、記載されているリンクをクリックしてください。その後、「続行する」をクリックしてください"; "account_msisdn_validation_title" = "認証の保留中"; "account_msisdn_validation_message" = "SMSで認証番号を送りました。以下にその番号を入力してください。"; -"account_msisdn_validation_error" = "電話番号を確認できません。"; +"account_msisdn_validation_error" = "電話番号を認証できません。"; "account_error_display_name_change_failed" = "表示名の変更に失敗しました"; "account_error_picture_change_failed" = "画像の変更に失敗しました"; "account_error_matrix_session_is_not_opened" = "Matrixセッションが開かれていません"; @@ -1395,7 +1395,7 @@ "microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; "local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはそのアクセス権限がありません"; "local_contacts_access_discovery_warning_title" = "ユーザーの探索"; -"local_contacts_access_discovery_warning" = "%@は、ユーザーを検索するためにあなたの連絡先から電子メールと電話番号をアップロードしたい"; +"local_contacts_access_discovery_warning" = "Matrixを既に使用している連絡先を見つけるため、%@は電話帳にあるメールアドレスと電話番号を、あなたが選択したMatrixのIDサーバーに送信することができます。サポートしている場合、個人データは送信前にハッシュ化されます。詳細はIDサーバーのプライバシーポリシーを確認してください。"; // Country picker "country_picker_title" = "国を選択"; // Language picker @@ -1495,8 +1495,8 @@ "ssl_trust" = "信頼"; "ssl_logout_account" = "ログアウト"; "ssl_remain_offline" = "無視"; -"ssl_fingerprint_hash" = "指紋 (%@):"; -"ssl_could_not_verify" = "リモートサーバーのIDを確認できませんでした。"; +"ssl_fingerprint_hash" = "フィンガープリント(%@):"; +"ssl_could_not_verify" = "リモートサーバーのIDを認証できませんでした。"; "ssl_cert_not_trust" = "これは、誰かがあなたのトラフィックを悪意を持って傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味します。"; "ssl_cert_new_account_expl" = "サーバー管理者がこれが予期されると述べた場合は、以下の指紋が提供された指紋と一致することを確認してください。"; "ssl_unexpected_existing_expl" = "証明書は、お使いの携帯電話にて信頼されたものから変更されました。 これは非常に珍しいことです。 この新しい証明書に同意しないことをお勧めします。"; @@ -1598,9 +1598,9 @@ "notice_room_created_for_dm" = "%@が参加しました。"; "onboarding_use_case_existing_server_button" = "サーバーに接続"; "callbar_only_single_active_group" = "タップしてグループ通話に参加 (%@)"; -"settings_confirm_media_size" = "送信時のサイズ確認"; -"settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面が表示されます。"; -"settings_contacts_enable_sync_description" = "IDサーバーを使用して連絡先を探すと同時に、連絡先があなたを探せるようにします。"; +"settings_confirm_media_size" = "送信時にサイズを確認"; +"settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面を表示します。"; +"settings_contacts_enable_sync_description" = "IDサーバーを使用すると、連絡先を探したり、相手があなたを探したりできるようになります。"; "home_syncing" = "同期しています"; "search_filter_placeholder" = "絞り込む"; @@ -1625,7 +1625,7 @@ "onboarding_splash_page_4_message" = "Elementは職場利用にも最適です。世界で最も安全な組織によって信頼されています。"; "onboarding_splash_page_4_title_no_pun" = "あなたのチームのメッセージングに。"; "onboarding_splash_page_3_message" = "エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。"; -"onboarding_splash_page_3_title" = "安全なメッセージ。"; +"onboarding_splash_page_3_title" = "安全なメッセージのやりとり。"; "onboarding_splash_page_2_message" = "会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。"; "onboarding_splash_page_2_title" = "主導権を握るのは、あなたです。"; "onboarding_splash_page_1_message" = "オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。"; @@ -1668,7 +1668,7 @@ // MARK: Sign out warning "sign_out_existing_key_backup_alert_title" = "サインアウトしてよろしいですか?"; -"find_your_contacts_message" = "%@ であなたの連絡先を表示し、知人とのチャットを素早く始めます。"; +"find_your_contacts_message" = "%@であなたの連絡先を表示し、知人との会話をすぐ始められるようにしましょう。"; "find_your_contacts_footer" = "この設定はいつでも無効にできます。"; "find_your_contacts_button_title" = "連絡先を検索"; "find_your_contacts_title" = "連絡先をリストアップ"; @@ -1716,7 +1716,7 @@ // Room suggestion Settings "room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; "room_details_promote_room_suggest_title" = "スペースメンバーへのおすすめ"; -"settings_default" = "デフォルトの通知"; +"settings_default" = "既定の通知"; "pin_protection_reset_alert_action_reset" = "リセット"; "authentication_recaptcha_title" = "あなたは人間ですか?"; "authentication_verify_msisdn_waiting_button" = "コードを再送信"; @@ -1735,7 +1735,7 @@ "password_validation_error_min_length" = "%d文字以上"; // MARK: Password Validation -"password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; +"password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; "space_selector_empty_view_title" = "まだスペースがありません"; "all_chats_empty_list_placeholder_title" = "未読はありません"; "all_chats_empty_unreads_placeholder_message" = "未読のメッセージがある場合は、ここに表示されます。"; @@ -1840,7 +1840,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "セッション名は連絡先にも表示されます。%@"; "manage_session_name_hint" = "セッション名を設定すると、端末をより簡単に認識できるようになります。"; -"security_settings_coming_soon" = "申し訳ありません。このアクションは%@ iOSではまだ利用できません。他のMatrixクライアントを使って設定してください。将来的には%@ iOSでも実装される予定です。"; +"security_settings_coming_soon" = "申し訳ありません。このアクションは%@ iOSではまだ利用できません。他のMatrixのクライアントを使って設定してください。将来的には%@ iOSでも実装される予定です。"; "security_settings_secure_backup_reset" = "再設定"; "security_settings_secure_backup_info_checking" = "確認しています…"; "settings_presence_offline_mode_description" = "有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。"; @@ -1856,10 +1856,10 @@ "settings_labs_enable_threads" = "メッセージのスレッド機能"; "settings_labs_enabled_polls" = "アンケート"; "settings_ui_show_redactions_in_room_history" = "削除されたメッセージに関する通知を表示"; -"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は%@を許可(IPアドレスは通話中に共有されます)。"; -"settings_callkit_info" = "画面がロックされているときに着信がありました。%@の着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; +"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は %@ を許可(IPアドレスが通話中に共有されます)。"; +"settings_callkit_info" = "ロック画面に着信を表示。%@の着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; "settings_notifications_disabled_alert_title" = "通知が無効です"; -"threads_discourage_information_1" = "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。 "; +"threads_discourage_information_1" = "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。 "; "threads_beta_cancel" = "後で"; "threads_beta_enable" = "試してみる"; "threads_beta_information_link" = "詳細を表示"; @@ -1878,7 +1878,7 @@ "directory_search_results_more_than" = ">%2$@の検索結果%1$tu件"; /* The placeholder %1$tu will be replaced with a number and %2$@ with the user's search terms. */ "directory_search_results" = "%2$@の検索結果%1$tu件"; -"room_recents_unknown_room_error_message" = "このルームが発見できません。存在することを確認してください"; +"room_recents_unknown_room_error_message" = "このルームを発見できません。存在することを確認してください"; "room_creation_dm_error" = "ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。"; "password_policy_pwd_in_dict_error" = "パスワードが辞書で見つかりました。許可できません。"; @@ -1895,10 +1895,10 @@ "authentication_qr_login_scan_title" = "QRコードをスキャン"; "authentication_qr_login_display_subtitle" = "サインアウトした端末で以下のQRコードをスキャンしてください。"; "authentication_qr_login_start_title" = "QRコードをスキャン"; -"authentication_terms_policy_url_error" = "選択した運営方針が見つかりませんでした。後でもう一度やり直す後でもう一度やり直してください。"; +"authentication_terms_policy_url_error" = "選択した運営方針が見つかりませんでした。後でもう一度やり直してください。"; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "%sの利用規約と運営方針を確認してください"; -"authentication_verify_msisdn_invalid_phone_number" = "無効な電話番号"; +"authentication_verify_msisdn_invalid_phone_number" = "電話番号が不正です"; /* The placeholder will show the phone number that was entered. */ "authentication_verify_msisdn_waiting_message" = "コードが%@に送信されました"; "authentication_verify_msisdn_waiting_title" = "電話番号を認証してください"; @@ -1938,7 +1938,7 @@ // MARK: Authentication "authentication_registration_title" = "アカウントを作成"; "onboarding_celebration_button" = "進みましょう"; -"onboarding_celebration_message" = "設定画面からいつでもプロフィールを更新できます"; +"onboarding_celebration_message" = "プロフィールは設定画面からいつでも更新できます"; "onboarding_celebration_title" = "問題ありません!"; "onboarding_avatar_accessibility_label" = "プロフィール画像"; "onboarding_avatar_title" = "プロフィール画像を追加"; @@ -1949,7 +1949,7 @@ "onboarding_personalization_skip" = "このステップをスキップ"; "onboarding_personalization_save" = "保存して続行"; "onboarding_congratulations_home_button" = "ホームに移動"; -"onboarding_congratulations_personalize_button" = "プロフィールを変更"; +"onboarding_congratulations_personalize_button" = "プロフィールを設定"; /* The placeholder string contains the user's matrix ID */ "onboarding_congratulations_message" = "あなたのアカウント %@ が作成されました"; "onboarding_congratulations_title" = "おめでとうございます!"; @@ -2099,7 +2099,7 @@ "user_other_session_unverified_sessions_header_subtitle" = "セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。"; "user_other_session_current_session_details" = "現在のセッション"; "user_other_session_verified_sessions_header_subtitle" = "セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。"; -"user_other_session_filter" = "絞り込み"; +"user_other_session_filter" = "絞り込む"; "user_other_session_filter_menu_all" = "全てのセッション"; "user_other_session_filter_menu_verified" = "認証済"; "user_other_session_filter_menu_unverified" = "未認証"; @@ -2291,7 +2291,7 @@ // User -"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; +"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者に解読することはできません。"; "key_verification_verified_new_session_title" = "新しいセッションを認証しました!"; "device_verification_verified_got_it_button" = "了解"; @@ -2338,7 +2338,7 @@ // MARK: Start "device_verification_start_title" = "短い文字列を比較して認証"; -"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済としてマークされ、自分のセッションも相手に信頼済としてマークされます。"; +"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済としてマークされ、あなたのセッションも相手に信頼済としてマークされます。"; "device_verification_incoming_description_1" = "このセッションを認証して、信頼済としてマークします。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; // MARK: Incoming @@ -2509,7 +2509,7 @@ "room_no_privileges_to_create_group_call" = "通話を開始するには管理者あるいはモデレーターである必要があります。"; "contacts_address_book_permission_denied_alert_message" = "連絡先を有効にするには、端末の設定画面を開いてください。"; "contacts_address_book_permission_denied_alert_title" = "連絡先が無効です"; -"password_policy_weak_pwd_error" = "パスワードが弱すぎます。8文字以上で、大文字、小文字、数字。特殊文字をそれぞれ1つずつ含めてください。"; +"password_policy_weak_pwd_error" = "パスワードが弱すぎます。8文字以上で、大文字、小文字、数字、特殊文字をそれぞれ1つずつ含めてください。"; "authentication_qr_login_loading_signed_in" = "他の端末でサインインしました。"; "authentication_qr_login_display_step1" = "他の端末でElementを開いてください"; "authentication_qr_login_start_display_qr" = "この端末でQRコードを表示"; @@ -2517,7 +2517,7 @@ "authentication_qr_login_start_step1" = "他の端末でElementを開いてください"; "authentication_qr_login_start_subtitle" = "この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください:"; "authentication_choose_password_not_verified_title" = "電子メールは認証されていません"; -"authentication_server_selection_generic_error" = "このURLでサーバーを発見できません。URLが正しいことを確認してください。"; +"authentication_server_selection_generic_error" = "このURLでサーバーを発見できません。URLを確認してください。"; "authentication_server_selection_register_title" = "あなたのホームサーバーを選択してください"; "authentication_server_selection_login_message" = "ホームサーバーのアドレスを入力してください"; "authentication_server_selection_login_title" = "ホームサーバーに接続"; @@ -2533,7 +2533,7 @@ "settings_notifications_disabled_alert_message" = "通知を有効にするには、端末の設定画面を開いてください。"; "room_accessibility_record_voice_message_hint" = "2回続けてタップし長押しすると録音。"; "room_preview_decline_invitation_options" = "招待を拒否するか、このユーザーを無視しますか?"; -"threads_beta_information" = "スレッド機能を使って、会話をまとめましょう。\n\nスレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 "; +"threads_beta_information" = "スレッド機能を使って、会話をまとめましょう。\n\nスレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 "; "threads_notice_information" = "実験期間中に作成されたスレッドは通常の返信として表示されます

スレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。"; "threads_empty_info_my" = "既存のスレッドに返信するか、メッセージをタップし「スレッド」から新しいスレッドを開始。"; "room_accessibility_thread_more" = "その他"; @@ -2545,8 +2545,8 @@ "authentication_qr_login_failure_request_timed_out" = "時間内にリンクが完了しませんでした。"; "authentication_qr_login_failure_title" = "リンクに失敗しました"; "authentication_qr_login_start_step2" = "設定から「セキュリティーとプライバシー」を開いてください"; -"authentication_qr_login_confirm_alert" = "このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに制限なくアクセスできるようになります。"; -"authentication_qr_login_scan_subtitle" = "QRコードを以下の四角に合わせてください"; +"authentication_qr_login_confirm_alert" = "このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに無制限にアクセスできるようになります。"; +"authentication_qr_login_scan_subtitle" = "QRコードを以下の四角形に合わせてください"; "authentication_qr_login_display_step2" = "「QRコードでサインイン」を選択してください"; "authentication_qr_login_start_step4" = "「この端末でQRコードを表示」を選択してください"; "authentication_qr_login_start_step3" = "「端末をリンク」を選択してください"; @@ -2588,7 +2588,7 @@ "call_holded" = "通話を保留しました"; "call_more_actions_unhold" = "再開"; "user_session_rename_session_description" = "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。\n\n相手はあなたとやり取りしていることを確かめることができますが、あなたがここに入力するセッション名は相手に対して表示されます。"; -"user_session_unverified_session_description" = "未認証のセッションは、アカウント情報でログインされていますが、クロス認証されていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。"; +"user_session_unverified_session_description" = "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。"; "user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。"; "user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。"; "launch_loading_server_syncing" = "サーバーと同期しています"; @@ -2624,7 +2624,7 @@ "analytics_prompt_title" = "%@の改善を手伝う"; "event_formatter_call_active_video" = "実行中のビデオ通話"; "event_formatter_call_active_voice" = "実行中の音声通話"; -"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@ 試行)"; +"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)"; "create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; "create_room_section_footer_type_public" = "スペース名にあるだけでなく、招待された連絡先のみが検索し、参加できます。"; "searchable_directory_x_network" = "%@ネットワーク"; @@ -2702,10 +2702,10 @@ "secrets_setup_recovery_passphrase_summary_information" = "セキュリティーフレーズを記憶。セキュリティーフレーズを使うと、暗号化したメッセージやデータのロックを解除することができます。"; "secrets_setup_recovery_key_storage_alert_message" = "✓ 印刷して安全な場所で保管\n✓ USBキーやバックアップ用ドライブに保存\n✓ 個人用のクラウドストレージにコピー"; "secrets_setup_recovery_key_information" = "セキュリティーキーは安全な場所で保管してください。セキュリティーキーを使うと、暗号化したメッセージやデータのロックを解除することができます。"; -"secrets_recovery_with_key_invalid_recovery_key_message" = "正しいセキュリティーキーを入力したことを確かめてください。"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "正しいセキュリティーキーを入力したことを確認してください。"; "secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "続行するにはセキュリティーフレーズを入力してください。"; "secrets_recovery_with_key_information_verify_device" = "セキュリティーキーを使用して、この端末を認証してください。"; -"secrets_recovery_with_passphrase_invalid_passphrase_message" = "正しいセキュリティーフレーズを入力したことを確かめてください。"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "正しいセキュリティーフレーズを入力したことを確認してください。"; "secrets_recovery_with_passphrase_recover_action" = "セキュリティーフレーズを使用"; "secrets_recovery_with_passphrase_information_verify_device" = "セキュリティーフレーズを使用して、この端末を認証してください。"; @@ -2735,3 +2735,92 @@ /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "規約は%@で確認できます。"; "analytics_prompt_message_upgrade" = "あなたは以前、利用状況に関する匿名データの共有に同意しました。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。"; +"spaces_creation_in_one_space" = "1個のスペースに"; +"spaces_creation_in_many_spaces" = "%@個のスペースに"; +"spaces_creation_in_spacename_plus_many" = "%@と%@個のスペースに"; +"spaces_creation_in_spacename_plus_one" = "%@と1個のスペースに"; +"spaces_creation_in_spacename" = "%@に"; +"event_formatter_group_call_incoming" = "%@(%@にて)"; + +// MARK: Reactions + +"room_event_action_reaction_more" = "他%@件"; +"notice_event_redacted_by_you" = " あなたにより"; +"room_displayname_all_other_members_left" = "%@(退出済)"; +"user_session_item_details_last_activity" = "直近のオンライン日時 %@"; +"version_check_modal_subtitle_deprecated" = "私たちは%@の高速化と改善に取り組んできました。残念ながら現在のiOSのバージョンはそれらの修正に対応していないため、サポートを終了しました。\nオペレーティングシステムをアップデートして、%@を最大限に活用しましょう。"; +"version_check_modal_subtitle_supported" = "私たちは%@の高速化と改善に取り組んできました。残念ながら現在のiOSのバージョンはそれらの修正に対応していないため、近日中にサポート外となります。\nオペレーティングシステムをアップデートして、%@を最大限に活用しましょう。"; +"key_verification_verified_this_session_information" = "保護されたメッセージをこの端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; +"key_verification_verified_new_session_information" = "保護されたメッセージを新しい端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; +"key_verification_verified_other_session_information" = "保護されたメッセージを他のセッションで読むことができます。また、他のユーザーもこのセッションを信頼することができます。"; +"call_consulting_with_user" = "%@に相談しています"; +"room_displayname_more_than_two_members" = "%@とその他%@人"; +"notice_error_unformattable_event" = "** メッセージを描画できません。不具合を報告してください"; +"wysiwyg_composer_format_action_un_indent" = "インデントを減らす"; +"wysiwyg_composer_format_action_indent" = "インデントを増やす"; +"wysiwyg_composer_format_action_strikethrough" = "下線で装飾"; +"wysiwyg_composer_format_action_underline" = "打ち消し線で装飾"; + + +// MARK: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "フォトライブラリー"; +"user_session_details_device_ip_location" = "IP位置情報"; +"user_session_details_session_section_footer" = "タップして押し続けるとデータをコピーします。"; +"device_name_mobile" = "%@モバイル"; +"device_name_web" = "%@ウェブ"; +// First item is client name and second item is session display name +"user_session_name" = "%@:%@"; +"user_session_verification_unknown_additional_info" = "現在のセッションを認証すると、このセッションの認証の状態を確認できます。"; +"user_sessions_overview_link_device" = "端末をリンク"; +"location_sharing_live_timer_incoming" = "%@まで共有(ライブ)"; +"location_sharing_live_list_item_last_update_invalid" = "最後の更新は不明です"; +"location_sharing_live_list_item_last_update" = "%@前に更新済"; +"location_sharing_live_list_item_sharing_expired" = "共有の期限が切れました"; +"location_sharing_map_credits_title" = "© Copyright"; +"location_sharing_allow_background_location_message" = "位置情報(ライブ)を共有したい場合、Elementはバックグラウンドで位置情報にアクセスできる必要があります。アクセスを許可するには、「設定」の「位置情報」にある「常に」をタップしてください。"; +"location_sharing_invalid_authorization_error_title" = "%@には位置情報にアクセスする権限がありません。「設定」の「位置情報」からアクセスを有効にできます。"; +"location_sharing_loading_map_error_title" = "%@は地図を読み込めませんでした。後でもう一度やり直してください。"; +"poll_history_no_past_poll_period_text" = "過去%@日に実施されたアンケートはありません。さらにアンケートを読み込み、前の月のアンケートを表示"; +"poll_history_no_active_poll_period_text" = "過去%@日に実施中のアンケートはありません。さらにアンケートを読み込み、前の月のアンケートを表示"; +"poll_history_detail_view_in_timeline" = "アンケートをタイムラインに表示"; +"space_invite_nav_title" = "スペースに招待"; + +// MARK: - Space Selector + +"space_selector_title" = "スペース"; +"room_invites_empty_view_information" = "ここに招待が表示されます。"; +"voice_message_stop_locked_mode_recording" = "録音をタップして停止または再生"; +"leave_space_and_more_rooms" = "スペースと%@個のルームから退出"; +"leave_space_and_one_room" = "スペースと1個のルームから退出"; +"spaces_creation_post_process_inviting_users" = "%@人のユーザーを招待しています"; +"spaces_creation_post_process_adding_rooms" = "%@個のルームを追加しています"; +"spaces_creation_new_rooms_message" = "それぞれにルームを作ります。"; +"spaces_creation_new_rooms_title" = "どのような議論を行いますか?"; +"spaces_subspace_creation_visibility_message" = "作成したスペースは%@に追加されます。"; +"spaces_feature_not_available" = "この機能はまだ利用できません。当面は、コンピューターで%@によりこれを行うことができます。"; +"spaces_no_member_found_detail" = "%@のメンバー以外の人を探していますか?当面は、ウェブ版またはデスクトップ版で招待することができます。"; +"spaces_coming_soon_detail" = "この機能はまだ実装されていません。当面は、コンピューターで%@によりこれを行うことができます。"; +"spaces_invites_coming_soon_title" = "招待は近日公開"; +"spaces_add_rooms_coming_soon_title" = "ルームの追加は近日公開"; +"spaces_no_room_found_detail" = "非公開で招待が必要なものは表示されていません。"; +"leave_space_message_admin_warning" = "あなたはこのスペースの管理者です。退出する前に、管理者の権限を別のメンバーに移譲してください。"; +"leave_space_message" = "%@から退出してよろしいですか?このスペースの全てのルームとスペースからも退出しますか?"; +"spaces_add_subspace_title" = "%@内にスペースを作成"; +"space_feature_unavailable_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。\n\n近日公開予定です。別のプラットフォームでスペースに参加すると、ここで参加するどのルームにもアクセスすることができます。"; +"space_beta_announce_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。iOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます。"; +"space_feature_unavailable_subtitle" = "スペースはiOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます"; +"space_beta_announce_subtitle" = "コミュニティー機能の新しいバージョン"; +"space_invite_not_enough_permission" = "このスペースにユーザーを招待する権限がありません"; +"room_invite_not_enough_permission" = "このルームにユーザーを招待する権限がありません"; +"room_invite_to_room_option_detail" = "%@には所属しません。"; +"room_invite_to_space_option_detail" = "%@を探索することはできますが、%@のメンバーにはなりません。"; + +// MARK: - Room invite + +"room_invite_to_space_option_title" = "%@に"; +"event_formatter_call_missed_video" = "不在着信(ビデオ)"; +"event_formatter_call_missed_voice" = "不在着信(音声)"; +"settings_push_rules_error" = "通知の設定をアップデートする際にエラーが発生しました。もう一度オプションを切り替えてみてください。"; +"settings_presence" = "プレゼンス(ステータス表示)"; From 5d80e359792132c84becef940eaec9a8820b9118 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 1 Feb 2023 15:00:15 +0000 Subject: [PATCH 435/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 3ddfd1586..7cf724608 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2925,3 +2925,4 @@ "wysiwyg_composer_format_action_un_indent" = "Зменшити відступ"; "wysiwyg_composer_format_action_indent" = "Збільшити відступ"; "settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз."; +"poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці"; From fac5605ab3bbd8f0a1755bff85c8aa9cae1d8b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 1 Feb 2023 15:07:03 +0000 Subject: [PATCH 436/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 866a2c1ac..530e2f1e8 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1473,7 +1473,7 @@ // Mark: - Polls -"poll_edit_form_create_poll" = "Koosta üks küsitlus"; +"poll_edit_form_create_poll" = "Loo selline küsitlus"; "settings_discovery_accept_terms" = "Nõustu isikutuvastusserveri tingimustega"; "poll_timeline_not_closed_action" = "Sobib"; "poll_timeline_not_closed_subtitle" = "Palun proovi uuesti"; @@ -1548,9 +1548,9 @@ // Onboarding "onboarding_splash_register_button_title" = "Loo kasutajakonto"; "poll_edit_form_poll_type_closed_description" = "Tulemusi kuvame vaid siis, kui küsitlus on lõppenud"; -"poll_edit_form_poll_type_closed" = "Küsitlus on lõppenud"; +"poll_edit_form_poll_type_closed" = "Suletud valikutega küsitlus"; "poll_edit_form_poll_type_open_description" = "Osalejad näevad tulemusi peale oma valiku salvestamist"; -"poll_edit_form_poll_type_open" = "Ava küsitlus"; +"poll_edit_form_poll_type_open" = "Avatud valikutega küsitlus"; "poll_edit_form_update_failure_subtitle" = "Palun proovi uuesti"; "poll_edit_form_update_failure_title" = "Küsitluse muutmine ei õnnestunud"; "poll_edit_form_poll_type" = "Küsitluse tüüp"; @@ -2672,3 +2672,4 @@ "wysiwyg_composer_format_action_un_indent" = "Vähenda taandrida"; "wysiwyg_composer_format_action_indent" = "Suurenda taandrida"; "settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada."; +"poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel"; From f18938150b7a62e27d3665c3e7ff356a3543f7bc Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 1 Feb 2023 23:49:09 +0000 Subject: [PATCH 437/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 21ff49f35..b0f0f77be 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2926,3 +2926,5 @@ "home_context_menu_mark_as_unread" = "Tandai sebagai belum dibaca"; "wysiwyg_composer_format_action_un_indent" = "Kurangi indentasi"; "wysiwyg_composer_format_action_indent" = "Tambahkan indentasi"; +"poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa"; +"settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda."; From a1903425564e847e0babaf6dc4ed44135ea372c5 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 1 Feb 2023 18:45:49 +0000 Subject: [PATCH 438/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 709eec675..18c4079b5 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2922,3 +2922,5 @@ "key_backup_recover_from_private_key_progress" = "%@%% Dokončené"; "wysiwyg_composer_format_action_indent" = "Zväčšenie odsadenia"; "wysiwyg_composer_format_action_un_indent" = "Zmenšenie odsadenia"; +"poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi"; +"settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova."; From 3bd03bda749ac050b5fc9d81be56e6da5e0557f9 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Feb 2023 13:56:45 +0000 Subject: [PATCH 439/468] Translated using Weblate (Japanese) Currently translated at 99.9% (2377 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 78 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 987aefa1f..fd8936720 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -325,7 +325,7 @@ "room_details_people" = "メンバー"; "room_details_files" = "アップロード"; "room_details_settings" = "設定"; -"room_details_photo" = "ルームのアイコン画像"; +"room_details_photo" = "ルームの画像"; "room_details_room_name" = "ルーム名"; "room_details_topic" = "トピック"; "room_details_favourite_tag" = "お気に入り"; @@ -337,27 +337,27 @@ "room_details_access_section_anyone_apart_from_guest" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_no_address_warning" = "このルームへのリンクを作成するには、ルームのアドレスが必要です"; -"room_details_access_section_directory_toggle" = "ルーム一覧へ公開"; -"room_details_history_section" = "履歴を閲覧できる人"; +"room_details_access_section_directory_toggle" = "このルームをルーム一覧に掲載"; +"room_details_history_section" = "履歴を閲覧できる人は?"; "room_details_history_section_anyone" = "誰でも"; -"room_details_history_section_members_only" = "メンバーのみ (この設定を選択した時点から)"; +"room_details_history_section_members_only" = "メンバーのみ(この設定を選択した時点から)"; "room_details_history_section_members_only_since_invited" = "メンバーのみ(招待を送った時点から)"; -"room_details_history_section_members_only_since_joined" = "メンバーのみ (参加した時点から)"; -"room_details_history_section_prompt_title" = "個人情報の警告"; -"room_details_history_section_prompt_msg" = "発言履歴を読むことができる人の変更は、以後の発言にのみ適用されます。既存の発言履歴の可視性は変更されません。"; +"room_details_history_section_members_only_since_joined" = "メンバーのみ(参加した時点から)"; +"room_details_history_section_prompt_title" = "プライバシーに関する警告"; +"room_details_history_section_prompt_msg" = "履歴の閲覧権限に関する変更は、今後、このルームで表示されるメッセージにのみ適用されます。既存の履歴の見え方には影響しません。"; "room_details_addresses_section" = "アドレス"; "room_details_no_local_addresses" = "このルームにはローカルアドレスがありません"; "room_details_new_address" = "新しいアドレスを追加"; "room_details_new_address_placeholder" = "新しいアドレスを追加(例 #foo%@)"; -"room_details_addresses_invalid_address_prompt_title" = "不正なエイリアスのフォーマット"; -"room_details_addresses_invalid_address_prompt_msg" = "%@はエイリアスの正しいフォーマットではありません"; +"room_details_addresses_invalid_address_prompt_title" = "エイリアスの形式が正しくありません"; +"room_details_addresses_invalid_address_prompt_msg" = "%@はエイリアスの正しい形式ではありません"; "room_details_addresses_disable_main_address_prompt_title" = "メインアドレスの警告"; -"room_details_addresses_disable_main_address_prompt_msg" = "メインアドレスが設定されていません。このルームのメインアドレスは無作為に選択、設定されます"; +"room_details_addresses_disable_main_address_prompt_msg" = "メインアドレスが設定されていません。このルームのメインアドレスはランダムに設定されます"; "room_details_banned_users_section" = "ブロックされたユーザー"; "room_details_advanced_section" = "高度な設定"; "room_details_advanced_room_id" = "ルームID:"; -"room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告: 有効後にこれを無効にすることはできません!)"; -"room_details_advanced_e2e_encryption_enabled" = "このルームでは暗号化が有効になっています"; +"room_details_advanced_enable_e2e_encryption" = "暗号化を有効にする(警告:有効にした後に無効にすることはできません!)"; +"room_details_advanced_e2e_encryption_enabled" = "このルームでは暗号化が有効です"; "room_details_advanced_e2e_encryption_disabled" = "このルームでは暗号化が有効ではありません。"; "room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "認証済のセッションにのみ暗号化"; "room_details_fail_to_update_avatar" = "ルームのアイコン画像の更新に失敗"; @@ -365,8 +365,8 @@ "room_details_fail_to_update_topic" = "トピックの更新に失敗"; "room_details_fail_to_update_room_guest_access" = "ゲストによるルームへのアクセスの設定更新に失敗"; "room_details_fail_to_update_room_join_rule" = "参加ルールの更新に失敗"; -"room_details_fail_to_update_room_directory_visibility" = "ルーム一覧の可視設定の更新に失敗"; -"room_details_fail_to_update_history_visibility" = "発言履歴の可視範囲の設定更新に失敗"; +"room_details_fail_to_update_room_directory_visibility" = "ルーム一覧の見え方の更新に失敗"; +"room_details_fail_to_update_history_visibility" = "履歴の見え方の設定更新に失敗"; "room_details_fail_to_add_room_aliases" = "新しいルームアドレスの追加に失敗"; "room_details_fail_to_remove_room_aliases" = "ルームアドレスの削除に失敗"; "room_details_fail_to_update_room_canonical_alias" = "メインアドレスの更新に失敗"; @@ -382,21 +382,21 @@ "read_receipts_list" = "既読一覧を見る"; "receipt_status_read" = "既読状況: "; // Media picker -"media_picker_library" = "ライブラリ"; +"media_picker_library" = "ライブラリー"; "media_picker_select" = "選択"; // Directory "directory_title" = "ルーム一覧"; "directory_server_picker_title" = "ルーム一覧を選択"; "directory_server_all_rooms" = "%@ サーバー上の全てのルーム"; "directory_server_all_native_rooms" = "全てのMatrix連携ルーム"; -"directory_server_type_homeserver" = "公開ルーム一覧を表示するための接続サーバーを入力してください"; +"directory_server_type_homeserver" = "公開ルームの一覧を表示するホームサーバーを入力してください"; "directory_server_placeholder" = "matrix.org"; // Events formatter -"event_formatter_member_updates" = "%tu権限が変更されました"; -"event_formatter_widget_added" = "%@ウィジェットが %@ さんにより追加されました"; -"event_formatter_widget_removed" = "%@ウィジェットが %@ さんにより削除されました"; -"event_formatter_jitsi_widget_added" = "音声会議が%@ さんにより追加されました"; -"event_formatter_jitsi_widget_removed" = "音声会議が%@ さんにより削除されました"; +"event_formatter_member_updates" = "%tu個の権限の変更"; +"event_formatter_widget_added" = "%@のウィジェットが%@により追加されました"; +"event_formatter_widget_removed" = "%@のウィジェットが%@により削除されました"; +"event_formatter_jitsi_widget_added" = "VoIP会議が%@により追加されました"; +"event_formatter_jitsi_widget_removed" = "VoIP会議が%@により削除されました"; // Others "or" = "または"; "you" = "あなた"; @@ -404,10 +404,10 @@ "yesterday" = "昨日"; "network_offline_prompt" = "インターネットへの接続が切れているようです。"; "public_room_section_title" = "公開ルーム(%@ にて):"; -"bug_report_prompt" = "前回アプリが異常終了しました。バグレポートを送信しますか?"; -"rage_shake_prompt" = "あなたは不満があって端末を揺らしているようです。バグレポートをしますか?"; +"bug_report_prompt" = "前回アプリケーションがクラッシュしました。クラッシュ報告を送信しますか?"; +"rage_shake_prompt" = "あなたは不満で端末を振っているようです。バグレポートを報告しますか?"; "do_not_ask_again" = "再び表示しない"; -"camera_access_not_granted" = "%@はカメラを使用する権限を持っていません。個人情報保護設定の変更をお願いします"; +"camera_access_not_granted" = "%@にはカメラを使用する権限がありません。プライバシー設定を変更してください"; "large_badge_value_k_format" = "%.1fK"; // room display name "room_displayname_room_invite" = "招待"; @@ -486,7 +486,7 @@ "settings_deactivate_my_account" = "アカウントを永久に無効にする"; "room_details_flair_section" = "コミュニティーの特色を表示"; "room_details_new_flair_placeholder" = "新しいコミュニティーIDを追加(例 +foo%@)"; -"room_details_flair_invalid_id_prompt_title" = "無効な形式"; +"room_details_flair_invalid_id_prompt_title" = "不正な形式です"; "room_details_flair_invalid_id_prompt_msg" = "%@はコミュニティーの有効な識別子ではありません"; "room_details_fail_to_update_room_communities" = "関連するコミュニティーの更新に失敗"; // Group Details @@ -495,9 +495,9 @@ "group_details_people" = "連絡先"; "group_details_rooms" = "ルーム"; // Group Home -"group_home_one_member_format" = "1名のメンバー"; -"group_home_multi_members_format" = "%tu名のメンバー"; -"group_home_one_room_format" = "1つのルーム"; +"group_home_one_member_format" = "1人のメンバー"; +"group_home_multi_members_format" = "%tu人のメンバー"; +"group_home_one_room_format" = "1個のルーム"; "group_home_multi_rooms_format" = "%tu個のルーム"; "group_invitation_format" = "%@がこのコミュニティーにあなたを招待しました"; // Group participants @@ -514,7 +514,7 @@ "group_participants_invite_malformed_id" = "不正なID。'@localpart:domain'のようなMatrix IDでなければなりません"; "group_participants_invited_section" = "招待中"; // Group rooms -"group_rooms_filter_rooms" = "コミュニティールームを絞り込む"; +"group_rooms_filter_rooms" = "コミュニティーのルームを絞り込む"; "event_formatter_rerequest_keys_part1_link" = "暗号鍵を再要求"; "event_formatter_rerequest_keys_part2" = " あなたの他のセッションに。"; "homeserver_connection_lost" = "ホームサーバーに接続できませんでした。"; @@ -645,8 +645,8 @@ "call_no_stun_server_error_message_1" = "通話を確実に機能させるためには、ホームサーバー%@の管理者にTURNサーバーの設定を依頼してください。"; "call_no_stun_server_error_title" = "サーバーの設定が間違っているため通話に失敗しました"; "room_does_not_exist" = "%@は存在しません"; -"photo_library_access_not_granted" = "%@はフォトライブラリにアクセスする権限がありません"; -"camera_unavailable" = "お使いの端末ではカメラを利用できません"; +"photo_library_access_not_granted" = "%@にはフォトライブラリーにアクセスする権限がありません。プライバシー設定を変更してください"; +"camera_unavailable" = "この端末ではカメラを利用できません"; "event_formatter_widget_removed_by_you" = "ウィジェットを削除しました:%@"; "event_formatter_jitsi_widget_removed_by_you" = "VoIP会議を削除しました"; "event_formatter_jitsi_widget_added_by_you" = "VoIP会議を追加しました"; @@ -660,15 +660,15 @@ "event_formatter_call_video" = "ビデオ通話"; "event_formatter_call_voice" = "音声通話"; "event_formatter_message_edited_mention" = "(編集済)"; -"image_picker_action_library" = "ライブラリを選ぶ"; +"image_picker_action_library" = "ライブラリーから選択"; // Image picker -"image_picker_action_camera" = "写真を撮る"; +"image_picker_action_camera" = "写真を撮影"; // Media picker -"media_picker_title" = "メディアライブラリ"; +"media_picker_title" = "メディアライブラリー"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "ここでは暗号化が有効ではありません。"; -"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効になっています"; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "ここでは暗号化が有効です"; "room_details_advanced_room_id_for_dm" = "ID:"; "room_details_no_local_addresses_for_dm" = "ここにはローカルアドレスがありません"; "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; @@ -1711,7 +1711,7 @@ "create_room_promotion_header" = "プロモート"; "searchable_directory_search_placeholder" = "名前または ID"; "room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; -"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースメンバーに参加を推奨するものとして PR されます。"; +"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースのメンバーに参加を推奨するものとしてPRされます。"; // Room suggestion Settings "room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; @@ -1739,7 +1739,7 @@ "space_selector_empty_view_title" = "まだスペースがありません"; "all_chats_empty_list_placeholder_title" = "未読はありません"; "all_chats_empty_unreads_placeholder_message" = "未読のメッセージがある場合は、ここに表示されます。"; -"room_notifs_settings_account_settings" = "アカウント設定"; +"room_notifs_settings_account_settings" = "アカウントの設定"; "room_access_settings_screen_upgrade_alert_upgrading" = "ルームをアップグレードしています"; "room_access_settings_screen_upgrade_alert_upgrade_button" = "アップグレード"; "room_access_settings_screen_edit_spaces" = "スペースを編集"; @@ -1747,7 +1747,7 @@ "room_access_settings_screen_upgrade_alert_title" = "ルームをアップグレード"; "room_access_settings_screen_public_message" = "誰でも検索・参加できます。"; "room_access_settings_screen_private_message" = "招待された人だけが検索・参加できます。"; -"room_access_settings_screen_message" = "誰が %@ を検索・参加できるか選択してください。"; +"room_access_settings_screen_message" = "誰が%@を検索・参加できるか選択してください。"; "space_settings_access_section" = "このスペースにアクセスできる人は?"; "room_access_settings_screen_title" = "このルームにアクセスできる人は?"; "room_notifs_settings_none" = "なし"; @@ -2501,7 +2501,7 @@ "space_private_join_rule" = "非公開のスペース"; "spaces_no_result_found_title" = "検索結果がありません"; "space_tag" = "スペース"; -"spaces_explore_rooms_one_room" = "1つのルーム"; +"spaces_explore_rooms_one_room" = "1個のルーム"; "spaces_explore_rooms_room_number" = "%@個のルーム"; "leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; "leave_space_only_action" = "どのルームからも退出しない"; From 567c4800d0bfa7785260f07125cc7baca8e61c45 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Feb 2023 17:56:52 +0000 Subject: [PATCH 440/468] Translated using Weblate (Japanese) Currently translated at 99.9% (2377 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 122 ++++++++++++++-------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index fd8936720..958c79f03 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -198,10 +198,10 @@ "room_offline_notification" = "サーバーとの接続が失われました。"; "room_unsent_messages_notification" = "メッセージを送信できませんでした。"; "room_unsent_messages_unknown_devices_notification" = "不明なセッションが存在するため、メッセージの送信に失敗しました。"; -"room_ongoing_conference_call" = "会議通話実施中。%@または%@で参加してください。"; -"room_ongoing_conference_call_with_close" = "会議通話実施中。%@または%@で参加してください。%@。"; +"room_ongoing_conference_call" = "グループ通話を実施中。%@または%@で参加してください。"; +"room_ongoing_conference_call_with_close" = "グループ通話を実施中。%@または%@で参加してください。%@。"; "room_ongoing_conference_call_close" = "閉じる"; -"room_conference_call_no_power" = "このルームで会議通話を管理する権限が必要です"; +"room_conference_call_no_power" = "このルームでグループ通話を管理するための権限が必要です"; "room_prompt_resend" = "全て再送信"; "room_prompt_cancel" = "全てキャンセル"; "room_resend_unsent_messages" = "未送信のメッセージを再送信"; @@ -404,7 +404,7 @@ "yesterday" = "昨日"; "network_offline_prompt" = "インターネットへの接続が切れているようです。"; "public_room_section_title" = "公開ルーム(%@ にて):"; -"bug_report_prompt" = "前回アプリケーションがクラッシュしました。クラッシュ報告を送信しますか?"; +"bug_report_prompt" = "前回アプリケーションがクラッシュしました。クラッシュレポートを送信しますか?"; "rage_shake_prompt" = "あなたは不満で端末を振っているようです。バグレポートを報告しますか?"; "do_not_ask_again" = "再び表示しない"; "camera_access_not_granted" = "%@にはカメラを使用する権限がありません。プライバシー設定を変更してください"; @@ -414,31 +414,31 @@ "room_displayname_two_members" = "%@と%@"; "room_displayname_no_title" = "だれもいない部屋"; // Call -"call_incoming_voice_prompt" = "%@ さんから通話の着信中"; -"call_incoming_video_prompt" = "%@ さんから映像つき通話の着信中"; +"call_incoming_voice_prompt" = "%@から通話の着信中"; +"call_incoming_video_prompt" = "%@からビデオ通話の着信中"; "call_incoming_voice" = "着信中…"; "call_incoming_video" = "ビデオ通話の着信中…"; "call_already_displayed" = "既に通話中です。"; -"call_jitsi_error" = "会議通話への参加に失敗しました。"; +"call_jitsi_error" = "グループ通話への参加に失敗しました。"; // No VoIP support -"no_voip_title" = "通話着信中"; -"no_voip" = "%@さんから通話の着信がありましたが、%@は通話をまだサポートしていません。\nこの通知を無視して、別の端末から着信に応答することも、拒否することもできます。"; +"no_voip_title" = "着信中"; +"no_voip" = "%@があなたを呼び出していますが、%@はまだ通話をサポートしていません。\nこの通知を無視して別の端末から着信に応答することも、または着信を拒否することもできます。"; // Crash report // Crypto "e2e_need_log_in_again" = "再度ログインして、このセッションのエンドツーエンド暗号鍵を生成し、公開鍵をホームサーバーに送信する必要があります。\nご迷惑をおかけしますが、ご了承ください。"; // Bug report "bug_report_title" = "バグレポート"; -"bug_report_description" = "誤動作の内容と状況の説明をお願い致します。あなたは何をしましたか?何が起こると思いますか?実際何が起こったのですか?"; -"bug_crash_report_title" = "異常終了報告"; -"bug_crash_report_description" = "異常停止する前にあなたがしていたことを記してください:"; -"bug_report_logs_description" = "開発者が問題を診断するために、このElementのログがバグレポートと一緒に送信されます。上記文章のみを送信したい場合は以下のチェックを解除してください:"; +"bug_report_description" = "不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか?"; +"bug_crash_report_title" = "クラッシュレポート"; +"bug_crash_report_description" = "クラッシュする前にあなたがしていたことを記してください:"; +"bug_report_logs_description" = "開発者が問題を診断するために、このElementのログがバグレポートと一緒に送信されます。上記の文章のみを送信したい場合は、以下のチェックを解除してください:"; "bug_report_send_logs" = "ログを送信"; -"bug_report_send_screenshot" = "画面のスクリーンショット画像を送信"; -"bug_report_progress_zipping" = "ログを収集"; +"bug_report_send_screenshot" = "スクリーンショットの画像を送信"; +"bug_report_progress_zipping" = "ログを収集しています"; "bug_report_progress_uploading" = "報告を送信しています"; "bug_report_send" = "送信"; // Widget -"widget_no_power_to_manage" = "あなたがこのルームでウィジェットを管理するための権限が必要です"; +"widget_no_power_to_manage" = "このルームでウィジェットを管理するための権限が必要です"; "widget_creation_failure" = "ウィジェットの作成に失敗しました"; // Widget Integration Manager "widget_integration_need_to_be_able_to_invite" = "それを行うにはユーザーを招待する権限が必要です。"; @@ -448,8 +448,8 @@ "widget_integration_positive_power_level" = "権限の数値は正の整数で入力してください。"; "widget_integration_must_be_in_room" = "あなたはこのルームに所属していません。"; "widget_integration_no_permission_in_room" = "あなたはこのルームで権限がありません。"; -"widget_integration_missing_room_id" = "ルーム固有IDの要求に失敗しました。"; -"widget_integration_missing_user_id" = "ユーザー固有IDの要求に失敗しました。"; +"widget_integration_missing_room_id" = "リクエストにroom_idがありません。"; +"widget_integration_missing_user_id" = "リクエストにuser_idがありません。"; "widget_integration_room_not_visible" = "ルーム %@ は見えません。"; // Share extension "share_extension_auth_prompt" = "メインのアプリにログインしてコンテンツを共有"; @@ -522,29 +522,29 @@ "widget_sticker_picker_no_stickerpacks_alert_add_now" = "今すぐ追加しますか?"; // Room key request dialog "e2e_room_key_request_title" = "暗号鍵の要求"; -"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; +"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション '%@' を追加しました。"; "e2e_room_key_request_message" = "未認証のセッション '%@' が暗号鍵を要求しています。"; "e2e_room_key_request_start_verification" = "認証を開始…"; "e2e_room_key_request_share_without_verifying" = "認証せず共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR -"gdpr_consent_not_given_alert_message" = "%@ホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; +"gdpr_consent_not_given_alert_message" = "%@のホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; "gdpr_consent_not_given_alert_review_now_action" = "確認"; -"deactivate_account_title" = "無効なアカウント"; -"deactivate_account_informations_part1" = "これにより、アカウントは永久に使用できなくなります。ログインすることはできず、誰も同じユーザーIDを再登録することはできません。これにより、あなたのアカウントは参加している全てのルームから退去し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; +"deactivate_account_title" = "アカウントを無効化"; +"deactivate_account_informations_part1" = "これにより、アカウントは永久に使用できなくなります。ログインしたり同じユーザーIDを再登録したりすることはできません。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; "deactivate_account_informations_part2_emphasize" = "この動作は元に戻せません。"; -"deactivate_account_informations_part3" = "\n\nアカウントの無効化 "; -"deactivate_account_informations_part4_emphasize" = "デフォルトではあなたが送信したメッセージを忘れることはありません。 "; -"deactivate_account_informations_part5" = "メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; -"deactivate_account_forget_messages_information_part1" = "アカウントが無効になったときに送信した全てのメッセージを忘れてください ("; +"deactivate_account_informations_part3" = "\n\nアカウントを無効化しても "; +"deactivate_account_informations_part4_emphasize" = "既定ではあなたが送信したメッセージは消去されません。 "; +"deactivate_account_informations_part5" = "メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様です。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; +"deactivate_account_forget_messages_information_part1" = "アカウントを無効化する際、全ての送信済のメッセージを消去("; "deactivate_account_forget_messages_information_part2_emphasize" = "警告"; -"deactivate_account_forget_messages_information_part3" = ":これは将来のユーザーに会話の不完全なビューが表示される)"; -"deactivate_account_validate_action" = "無効なアカウント"; -"deactivate_account_password_alert_title" = "無効なアカウント"; -"deactivate_account_password_alert_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; +"deactivate_account_forget_messages_information_part3" = ":今後のユーザーには、不完全な会話が表示されます)"; +"deactivate_account_validate_action" = "アカウントを無効化"; +"deactivate_account_password_alert_title" = "アカウントを無効化"; +"deactivate_account_password_alert_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; // Re-request confirmation dialog -"rerequest_keys_alert_title" = "要求が送信されました"; -"rerequest_keys_alert_message" = "鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で%@を起動してください。"; +"rerequest_keys_alert_title" = "要求を送信しました"; +"rerequest_keys_alert_message" = "鍵をこのセッションに送信するために、メッセージを復号化できる他の端末で%@を起動してください。"; "room_event_action_ban_prompt_reason" = "このユーザーをブロックする理由"; "room_resource_limit_exceeded_message_contact_1" = " お願い "; "settings_ui_theme_black" = "ブラック"; @@ -606,7 +606,7 @@ "device_verification_emoji_clock" = "時計"; "device_verification_emoji_hourglass" = "砂時計"; "device_verification_emoji_umbrella" = "雨"; -"device_verification_emoji_thumbs up" = "親指を立てる"; +"device_verification_emoji_thumbs up" = "いいね"; "device_verification_emoji_spanner" = "スパナ"; "device_verification_emoji_santa" = "サンタ"; "device_verification_emoji_glasses" = "メガネ"; @@ -619,7 +619,7 @@ // Room widget permissions "room_widget_permission_title" = "ウィジェットを読み込む"; -"widget_picker_manage_integrations" = "インテグレーションを管理する…"; +"widget_picker_manage_integrations" = "インテグレーションを管理…"; // Widget Picker "widget_picker_title" = "インテグレーション"; @@ -627,12 +627,12 @@ "widget_menu_remove" = "全員から削除"; "widget_menu_revoke_permission" = "アクセスを取り消す"; "widget_menu_open_outside" = "ブラウザーで開く"; -"widget_menu_refresh" = "リフレッシュ"; -"widget_integrations_server_failed_to_connect" = "インテグレーションサーバーへの接続が失敗しました"; +"widget_menu_refresh" = "再読み込み"; +"widget_integrations_server_failed_to_connect" = "インテグレーションサーバーへの接続に失敗しました"; // Widget "widget_no_integrations_server_configured" = "インテグレーションサーバーが設定されていません"; -"bug_report_background_mode" = "バックグラウンドで継続"; +"bug_report_background_mode" = "バックグラウンドで続行"; "e2e_key_backup_wrong_version_button_wasme" = "これはわたしです"; "e2e_key_backup_wrong_version_button_settings" = "設定"; "e2e_key_backup_wrong_version" = "メッセージの鍵の新しい安全なバックアップが検出されました。\n\nこれがあなたによるものではない場合は、設定から新しいパスフレーズを設定してください。"; @@ -640,10 +640,10 @@ // Key backup wrong version "e2e_key_backup_wrong_version_title" = "新しい鍵のバックアップ"; "call_no_stun_server_error_use_fallback_button" = "%@を使ってみてください"; -"call_actions_unhold" = "やり直す"; -"call_no_stun_server_error_message_2" = "代わりに、%@のパブリックサーバーを使用することもできますが、これは信頼性が低くあなたのIPアドレスがそのサーバーと共有されてしまいます。これは、設定から管理することができます"; -"call_no_stun_server_error_message_1" = "通話を確実に機能させるためには、ホームサーバー%@の管理者にTURNサーバーの設定を依頼してください。"; -"call_no_stun_server_error_title" = "サーバーの設定が間違っているため通話に失敗しました"; +"call_actions_unhold" = "再開"; +"call_no_stun_server_error_message_2" = "または %@ の公開サーバーを使用することもできますが、信頼性が低く、また、あなたのIPアドレスがそのサーバーと共有されてしまいます。これは設定画面からも管理できます"; +"call_no_stun_server_error_message_1" = "安定した通話のために、ホームサーバー %@ の管理者にTURNサーバーの設定を依頼してください。"; +"call_no_stun_server_error_title" = "サーバーの不正な設定のため通話に失敗しました"; "room_does_not_exist" = "%@は存在しません"; "photo_library_access_not_granted" = "%@にはフォトライブラリーにアクセスする権限がありません。プライバシー設定を変更してください"; "camera_unavailable" = "この端末ではカメラを利用できません"; @@ -929,7 +929,7 @@ "key_verification_incoming_request_incoming_alert_message" = "%@は認証を要求しています"; "key_verification_tile_conclusion_warning_title" = "信頼されていないサインイン"; "key_verification_tile_conclusion_done_title" = "認証済"; -"key_verification_tile_request_incoming_approval_decline" = "却下"; +"key_verification_tile_request_incoming_approval_decline" = "拒否"; "key_verification_tile_request_incoming_approval_accept" = "承認"; "key_verification_tile_request_status_accepted" = "あなたは承認しました"; "key_verification_tile_request_status_cancelled" = "%@はキャンセルしました"; @@ -955,20 +955,20 @@ // MARK: Reaction history "reaction_history_title" = "リアクションの履歴"; -"emoji_picker_places_category" = "旅と場所"; -"emoji_picker_flags_category" = "国旗"; +"emoji_picker_places_category" = "旅行と場所"; +"emoji_picker_flags_category" = "旗"; "emoji_picker_symbols_category" = "シンボル"; -"emoji_picker_objects_category" = "オブジェクト"; +"emoji_picker_objects_category" = "物体"; "emoji_picker_foods_category" = "食べ物と飲み物"; "emoji_picker_nature_category" = "動物と自然"; -"emoji_picker_people_category" = "笑顔とみんな"; +"emoji_picker_people_category" = "表情と人々"; // MARK: Emoji picker "emoji_picker_title" = "ピッカー"; // MARK: File upload "file_upload_error_title" = "ファイルのアップロードエラー"; -"file_upload_error_unsupported_file_type_message" = "ファイルのタイプがサポートされていません。"; +"file_upload_error_unsupported_file_type_message" = "ファイルの種類がサポートされていません。"; "device_verification_emoji_pin" = "ピン"; "device_verification_emoji_folder" = "フォルダー"; "device_verification_emoji_headphones" = "ヘッドフォン"; @@ -982,7 +982,7 @@ "device_verification_emoji_aeroplane" = "飛行機"; "device_verification_emoji_bicycle" = "自転車"; "device_verification_emoji_train" = "電車"; -"device_verification_emoji_flag" = "フラグ"; +"device_verification_emoji_flag" = "旗"; "device_verification_emoji_telephone" = "テレフォン"; "device_verification_emoji_hammer" = "ハンマー"; "device_verification_emoji_key" = "鍵"; @@ -1014,20 +1014,20 @@ "room_event_action_reaction_show_all" = "全てを見る"; "room_event_action_edit" = "編集"; "room_event_action_reply" = "返信"; -"device_verification_security_advice_emoji" = "絵文字を比較して、同じ順番で現れているのを確認してください。"; +"device_verification_security_advice_emoji" = "絵文字を比較して、同じ順番で現れていることを確認してください。"; "key_verification_verify_sas_validate_action" = "一致しています"; -"key_verification_verify_sas_cancel_action" = "一致しません"; +"key_verification_verify_sas_cancel_action" = "一致していません"; // MARK: Verify -"key_verification_verify_sas_title_emoji" = "絵文字の比較"; +"key_verification_verify_sas_title_emoji" = "絵文字を比較"; "device_verification_self_verify_alert_validate_action" = "認証"; -"device_verification_self_verify_alert_message" = "ログインを認証してください:%@"; +"device_verification_self_verify_alert_message" = "新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%@"; // MARK: Self verification start // New login -"device_verification_self_verify_alert_title" = "ログインしましたか?"; +"device_verification_self_verify_alert_title" = "新しいログインです。ログインしましたか?"; "room_recents_suggested_rooms_section" = "おすすめのルーム"; "settings_show_url_previews_description" = "プレビューは暗号化されていないルームでのみ表示されます。"; "settings_show_url_previews" = "ウェブサイトのプレビューを表示"; @@ -1789,7 +1789,7 @@ "location_sharing_live_share_title" = "位置情報(ライブ)を共有"; "service_terms_modal_decline_button" = "拒否"; "service_terms_modal_accept_button" = "同意"; -"service_terms_modal_description_identity_server" = "この操作により、端末の連絡先にあなたの電話番号や電子メールを保存している人があなたを検索できるようになります。"; +"service_terms_modal_description_identity_server" = "これにより、端末の連絡先にあなたの電話番号や電子メールを保存している人が、あなたを検索できるようになります。"; // Service terms "service_terms_modal_title_message" = "続行するには、以下の利用規約に同意してください"; @@ -1974,7 +1974,7 @@ "key_backup_setup_passphrase_title" = "バックアップをセキュリティーフレーズで保護"; "key_backup_setup_intro_manual_export_action" = "手動で鍵をエクスポート"; "key_backup_setup_intro_manual_export_info" = "(高度)"; -"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "このセッションを鍵のバックアップに接続"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "この端末を鍵のバックアップに接続"; "key_backup_setup_intro_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。\n\n鍵を失くさないよう、鍵を安全にバックアップしてください。"; // Intro @@ -2042,7 +2042,7 @@ // Recover from passphrase -"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、メッセージの履歴のロックを解除できます"; +"key_backup_recover_from_passphrase_info" = "セキュリティーフレーズを使うと、暗号化されたメッセージの履歴のロックを解除できます"; "key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "セキュリティーフレーズが分かりませんか?そんなときは "; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "。"; "key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "セキュリティーキーを使いましょう"; @@ -2339,14 +2339,14 @@ // MARK: Start "device_verification_start_title" = "短い文字列を比較して認証"; "device_verification_incoming_description_2" = "このセッションを認証すると、信頼済としてマークされ、あなたのセッションも相手に信頼済としてマークされます。"; -"device_verification_incoming_description_1" = "このセッションを認証して、信頼済としてマークします。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; +"device_verification_incoming_description_1" = "このセッションを認証すると、信頼済としてマークされます。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; // MARK: Incoming "device_verification_incoming_title" = "認証のリクエストが届いています"; "device_verification_error_cannot_load_device" = "セッションの情報を読み込めません。"; "device_verification_cancelled_by_me" = "認証がキャンセルされました。理由:%@"; "device_verification_cancelled" = "相手が認証をキャンセルしました。"; -"device_verification_security_advice_number" = "数字を比較して、同じ順番で現れているのを確認してください。"; +"device_verification_security_advice_number" = "数字を比較して、同じ順番で現れていることを確認してください。"; "key_verification_this_session_title" = "このセッションを認証"; // MARK: - Device Verification @@ -2356,7 +2356,7 @@ "sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。"; "sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "暗号化されたメッセージは不要です"; "sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "セキュアバックアップを使用開始"; -"sign_out_non_existing_key_backup_alert_title" = "今ここでサインアウトすると、あなたの暗号化されたメッセージにアクセスできなくなります"; +"sign_out_non_existing_key_backup_alert_title" = "今サインアウトすると、あなたの暗号化されたメッセージにアクセスできなくなります"; "sign_out_confirmation_message" = "サインアウトしてよろしいですか?"; // MARK: Sign out warning @@ -2717,7 +2717,7 @@ "device_verification_self_verify_wait_additional_information" = "これは%@と、クロス署名に対応した他のMatrixのクライアントで機能します。"; "device_verification_self_verify_wait_information" = "暗号化されたメッセージにアクセスするには、あなたの他のセッションからこのセッションを認証する必要があります。\n\n他の端末で最新の%@を使用してください:"; "key_verification_self_verify_current_session_alert_message" = "他のユーザーは信頼しないかもしれません。"; -"device_verification_start_use_legacy" = "何も表示されますか?まだ全てのクライアントはインタラクティブな認証をサポートしていません。レガシー認証を使用してください。"; +"device_verification_start_use_legacy" = "何も表示されませんか?まだ全てのクライアントはインタラクティブな認証をサポートしていません。レガシー認証を使用してください。"; "device_verification_start_wait_partner" = "相手の承諾を待機しています…"; "key_verification_user_title" = "認証"; "key_verification_new_session_title" = "新しいセッションを認証"; @@ -2728,7 +2728,7 @@ "service_terms_modal_information_description_integration_manager" = "インテグレーションマネージャーを使うと、第三者による機能を追加することができます。"; "service_terms_modal_information_description_identity_server" = "IDサーバーを使うと、電話番号やメールアドレスを検索して、連絡先が既にアカウントをもっているかどうか確認することができます。"; "service_terms_modal_description_integration_manager" = "ボット、ブリッジ、ウィジェット、ステッカーパックの使用を許可します。"; -"share_extension_low_quality_video_title" = "動画を低品質で送信します"; +"share_extension_low_quality_video_title" = "動画を低品質で送信"; "analytics_prompt_yes" = "はい、大丈夫です"; /* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ "analytics_prompt_terms_upgrade" = "規約を%@で確認してください。よろしいですか?"; From bcc55749085d3cc876a4fb1f5d7828edc8ec09d3 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Thu, 2 Feb 2023 19:06:05 +0000 Subject: [PATCH 441/468] Translated using Weblate (Japanese) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 48 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 958c79f03..f64ab7b57 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -597,22 +597,22 @@ // Call Bar "callbar_only_single_active" = "タップして通話(%@)に戻る"; "settings_add_3pid_password_title_msidsn" = "電話番号を追加"; -"device_verification_emoji_scissors" = "ハサミ"; -"device_verification_emoji_paperclip" = "ペーパークリップ"; +"device_verification_emoji_scissors" = "はさみ"; +"device_verification_emoji_paperclip" = "クリップ"; "device_verification_emoji_pencil" = "鉛筆"; "device_verification_emoji_book" = "本"; "device_verification_emoji_light bulb" = "電球"; "device_verification_emoji_gift" = "ギフト"; "device_verification_emoji_clock" = "時計"; "device_verification_emoji_hourglass" = "砂時計"; -"device_verification_emoji_umbrella" = "雨"; +"device_verification_emoji_umbrella" = "傘"; "device_verification_emoji_thumbs up" = "いいね"; "device_verification_emoji_spanner" = "スパナ"; "device_verification_emoji_santa" = "サンタ"; -"device_verification_emoji_glasses" = "メガネ"; -"device_verification_emoji_hat" = "ハット"; +"device_verification_emoji_glasses" = "めがね"; +"device_verification_emoji_hat" = "帽子"; "device_verification_emoji_robot" = "ロボット"; -"device_verification_emoji_smiley" = "笑顔"; +"device_verification_emoji_smiley" = "スマイル"; "device_verification_emoji_heart" = "ハート"; "device_verification_emoji_cake" = "ケーキ"; "device_verification_emoji_pizza" = "ピザ"; @@ -726,7 +726,7 @@ "room_accessiblity_scroll_to_bottom" = "いちばん下までスクロール"; "room_member_power_level_short_custom" = "カスタム"; "room_member_power_level_short_moderator" = "モデレーター"; -"room_member_power_level_custom_in" = "カスタム (%@) in %@"; +"room_member_power_level_custom_in" = "カスタム(%@):%@"; "room_member_power_level_short_admin" = "管理者"; "room_member_power_level_moderator_in" = "%@のモデレーター"; "room_member_power_level_admin_in" = "%@の管理者"; @@ -902,20 +902,20 @@ "key_verification_scan_confirmation_scanned_user_information" = "%@は同じシールドを表示していますか?"; // Scanned -"key_verification_scan_confirmation_scanned_title" = "まもなくです!"; +"key_verification_scan_confirmation_scanned_title" = "もう少しです!"; "key_verification_scan_confirmation_scanning_device_waiting_other" = "他の端末を待機しています…"; // MARK: Scan confirmation // Scanning -"key_verification_scan_confirmation_scanning_title" = "もう少しです。確認を待っています…"; +"key_verification_scan_confirmation_scanning_title" = "もう少しです!確認を待機しています…"; "key_verification_scan_confirmation_scanning_user_waiting_other" = "%@を待機しています…"; "key_verification_verify_qr_code_scan_other_code_success_message" = "QRコードを正常に検証しました。"; "key_verification_verify_qr_code_scan_other_code_success_title" = "コードを検証しました!"; "key_verification_verify_qr_code_other_scan_my_code_title" = "相手がQRコードを正常に読み取りましたか?"; -"key_verification_verify_qr_code_start_emoji_action" = "絵文字による認証"; +"key_verification_verify_qr_code_start_emoji_action" = "絵文字で認証"; "key_verification_verify_qr_code_cannot_scan_action" = "スキャンできませんか?"; -"key_verification_verify_qr_code_scan_code_action" = "コードをスキャン"; +"key_verification_verify_qr_code_scan_code_action" = "コードをスキャンしてください"; "key_verification_verify_qr_code_emoji_information" = "絵文字の並びを比較して認証。"; "key_verification_verify_qr_code_information_other_device" = "以下のコードをスキャンして認証してください:"; "key_verification_verify_qr_code_information" = "コードをスキャンして、お互いを安全に認証しましょう。"; @@ -931,13 +931,13 @@ "key_verification_tile_conclusion_done_title" = "認証済"; "key_verification_tile_request_incoming_approval_decline" = "拒否"; "key_verification_tile_request_incoming_approval_accept" = "承認"; -"key_verification_tile_request_status_accepted" = "あなたは承認しました"; +"key_verification_tile_request_status_accepted" = "承認しました"; "key_verification_tile_request_status_cancelled" = "%@はキャンセルしました"; -"key_verification_tile_request_status_cancelled_by_me" = "あなたはキャンセルしました"; +"key_verification_tile_request_status_cancelled_by_me" = "キャンセルしました"; "key_verification_tile_request_status_expired" = "期限切れ"; "key_verification_tile_request_status_waiting" = "待機しています…"; -"key_verification_tile_request_status_data_loading" = "日時を読み込み…"; -"key_verification_tile_request_outgoing_title" = "認証を送信済"; +"key_verification_tile_request_status_data_loading" = "日時を読み込んでいます…"; +"key_verification_tile_request_outgoing_title" = "認証を送信しました"; // Tiles @@ -951,7 +951,7 @@ // Generic errors -"error_invite_3pid_with_no_identity_server" = "メールで招待するために設定からIDサーバーを追加します。"; +"error_invite_3pid_with_no_identity_server" = "メールで招待するには、設定でIDサーバーを追加してください。"; // MARK: Reaction history "reaction_history_title" = "リアクションの履歴"; @@ -971,8 +971,8 @@ "file_upload_error_unsupported_file_type_message" = "ファイルの種類がサポートされていません。"; "device_verification_emoji_pin" = "ピン"; "device_verification_emoji_folder" = "フォルダー"; -"device_verification_emoji_headphones" = "ヘッドフォン"; -"device_verification_emoji_anchor" = "アンカー"; +"device_verification_emoji_headphones" = "ヘッドホン"; +"device_verification_emoji_anchor" = "いかり"; "device_verification_emoji_bell" = "ベル"; "device_verification_emoji_trumpet" = "トランペット"; "device_verification_emoji_guitar" = "ギター"; @@ -983,10 +983,10 @@ "device_verification_emoji_bicycle" = "自転車"; "device_verification_emoji_train" = "電車"; "device_verification_emoji_flag" = "旗"; -"device_verification_emoji_telephone" = "テレフォン"; -"device_verification_emoji_hammer" = "ハンマー"; +"device_verification_emoji_telephone" = "電話機"; +"device_verification_emoji_hammer" = "金槌"; "device_verification_emoji_key" = "鍵"; -"device_verification_emoji_lock" = "錠"; +"device_verification_emoji_lock" = "錠前"; "settings_three_pids_management_information_part1" = "ログインやアカウントの回復に使用できるメールアドレスや電話番号をここで管理。あなたを見つけられる人を "; "settings_identity_server_settings" = "IDサーバー"; "external_link_confirmation_title" = "このリンクを再確認してください"; @@ -1847,7 +1847,7 @@ "settings_presence_offline_mode" = "オフラインモード"; "settings_enable_room_message_bubbles" = "吹き出しでメッセージを表示"; "settings_discovery_accept_terms" = "IDサーバーの利用規約を承諾"; -"settings_labs_confirm_crypto_sdk" = "このオプションは、新しく、より高速で安定性のあるエンドツーエンド暗号化(Rustで記述)を有効にします。無効にするにはログアウトが必要となります。続行してよろしいですか?"; +"settings_labs_confirm_crypto_sdk" = "この機能は実験段階のため、予期したとおりに機能せず、意図しない結果を引き起こす可能性があります。この機能を無効にするには、ログアウトして再度ログインしてください。自分自身の判断で慎重に使ってください。"; "settings_labs_enable_voice_broadcast" = "音声配信"; "settings_labs_enable_new_app_layout" = "アプリケーションの新しいレイアウト"; "settings_labs_enable_new_client_info_feature" = "クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定"; @@ -2600,8 +2600,8 @@ "wysiwyg_composer_format_action_code_block" = "コードブロックの表示を切り替える"; "wysiwyg_composer_format_action_quote" = "引用の表示を切り替える"; "poll_timeline_reply_ended_poll" = "終了したアンケート"; -"settings_labs_enable_crypto_sdk" = "エンドツーエンド暗号化2.0"; -"settings_labs_disable_crypto_sdk" = "エンドツーエンド暗号化2.0(無効にするにはログアウトしてください)"; +"settings_labs_enable_crypto_sdk" = "Rust エンドツーエンド暗号化"; +"settings_labs_disable_crypto_sdk" = "Rust エンドツーエンド暗号化(無効にするにはログアウトしてください)"; // MARK: - Launch loading From e85fbf6ef6cba8743181a3c649f22282112aba8f Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 3 Feb 2023 05:58:20 +0000 Subject: [PATCH 442/468] Translated using Weblate (German) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index c9ee49d16..dc6a2b333 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2720,9 +2720,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migriere Daten\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Ende-zu-Ende-Verschlüsselung 2.0 (zum Deaktivieren abmelden)"; -"settings_labs_confirm_crypto_sdk" = "Diese Option wird eine neue, schnellere und zuverlässigere Ende-zu-Ende-Verschlüsselungs-Engine aktivieren, die in Rust geschrieben wurde. Einmal aktiviert, wirst du dich abmelden müssen, um sie zu deaktivieren. Möchtest du fortfahren?"; -"settings_labs_enable_crypto_sdk" = "Ende-zu-Ende-Verschlüsselung 2.0"; +"settings_labs_disable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung (zum Deaktivieren abmelden)"; +"settings_labs_confirm_crypto_sdk" = "Bitte beachte, dass diese Funktion noch experimentell ist, womöglich nicht wie erwartet funktioniert und unerwünschte Nebeneffekte haben kann. Melde dich zum deaktivieren einfach ab und erneut an. Nutze diese Funktion nach eigenem Ermessen und mit Vorsicht."; +"settings_labs_enable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung"; "poll_history_no_past_poll_period_text" = "Für die vergangenen %@ Tage sind keine beendeten Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; "poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen"; "poll_history_load_more" = "Weitere Umfragen laden"; From beb86ba21361a721483980043750c8bf2dd9c0a7 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Fri, 3 Feb 2023 08:59:43 +0000 Subject: [PATCH 443/468] Translated using Weblate (Japanese) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index f64ab7b57..00648ebc0 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -569,7 +569,7 @@ "create_room_show_in_directory" = "ルーム一覧に掲載"; "create_room_section_footer_type" = "非公開のルームは、ルームに招待された人のみ参加できます。"; "create_room_type_public" = "公開ルーム(誰でも参加可能)"; -"create_room_type_private" = "非公開ルーム (招待者のみ参加可能)"; +"create_room_type_private" = "非公開のルーム(招待者のみ参加可能)"; "create_room_section_header_type" = "アクセスできる人"; "create_room_section_footer_encryption" = "暗号化はあとから無効にすることはできません。"; "create_room_section_header_encryption" = "暗号化"; @@ -588,7 +588,7 @@ // Mark: - Room creation introduction cell -"room_intro_cell_add_participants_action" = "参加者を追加"; +"room_intro_cell_add_participants_action" = "連絡先を追加"; "room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; "room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンドで暗号化されていません。"; "room_intro_cell_information_dm_sentence1_part3" = "とのダイレクトメッセージの始まりです。 "; @@ -1607,7 +1607,7 @@ // MARK: - Share invite link "share_invite_link_action" = "招待リンクを共有"; -"room_intro_cell_information_room_with_topic_sentence2" = "トピック: %@"; +"room_intro_cell_information_room_with_topic_sentence2" = "トピック:%@"; "room_intro_cell_information_room_sentence1_part3" = "の始まりです。 "; "room_intro_cell_information_room_sentence1_part1" = "ここが "; "room_intro_cell_information_dm_sentence1_part1" = "ここが "; @@ -1642,7 +1642,7 @@ "stop" = "停止"; "spaces_creation_post_process_creating_space_task" = "%@を作成しています"; "side_menu_coach_message" = "右にスワイプまたはタップで全てのルームが表示されます"; -"spaces_creation_post_process_creating_space" = "スペースを作成中"; +"spaces_creation_post_process_creating_space" = "スペースを作成しています"; "spaces_creation_add_rooms_message" = "このスペースはあなた専用のため、他の人に通知されることはありません。この設定は後から変更できます。"; "spaces_creation_add_rooms_title" = "どれを追加しますか?"; "spaces_creation_sharing_type_me_and_teammates_detail" = "あなたとチームメイトの非公開のスペース"; @@ -1689,7 +1689,7 @@ // MARK: - Call Transfer "call_transfer_title" = "転送"; -"room_info_back_button_title" = "ルーム情報"; +"room_info_back_button_title" = "ルームの情報"; "create_room_processing" = "ルームを作成しています"; "call_transfer_users" = "ユーザー"; "home_context_menu_notifications" = "通知"; @@ -1706,16 +1706,16 @@ "leave_space_action" = "スペースから退出"; "leave_space_selection_title" = "ルームを選択"; "create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; -"create_room_suggest_room" = "スペースメンバーにおすすめ"; +"create_room_suggest_room" = "スペースのメンバーへのおすすめ"; "create_room_show_in_directory_footer" = "他の人が検索・参加できるようになります。"; "create_room_promotion_header" = "プロモート"; "searchable_directory_search_placeholder" = "名前または ID"; "room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; -"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースのメンバーに参加を推奨するものとしてPRされます。"; +"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; // Room suggestion Settings "room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; -"room_details_promote_room_suggest_title" = "スペースメンバーへのおすすめ"; +"room_details_promote_room_suggest_title" = "スペースのメンバーへのおすすめ"; "settings_default" = "既定の通知"; "pin_protection_reset_alert_action_reset" = "リセット"; "authentication_recaptcha_title" = "あなたは人間ですか?"; @@ -1746,7 +1746,7 @@ "room_access_settings_screen_upgrade_required" = "アップグレードが必要"; "room_access_settings_screen_upgrade_alert_title" = "ルームをアップグレード"; "room_access_settings_screen_public_message" = "誰でも検索・参加できます。"; -"room_access_settings_screen_private_message" = "招待された人だけが検索・参加できます。"; +"room_access_settings_screen_private_message" = "招待された人のみ検索・参加できます。"; "room_access_settings_screen_message" = "誰が%@を検索・参加できるか選択してください。"; "space_settings_access_section" = "このスペースにアクセスできる人は?"; "room_access_settings_screen_title" = "このルームにアクセスできる人は?"; @@ -2626,7 +2626,7 @@ "event_formatter_call_active_voice" = "実行中の音声通話"; "launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)"; "create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; -"create_room_section_footer_type_public" = "スペース名にあるだけでなく、招待された連絡先のみが検索し、参加できます。"; +"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索・参加できます。"; "searchable_directory_x_network" = "%@ネットワーク"; "pin_protection_explanatory" = "PINコードを設定すると、メッセージや連絡先などのデータを保護できます。アプリの開始時にPINコードを入力するよう要求します。"; "secrets_recovery_with_key_information_default" = "セキュリティーキーを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; @@ -2673,7 +2673,7 @@ // MARK: - Dial Pad "dialpad_title" = "ダイヤルパッド"; -"create_room_type_restricted" = "スペースの参加者"; +"create_room_type_restricted" = "スペースのメンバー"; "biometrics_cant_unlocked_alert_message_login" = "再ログイン"; "biometrics_cant_unlocked_alert_message_x" = "ロックを解除するには、%@を使用するか、再ログインして%@を有効にしてください"; "biometrics_setup_subtitle" = "時間を節約"; @@ -2684,7 +2684,7 @@ "pin_protection_settings_change_pin" = "PINコードを変更"; "pin_protection_settings_enabled_forced" = "PINコードが有効です"; "pin_protection_settings_section_footer" = "PINコードを再設定するには、再ログインして新しいコードを作成してください。"; -"pin_protection_mismatch_too_many_times_error_message" = "PINコードを覚えていない場合は「PINコードを忘れました」をタップしてください。"; +"pin_protection_mismatch_too_many_times_error_message" = "PINコードを覚えていない場合は「PINコードを忘れました」のボタンをタップしてください。"; "pin_protection_mismatch_error_message" = "もう一度やり直してください"; "pin_protection_mismatch_error_title" = "PINコードが一致しません"; "pin_protection_reset_alert_message" = "PINコードを再設定するには、再ログインして新しいコードを作成してください"; @@ -2699,7 +2699,7 @@ "major_update_title" = "Riotは%@になりました"; "secrets_reset_authentication_message" = "承認するにはMatrixのアカウントのパスワードを入力してください"; -"secrets_setup_recovery_passphrase_summary_information" = "セキュリティーフレーズを記憶。セキュリティーフレーズを使うと、暗号化したメッセージやデータのロックを解除することができます。"; +"secrets_setup_recovery_passphrase_summary_information" = "セキュリティーフレーズを記録してください。セキュリティーフレーズを使うと、暗号化したメッセージやデータのロックを解除することができます。"; "secrets_setup_recovery_key_storage_alert_message" = "✓ 印刷して安全な場所で保管\n✓ USBキーやバックアップ用ドライブに保存\n✓ 個人用のクラウドストレージにコピー"; "secrets_setup_recovery_key_information" = "セキュリティーキーは安全な場所で保管してください。セキュリティーキーを使うと、暗号化したメッセージやデータのロックを解除することができます。"; "secrets_recovery_with_key_invalid_recovery_key_message" = "正しいセキュリティーキーを入力したことを確認してください。"; @@ -2814,7 +2814,7 @@ "space_beta_announce_subtitle" = "コミュニティー機能の新しいバージョン"; "space_invite_not_enough_permission" = "このスペースにユーザーを招待する権限がありません"; "room_invite_not_enough_permission" = "このルームにユーザーを招待する権限がありません"; -"room_invite_to_room_option_detail" = "%@には所属しません。"; +"room_invite_to_room_option_detail" = "%@のメンバーにはなりません。"; "room_invite_to_space_option_detail" = "%@を探索することはできますが、%@のメンバーにはなりません。"; // MARK: - Room invite From 147b8cc69667136991fcde693a65bd947380c937 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 3 Feb 2023 08:45:41 +0000 Subject: [PATCH 444/468] Translated using Weblate (Italian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index a88a64ded..8d1cc83f0 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2690,12 +2690,14 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migrazione dati\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end 2.0 (disconnettiti per disattivarla)"; -"settings_labs_confirm_crypto_sdk" = "Questa opzione attiverà un motore nuovo, più veloce e affidabile per la crittografia end-to-end scritto in Rust. Una volta attivato, dovrai disconnetterti per disattivarlo. Vuoi procedere?"; -"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end 2.0"; +"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end Rust (disconnettiti per disattivarla)"; +"settings_labs_confirm_crypto_sdk" = "Si noti che questa funzione, essendo ancora in fase sperimentale, potrebbe non funzionare come previsto e potrebbe avere conseguenze indesiderate. Per disattivare la funzione, è sufficiente disconnettersi e riaccedere. Utilizzare a propria discrezione e con cautela."; +"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end Rust"; "wysiwyg_composer_format_action_un_indent" = "Diminuisci indentazione"; "wysiwyg_composer_format_action_indent" = "Aumenta indentazione"; "poll_history_fetching_error" = "Errore di recupero dei sondaggi."; "voice_broadcast_playback_unable_to_decrypt" = "Impossibile decifrare questa trasmissione vocale."; "home_context_menu_mark_as_unread" = "Segna come non letto"; "key_backup_recover_from_private_key_progress" = "%@%% Completato"; +"poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale"; +"settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione."; From 49a355e1f7db0d54c6c3b8e715b85e0f6d08e5a4 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 2 Feb 2023 19:39:07 +0000 Subject: [PATCH 445/468] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 7cf724608..cc3bd8b93 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2911,9 +2911,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Перенесення даних\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування 2.0 (вийдіть, щоб вимкнути)"; -"settings_labs_confirm_crypto_sdk" = "Ця опція увімкне новий, швидший і надійніший механізм наскрізного шифрування, написаний на Rust. Після увімкнення вам потрібно буде вийти з системи, щоб вимкнути її. Бажаєте продовжити?"; -"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування 2.0"; +"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування Rust (вийдіть, щоб вимкнути)"; +"settings_labs_confirm_crypto_sdk" = "Зауважте, що оскільки ця функція досі перебуває на стадії експерименту, вона може працювати не так, як очікується, і може мати непередбачувані наслідки. Щоб вимкнути цю функцію, просто вийдіть з системи та увійдіть знову. Використовуйте на власний розсуд і з обережністю."; +"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування Rust"; "poll_history_load_more" = "Завантажити більше опитувань"; "poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; "poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці"; From 389c9da251b7ae6511730cb6b4b3e7c5c00ccb0c Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 2 Feb 2023 23:59:17 +0000 Subject: [PATCH 446/468] Translated using Weblate (Indonesian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index b0f0f77be..0120b2b5a 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2913,9 +2913,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Memigrasikan data\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung 2,0 (keluar dari akun untuk menonaktifkan)"; -"settings_labs_confirm_crypto_sdk" = "Opsi ini akan mengaktifkan mesin ditulis dalam Rust baru yang lebih cepat dan lebih andal untuk enkripsi ujung ke ujung. Setelah diaktifkan, Anda harus keluar dari akun untuk menonaktifkannya. Apakah Anda ingin melanjutkan?"; -"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung 2,0"; +"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung Rust (keluar dari akun untuk menonaktifkan)"; +"settings_labs_confirm_crypto_sdk" = "Ketahui bahwa fitur ini masih dalam masa eksperimental, ini mungkin tidak berfungsi seperti yang diharapkan dan dapat memiliki konsekuensi yang tidak terduga. Untuk mengembalikan fitur, cukup keluar dari akun dan masuk kembali ke akun. Gunakan dengan pengetahuan dan risiko Anda."; +"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung Rust"; "poll_history_load_more" = "Muat lebih banyak pemungutan suara"; "poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya"; "poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya"; From d999bfb90b3128eab7583704d2408cc330e97535 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 2 Feb 2023 21:38:08 +0000 Subject: [PATCH 447/468] Translated using Weblate (Slovak) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 18c4079b5..0aace81cc 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2909,9 +2909,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Migrácia údajov\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "End-to-end šifrovanie 2.0 (odhláste sa, aby ste ho vypli)"; -"settings_labs_confirm_crypto_sdk" = "Táto možnosť umožní použitie nového, rýchlejšieho a spoľahlivejšieho nástroja na end-to-end šifrovanie napísaného v jazyku Rust. Po jeho zapnutí sa budete musieť odhlásiť, aby ste ho mohli vypnúť. Chcete pokračovať?"; -"settings_labs_enable_crypto_sdk" = "End-to-end šifrovanie 2.0"; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end šifrovanie (odhláste sa, aby ste ho vypli)"; +"settings_labs_confirm_crypto_sdk" = "Upozorňujeme, že táto funkcia je stále v experimentálnej fáze, preto nemusí fungovať podľa očakávaní a môže mať potenciálne nezamýšľané dôsledky. Ak chcete funkciu vrátiť späť, jednoducho sa odhláste a znova prihláste. Používajte ju podľa vlastného uváženia a s opatrnosťou."; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end šifrovanie"; "poll_history_load_more" = "Načítať ďalšie ankety"; "poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; "poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace"; From 8cd0dd38ffa175255edda244f4b35ee5fddba273 Mon Sep 17 00:00:00 2001 From: phardyle Date: Sat, 4 Feb 2023 00:10:02 +0000 Subject: [PATCH 448/468] Translated using Weblate (Chinese (Simplified)) Currently translated at 83.0% (1975 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index a503d920b..69d2d7cb9 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1044,15 +1044,15 @@ "key_verification_bootstrap_not_setup_title" = "错误"; "key_verification_bootstrap_not_setup_message" = "您需要先启动交叉签名。"; "key_verification_verify_qr_code_title" = "通过扫描进行验证"; -"key_verification_verify_qr_code_information" = "扫描代码以安全地相互验证。"; -"key_verification_verify_qr_code_information_other_device" = "扫描以下代码以验证:"; +"key_verification_verify_qr_code_information" = "扫描条码以安全地相互验证。"; +"key_verification_verify_qr_code_information_other_device" = "扫描以下条码以验证:"; "key_verification_verify_qr_code_emoji_information" = "通过比较唯一的表情符号进行验证。"; -"key_verification_verify_qr_code_scan_code_action" = "扫描他们的代码"; +"key_verification_verify_qr_code_scan_code_action" = "扫描他们的条码"; "key_verification_verify_qr_code_cannot_scan_action" = "不能扫描吗?"; "key_verification_verify_qr_code_start_emoji_action" = "通过表情符号验证"; -"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了二维码?"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了QR码?"; "key_verification_verify_qr_code_scan_other_code_success_title" = "代码已验证!"; -"key_verification_verify_qr_code_scan_other_code_success_message" = "二维码已成功验证。"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR码已成功验证。"; // Scanning "key_verification_scan_confirmation_scanning_title" = "快好了!正在等待确认…"; "key_verification_scan_confirmation_scanning_user_waiting_other" = "等待中%@…"; @@ -2259,23 +2259,23 @@ "authentication_qr_login_failure_retry" = "再试一次"; "authentication_qr_login_failure_request_timed_out" = "连接没有在规定的时间内完成。"; "authentication_qr_login_failure_request_denied" = "请求在另一个设备上被拒绝。"; -"authentication_qr_login_failure_invalid_qr" = "二维码无效。"; +"authentication_qr_login_failure_invalid_qr" = "QR码无效。"; "authentication_qr_login_failure_title" = "连接失败"; "authentication_qr_login_loading_signed_in" = "您现在已经登录到另一个设备上。"; -"authentication_qr_login_loading_waiting_signin" = "等待设备登录。"; -"authentication_qr_login_loading_connecting_device" = "连接到设备"; -"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您帐户的完全访问权限。"; -"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:"; -"authentication_qr_login_confirm_title" = "建立安全连接"; -"authentication_qr_login_scan_subtitle" = "将二维码放置在下面的方框中"; -"authentication_qr_login_scan_title" = "扫描二维码"; -"authentication_qr_login_display_step2" = "选择“以二维码登入”"; +"authentication_qr_login_loading_waiting_signin" = "正在等待设备以登录。"; +"authentication_qr_login_loading_connecting_device" = "正在连接到设备"; +"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您账户的完全访问权限。"; +"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:"; +"authentication_qr_login_confirm_title" = "安全连接已建立"; +"authentication_qr_login_scan_subtitle" = "将QR码放置在下面的方框中"; +"authentication_qr_login_scan_title" = "扫描QR码"; +"authentication_qr_login_display_step2" = "选择“以QR码登入”"; "authentication_qr_login_display_step1" = "在您的其它设备中打开Element"; "onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。"; "user_session_learn_more" = "了解更多"; "manage_session_name_info_link" = "了解更多"; "threads_beta_information_link" = "了解更多"; -"authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的二维码。"; +"authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的QR码。"; "room_invite_to_space_option_detail" = "他们可以探索 %@,但不会成为 %@ 的成员。"; "analytics_prompt_message_new_user" = "通过分享匿名的使用数据,帮助我们识别问题并改进 %@ 。为了了解人们如何使用多个设备,我们将生成一个随机的标识符,由你的设备共享。"; "threads_notice_done" = "知道了"; From b249cdc5ec59712eca10db0cb7ecc03c556a16bc Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Mon, 6 Feb 2023 13:32:22 +0000 Subject: [PATCH 449/468] Translated using Weblate (Japanese) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 364 ++++++++++++++-------------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 00648ebc0..efb3b3776 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -21,7 +21,7 @@ "save" = "保存"; "join" = "参加"; "decline" = "拒否"; -"accept" = "受諾"; +"accept" = "同意"; "preview" = "プレビュー"; "camera" = "カメラ"; "voice" = "音声"; @@ -55,8 +55,8 @@ "auth_invalid_login_param" = "ユーザー名とパスワードの一方あるいは両方が正しくありません"; "auth_invalid_user_name" = "ユーザー名には半角英数字、ドット、ハイフン、アンダースコアのみを使用してください"; "auth_invalid_password" = "パスワードが短すぎます(最小6文字)"; -"auth_invalid_email" = "メールアドレスの形式が正しくないようです"; -"auth_invalid_phone" = "電話番号の形式が正しくないようです"; +"auth_invalid_email" = "メールアドレスの形式が正しくありません"; +"auth_invalid_phone" = "電話番号の形式が正しくありません"; "auth_missing_password" = "パスワードが入力されていません"; "auth_add_email_message" = "電子メールアドレスを登録すると, 誰かがあなたを検索をしたり, パスワード紛失時に初期化のメールを送ることができます."; "auth_add_phone_message" = "電話番号を登録すると, 誰かがあなたを電話番号で検索できるようになります."; @@ -83,7 +83,7 @@ "auth_reset_password_missing_password" = "新しいパスワードの入力が必要です。"; "auth_reset_password_email_validation_message" = "%@ へ電子メールを送信しました。電子メール内のリンクを開いた後、以下をクリックしてください。"; "auth_reset_password_next_step_button" = "メールアドレスを確認しました"; -"auth_reset_password_error_unauthorized" = "メールアドレスの認証に失敗しました:電子メール内のリンクを開いたことを確認してください"; +"auth_reset_password_error_unauthorized" = "メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください"; "auth_reset_password_error_not_found" = "あなたのメールアドレスは、このホームサーバー上のMatrix IDと関連付けられていないようです。"; "auth_reset_password_success_message" = "あなたのMatrixのアカウントのパスワードは初期化されました。\n\n全てのセッションからログアウトしたため、プッシュ通知は送信されません。通知を再度有効にするには、各端末で再度ログインしてください。"; "auth_add_email_and_phone_warning" = "電子メールと電話番号の両方による登録は、まだサポートしていません。電話番号のみでの登録を受け付けています。メールアドレスは、設定内のプロフィールから後ほど追加できます。"; @@ -173,7 +173,7 @@ "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; "room_participants_action_leave" = "このルームから退出"; -"room_participants_action_remove" = "このルームから削除"; +"room_participants_action_remove" = "このルームから追放"; "room_participants_action_ban" = "このルームからブロック"; "room_participants_action_unban" = "ブロックを解除"; "room_participants_action_ignore" = "このユーザーのメッセージを全て非表示にする"; @@ -212,9 +212,9 @@ "room_event_action_more" = "その他"; "room_event_action_share" = "共有"; "room_event_action_permalink" = "メッセージへのリンクをコピー"; -"room_event_action_view_source" = "ソースを表示"; -"room_event_action_report" = "内容を報告"; -"room_event_action_report_prompt_reason" = "この内容を報告する理由"; +"room_event_action_view_source" = "ソースコードを表示"; +"room_event_action_report" = "コンテンツを報告"; +"room_event_action_report_prompt_reason" = "このコンテンツを報告する理由"; "room_event_action_report_prompt_ignore_user" = "このユーザーからの全ての発言を非表示にしますか?"; "room_event_action_save" = "保存"; "room_event_action_resend" = "再送信"; @@ -305,7 +305,7 @@ "settings_copyright" = "著作権"; "settings_term_conditions" = "利用規約"; "settings_privacy_policy" = "プライバシーポリシー"; -"settings_third_party_notices" = "外部ライブラリーの規約"; +"settings_third_party_notices" = "外部ライブラリーのライセンス"; "settings_send_crash_report" = "匿名利用状況と誤動作情報を送信"; "settings_enable_rageshake" = "端末を振って不具合を報告"; "settings_clear_cache" = "キャッシュを消去"; @@ -386,13 +386,13 @@ "media_picker_select" = "選択"; // Directory "directory_title" = "ルーム一覧"; -"directory_server_picker_title" = "ルーム一覧を選択"; -"directory_server_all_rooms" = "%@ サーバー上の全てのルーム"; +"directory_server_picker_title" = "ルームディレクトリーを選択"; +"directory_server_all_rooms" = "%@サーバー上の全てのルーム"; "directory_server_all_native_rooms" = "全てのMatrix連携ルーム"; "directory_server_type_homeserver" = "公開ルームの一覧を表示するホームサーバーを入力してください"; "directory_server_placeholder" = "matrix.org"; // Events formatter -"event_formatter_member_updates" = "%tu個の権限の変更"; +"event_formatter_member_updates" = "%tu個のメンバーシップの変更"; "event_formatter_widget_added" = "%@のウィジェットが%@により追加されました"; "event_formatter_widget_removed" = "%@のウィジェットが%@により削除されました"; "event_formatter_jitsi_widget_added" = "VoIP会議が%@により追加されました"; @@ -447,7 +447,7 @@ "widget_integration_room_not_recognised" = "このルームでは認められません。"; "widget_integration_positive_power_level" = "権限の数値は正の整数で入力してください。"; "widget_integration_must_be_in_room" = "あなたはこのルームに所属していません。"; -"widget_integration_no_permission_in_room" = "あなたはこのルームで権限がありません。"; +"widget_integration_no_permission_in_room" = "このルームでそれを行う権限がありません。"; "widget_integration_missing_room_id" = "リクエストにroom_idがありません。"; "widget_integration_missing_user_id" = "リクエストにuser_idがありません。"; "widget_integration_room_not_visible" = "ルーム %@ は見えません。"; @@ -466,7 +466,7 @@ "room_do_not_have_permission_to_post" = "このルームに投稿する権限がありません"; "encrypted_room_message_reply_to_placeholder" = "暗号化された返信を送る…"; "room_message_reply_to_short_placeholder" = "返信を送る…"; -"room_event_action_view_decrypted_source" = "復号化されたソースを表示"; +"room_event_action_view_decrypted_source" = "復号化されたソースコードを表示"; "room_event_action_kick_prompt_reason" = "このユーザーを追放する理由"; "room_action_send_photo_or_video" = "写真または動画を送信"; "room_action_send_sticker" = "ステッカーを送信"; @@ -518,12 +518,12 @@ "event_formatter_rerequest_keys_part1_link" = "暗号鍵を再要求"; "event_formatter_rerequest_keys_part2" = " あなたの他のセッションに。"; "homeserver_connection_lost" = "ホームサーバーに接続できませんでした。"; -"widget_sticker_picker_no_stickerpacks_alert" = "現在、ステッカーパックを有効にしていません。"; +"widget_sticker_picker_no_stickerpacks_alert" = "現在、ステッカーパックが有効になっていません。"; "widget_sticker_picker_no_stickerpacks_alert_add_now" = "今すぐ追加しますか?"; // Room key request dialog "e2e_room_key_request_title" = "暗号鍵の要求"; -"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション '%@' を追加しました。"; -"e2e_room_key_request_message" = "未認証のセッション '%@' が暗号鍵を要求しています。"; +"e2e_room_key_request_message_new_device" = "暗号鍵を要求している新しいセッション'%@'を追加しました。"; +"e2e_room_key_request_message" = "未認証のセッション'%@'が暗号鍵を要求しています。"; "e2e_room_key_request_start_verification" = "認証を開始…"; "e2e_room_key_request_share_without_verifying" = "認証せず共有"; "e2e_room_key_request_ignore_request" = "要求を無視"; @@ -531,11 +531,11 @@ "gdpr_consent_not_given_alert_message" = "%@のホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; "gdpr_consent_not_given_alert_review_now_action" = "確認"; "deactivate_account_title" = "アカウントを無効化"; -"deactivate_account_informations_part1" = "これにより、アカウントは永久に使用できなくなります。ログインしたり同じユーザーIDを再登録したりすることはできません。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; -"deactivate_account_informations_part2_emphasize" = "この動作は元に戻せません。"; -"deactivate_account_informations_part3" = "\n\nアカウントを無効化しても "; -"deactivate_account_informations_part4_emphasize" = "既定ではあなたが送信したメッセージは消去されません。 "; -"deactivate_account_informations_part5" = "メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様です。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; +"deactivate_account_informations_part1" = "この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; +"deactivate_account_informations_part2_emphasize" = "この操作は取り消せません。"; +"deactivate_account_informations_part3" = "\n\nアカウントを無効化しても、 "; +"deactivate_account_informations_part4_emphasize" = "デフォルトではあなたが送信したメッセージの履歴は消去されません。 "; +"deactivate_account_informations_part5" = "メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。\n\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。"; "deactivate_account_forget_messages_information_part1" = "アカウントを無効化する際、全ての送信済のメッセージを消去("; "deactivate_account_forget_messages_information_part2_emphasize" = "警告"; "deactivate_account_forget_messages_information_part3" = ":今後のユーザーには、不完全な会話が表示されます)"; @@ -544,7 +544,7 @@ "deactivate_account_password_alert_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "要求を送信しました"; -"rerequest_keys_alert_message" = "鍵をこのセッションに送信するために、メッセージを復号化できる他の端末で%@を起動してください。"; +"rerequest_keys_alert_message" = "鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で%@を起動してください。"; "room_event_action_ban_prompt_reason" = "このユーザーをブロックする理由"; "room_resource_limit_exceeded_message_contact_1" = " お願い "; "settings_ui_theme_black" = "ブラック"; @@ -622,7 +622,7 @@ "widget_picker_manage_integrations" = "インテグレーションを管理…"; // Widget Picker -"widget_picker_title" = "インテグレーション"; +"widget_picker_title" = "インテグレーション(統合)"; "widget_integration_manager_disabled" = "設定でインテグレーションマネージャーを有効にする必要があります"; "widget_menu_remove" = "全員から削除"; "widget_menu_revoke_permission" = "アクセスを取り消す"; @@ -676,7 +676,7 @@ "room_details_access_section_anyone_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを含む)"; "room_details_access_section_for_dm" = "これにアクセスできる人は?"; "room_details_photo_for_dm" = "画像"; -"room_details_integrations" = "インテグレーション"; +"room_details_integrations" = "インテグレーション(統合)"; "room_details_search" = "ルーム内検索"; "room_details_title_for_dm" = "詳細"; "identity_server_settings_alert_error_invalid_identity_server" = "%@は有効なIDサーバーではありません。"; @@ -695,12 +695,12 @@ "identity_server_settings_change" = "変更"; "identity_server_settings_add" = "追加"; "identity_server_settings_place_holder" = "IDサーバーを入力"; -"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。あなたの知っている連絡先を見つけたり、その連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; +"identity_server_settings_no_is_description" = "現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; "identity_server_settings_description" = "現在 %@ を使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。"; "security_settings_complete_security_alert_title" = "セキュリティーを確認"; "security_settings_crosssigning_complete_security" = "セキュリティーを確認"; "security_settings_crosssigning_bootstrap" = "設定"; -"settings_devices_description" = "セッションの公開名は、あなたとやり取りする人々に対して表示されます"; +"settings_devices_description" = "セッションの公開名は、あなたとやり取りする連絡先に対して表示されます"; "settings_key_backup_delete_confirmation_prompt_title" = "バックアップを削除"; "settings_key_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "settings_key_backup_info_algorithm" = "アルゴリズム:%@"; @@ -710,7 +710,7 @@ "settings_add_3pid_password_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; "settings_add_3pid_invalid_password_message" = "認証情報が正しくありません"; "settings_add_3pid_password_title_email" = "メールアドレスを追加"; -"settings_integrations_allow_description" = "インテグレーションマネージャー %@ を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理。\n\n設定データを受け取り、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。"; +"settings_integrations_allow_description" = "インテグレーションマネージャー %@ を使用すると、ボット、ブリッジ、ウィジェット、ステッカーパックを管理できます。\n\n設定データを受信し、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。"; "settings_integrations_allow_button" = "インテグレーションを管理"; "settings_calls_stun_server_fallback_button" = "フォールバック用の通話アシストサーバーを許可"; "settings_key_backup" = "鍵のバックアップ"; @@ -721,7 +721,7 @@ "room_event_action_delete_confirmation_title" = "未送信のメッセージを削除"; "room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; "room_unsent_messages_cancel_title" = "未送信のメッセージを削除"; -"room_message_replying_to" = "%@に返信中"; +"room_message_replying_to" = "%@に返信しています"; "room_message_editing" = "編集中"; "room_accessiblity_scroll_to_bottom" = "いちばん下までスクロール"; "room_member_power_level_short_custom" = "カスタム"; @@ -776,7 +776,7 @@ "auth_softlogout_sign_in" = "サインイン"; "auth_softlogout_signed_out" = "サインアウトしました"; "auth_autodiscover_invalid_response" = "ホームサーバーのディスカバリー(発見)に関する不正な応答です"; -"auth_accept_policies" = "このホームサーバーの運営方針を確認して承諾してください:"; +"auth_accept_policies" = "このホームサーバーの運営方針を確認し、同意してください:"; "auth_reset_password_error_is_required" = "IDサーバーが設定されていません:Matrixのアカウントのパスワードを再設定するためにサーバーオプションに追加してください。"; "auth_forgot_password_error_no_configured_identity_server" = "IDサーバーが設定されていません:パスワードを再設定するためにIDサーバーを追加してください。"; "auth_phone_is_required" = "IDサーバーが設定されていないため、Matrixアカウントのパスワードの再設定に使用する電話番号を追加することができません。"; @@ -829,7 +829,7 @@ // Security settings "security_settings_title" = "セキュリティー"; "settings_show_NSFW_public_rooms" = "NSFWパブリックルームを表示"; -"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。自分の連絡先を見つけたり、連絡先から見つけてもらったりするには、以上にIDサーバーを追加してください。"; +"settings_identity_server_no_is_description" = "現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以上でIDサーバーを追加してください。"; "settings_identity_server_no_is" = "IDサーバーが設定されていません"; "settings_identity_server_description" = "上記で設定したIDサーバーを使うと、自分の連絡先を見つけたり、連絡先から見つけてもらったりすることができます。"; "settings_discovery_three_pid_details_enter_sms_code_action" = "SMSアクティベーションコードを入力"; @@ -845,7 +845,7 @@ "settings_discovery_three_pids_management_information_part2" = "ユーザー設定"; "settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストに、メールアドレスや電話番号を追加したり、削除したりすることができます。 "; "settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー %@ の利用規約への同意が必要です。"; -"settings_discovery_no_identity_server" = "現在IDサーバーを使用していません。あなたの知っている連絡先から見つけてもらえるようにするには、IDサーバーを追加してください。"; +"settings_discovery_no_identity_server" = "現在、IDサーバーを使用していません。連絡先から見つけてもらうようにするには、IDサーバーを追加してください。"; "settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを読み取れなくなってしまいます。"; "settings_key_backup_button_connect" = "このセッションを鍵のバックアップに接続"; "settings_key_backup_button_delete" = "バックアップを削除"; @@ -865,7 +865,7 @@ "settings_labs_message_reaction" = "絵文字でメッセージに反応"; "settings_security" = "セキュリティー"; "settings_three_pids_management_information_part3" = "で設定しましょう。"; -"settings_three_pids_management_information_part2" = "ディスカバリー"; +"settings_three_pids_management_information_part2" = "ディスカバリー(発見)"; "store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールできます。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化と、クロス署名による認証で、あなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrix――オープンな分散型通信の標準規格――で動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有:データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション:Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全:本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の端末を認証するためのクロス署名を行います。\n\n包括的なコミュニケーション:メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても:全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " が新しいセッションを使ってサインインしました:"; @@ -930,7 +930,7 @@ "key_verification_tile_conclusion_warning_title" = "信頼されていないサインイン"; "key_verification_tile_conclusion_done_title" = "認証済"; "key_verification_tile_request_incoming_approval_decline" = "拒否"; -"key_verification_tile_request_incoming_approval_accept" = "承認"; +"key_verification_tile_request_incoming_approval_accept" = "同意"; "key_verification_tile_request_status_accepted" = "承認しました"; "key_verification_tile_request_status_cancelled" = "%@はキャンセルしました"; "key_verification_tile_request_status_cancelled_by_me" = "キャンセルしました"; @@ -964,7 +964,7 @@ "emoji_picker_people_category" = "表情と人々"; // MARK: Emoji picker -"emoji_picker_title" = "ピッカー"; +"emoji_picker_title" = "リアクション"; // MARK: File upload "file_upload_error_title" = "ファイルのアップロードエラー"; @@ -1054,7 +1054,7 @@ "settings_group_messages" = "グループメッセージ"; "settings_encrypted_direct_messages" = "暗号化されたダイレクトメッセージ"; "settings_direct_messages" = "ダイレクトメッセージ"; -"settings_notify_me_for" = "以下がメッセージに含まれる場合に通知"; +"settings_notify_me_for" = "以下の場合に通知"; "settings_phone_contacts" = "端末の連絡先"; "settings_notifications" = "通知"; "settings_links" = "リンク"; @@ -1105,13 +1105,13 @@ "secrets_setup_recovery_passphrase_validate_action" = "完了"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "バックアップ"; "room_event_action_forward" = "転送"; -"room_event_action_view_in_room" = "ルームに表示"; +"room_event_action_view_in_room" = "ルーム内で表示"; "room_notifs_settings_encrypted_room_notice" = "暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。"; "room_notifs_settings_mentions_and_keywords" = "メンションとキーワードのみ"; "security_settings_secure_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "key_backup_setup_intro_setup_action_without_existing_backup" = "鍵のバックアップを使用開始"; "space_participants_action_ban" = "このスペースからブロック"; -"space_participants_action_remove" = "このスペースから削除"; +"space_participants_action_remove" = "このスペースから追放"; "accessibility_button_label" = "ボタン"; "ok" = "OK"; "spaces_empty_space_detail" = "非公開で招待が必要なルームは表示されていません。"; @@ -1135,7 +1135,7 @@ // Intro "secure_key_backup_setup_intro_title" = "セキュアバックアップ"; -"spaces_explore_rooms" = "ルームを探索"; +"spaces_explore_rooms" = "ルームを探す"; "secure_key_backup_setup_intro_use_security_key_info" = "セキュリティーキーを生成します。パスワードマネージャーもしくは金庫のような安全な場所で保管してください。"; "secure_key_backup_setup_intro_info" = "サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。"; "secure_backup_setup_banner_subtitle" = "暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう"; @@ -1146,11 +1146,11 @@ "matrix" = "Matrix"; // Login Screen -"login_create_account" = "アカウント作成:"; +"login_create_account" = "アカウントを作成:"; "login_server_url_placeholder" = "URL (例 https://matrix.org)"; -"login_home_server_title" = "接続先サーバーURL:"; -"login_home_server_info" = "あなたの接続先サーバーは、あなたの全ての会話とアカウント情報を保存します"; -"login_identity_server_title" = "認証サーバーURL:"; +"login_home_server_title" = "ホームサーバーのURL:"; +"login_home_server_info" = "あなたのホームサーバーは、あなたの全ての会話とアカウント情報を保存します"; +"login_identity_server_title" = "IDサーバーのURL:"; "login_password_placeholder" = "パスワード"; "login_email_placeholder" = "メールアドレス"; // Action @@ -1160,28 +1160,28 @@ "resend_message" = "メッセージを再送信"; "select_all" = "全て選択"; "show_details" = "詳細を表示"; -"login_identity_server_info" = "Matrixは、どの電子メールなどがどのMatrix IDに属しているかを追跡するアイデンティティサーバーを提供します。 現在 https://matrix.org のみが存在します。"; +"login_identity_server_info" = "Matrixは、電子メールなどからMatrix IDを検索するIDサーバーを提供します。現在は https://matrix.org のみが存在します。"; "login_user_id_placeholder" = "Matrix ID(例 @bob:matrix.org または bob)"; -"login_optional_field" = "オプション"; -"login_display_name_placeholder" = "表示名 (例 Bob Obson)"; -"login_email_info" = "メールアドレスを指定すると、他のユーザーがあなたをMatrixで簡単に見つけることができ、今後パスワードをリセットすることができます。"; -"login_prompt_email_token" = "メールの認証トークンを入力してください:"; +"login_optional_field" = "任意"; +"login_display_name_placeholder" = "表示名(例 Bob Obson)"; +"login_email_info" = "メールアドレスを指定すると、他のユーザーがあなたをMatrixでより簡単に見つけられます。また、電子メールでパスワードをリセットすることも可能となります。"; +"login_prompt_email_token" = "電子メールの認証トークンを入力してください:"; "login_error_title" = "ログインに失敗しました"; "login_error_no_login_flow" = "このホームサーバーから認証情報を取得できませんでした"; "login_error_do_not_support_login_flows" = "現在、このホームサーバーによって定義されたログインフローの一部または全てをサポートしていません"; "login_error_registration_is_not_supported" = "登録は現在サポートされていません"; -"login_error_forbidden" = "無効なユーザー名/パスワード"; +"login_error_forbidden" = "ユーザー名かパスワードが正しくありません"; "login_error_unknown_token" = "指定されたアクセストークンが認識されませんでした"; "login_error_bad_json" = "不正な形式のJSON"; "login_error_not_json" = "有効なJSONを含んでいませんでした"; -"login_error_limit_exceeded" = "あまりにも多くのリクエストが送られました"; +"login_error_limit_exceeded" = "ログイン要求が多すぎます"; "login_error_user_in_use" = "このユーザー名は既に使用されています"; -"login_error_login_email_not_yet" = "まだクリックされていないメールリンク"; -"login_use_fallback" = "フォールバックページを使用"; +"login_error_login_email_not_yet" = "まだクリックされていない電子メールのリンク"; +"login_use_fallback" = "フォールバック用のページを使用"; "login_leave_fallback" = "キャンセル"; "login_invalid_param" = "無効なパラメーター"; "register_error_title" = "登録に失敗しました"; -"login_error_forgot_password_is_not_supported" = "Forgot passwordは現在サポートされていません"; +"login_error_forgot_password_is_not_supported" = "「パスワードを忘れた場合」は現在サポートされていません"; "login_mobile_device" = "携帯端末"; "login_tablet_device" = "タブレット"; "login_desktop_device" = "デスクトップ"; @@ -1193,7 +1193,7 @@ "abort" = "中断"; "discard" = "破棄"; "dismiss" = "却下"; -"submit" = "提出"; +"submit" = "送信"; "submit_code" = "コードを送信"; "set_default_power_level" = "権限レベルをリセット"; "set_moderator" = "モデレーターを設定"; @@ -1203,10 +1203,10 @@ "start_video_call" = "ビデオ通話を開始"; "mention" = "メンション"; "select_account" = "アカウントを選択"; -"attach_media" = "ライブラリからメディアを添付"; -"capture_media" = "写真/ビデオを撮る"; +"attach_media" = "ライブラリーからメディアを添付"; +"capture_media" = "写真/動画を撮る"; "invite_user" = "Matrixユーザーを招待"; -"reset_to_default" = "デフォルトにリセット"; +"reset_to_default" = "既定にリセット"; "cancel_upload" = "アップロードをキャンセル"; "cancel_download" = "ダウンロードをキャンセル"; "answer_call" = "通話に応答"; @@ -1217,17 +1217,17 @@ "notice_avatar_changed_too" = "(アバターも変更されました)"; "notice_room_name_removed" = "%@がルーム名を削除しました"; "notice_room_topic_removed" = "%@がトピックを削除しました"; -"notice_event_redacted" = "<編集された%@>"; +"notice_event_redacted" = "<%@が編集されました>"; "notice_event_redacted_by" = " %@により"; "notice_event_redacted_reason" = " [理由: %@]"; "notice_profile_change_redacted" = "%@がプロフィール%@を更新しました"; "notice_room_created" = "%@がルームを作成し設定しました。"; -"notice_room_join_rule" = "結合ルールは次のとおり: %@"; -"notice_room_power_level_intro" = "ルームメンバーの権限レベル:"; -"notice_room_power_level_acting_requirement" = "アクション前にユーザーの必要な最小権限レベル:"; -"notice_room_power_level_event_requirement" = "イベントに関連する最小権限レベル:"; -"notice_room_aliases" = "ルームエイリアス: %@"; -"notice_room_related_groups" = "このルームに関連付けられたグループ: %@"; +"notice_room_join_rule" = "参加ルール:%@"; +"notice_room_power_level_intro" = "ルームメンバーの権限レベル:"; +"notice_room_power_level_acting_requirement" = "アクションに必要なユーザーの最小権限レベル:"; +"notice_room_power_level_event_requirement" = "イベントに関連する最小権限レベル:"; +"notice_room_aliases" = "ルームのエイリアス:%@"; +"notice_room_related_groups" = "このルームに関連付けられたグループ:%@"; "notice_encrypted_message" = "暗号化されたメッセージ"; "notice_image_attachment" = "画像添付"; "notice_audio_attachment" = "音声添付"; @@ -1235,17 +1235,17 @@ "notice_location_attachment" = "位置情報添付"; "notice_file_attachment" = "ファイル添付"; "notice_invalid_attachment" = "無効な添付"; -"notice_unsupported_attachment" = "サポートされていない添付: %@"; -"notice_feedback" = "フィードバックイベント (id: %@): %@"; -"notice_redaction" = "%@はイベントを編集しました (id: %@)"; +"notice_unsupported_attachment" = "サポートされていない添付ファイル:%@"; +"notice_feedback" = "フィードバックイベント(id:%@):%@"; +"notice_redaction" = "%@はイベントを編集しました(id:%@)"; "notice_error_unsupported_event" = "サポートされていないイベント"; "notice_error_unexpected_event" = "予期しないイベント"; "notice_error_unknown_event_type" = "不明なイベントタイプ"; -"notice_room_history_visible_to_anyone" = "%@が今後のルーム履歴を「誰でも」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members" = "%@が今後のルーム履歴を「メンバーのみ」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_from_invited_point" = "%@が今後のルーム履歴を「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_from_joined_point" = "%@が今後のルーム履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; -"notice_crypto_unable_to_decrypt" = "** 復号化できません: %@ **"; +"notice_room_history_visible_to_anyone" = "%@が今後のルームの履歴を「誰でも」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members" = "%@が今後のルームの履歴を「メンバーのみ」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_invited_point" = "%@が今後のルームの履歴を「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point" = "%@が今後のルームの履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; +"notice_crypto_unable_to_decrypt" = "** 復号化できません:%@ **"; "notice_crypto_error_unknown_inbound_session_id" = "送信者のセッションからこのメッセージ用の鍵が送信されていません。"; "notice_sticker" = "ステッカー"; "notice_in_reply_to" = "返信先"; @@ -1255,15 +1255,15 @@ "settings" = "設定"; "settings_enable_inapp_notifications" = "アプリ内通知を有効にする"; "settings_enable_push_notifications" = "プッシュ通知を有効にする"; -"settings_enter_validation_token_for" = "%@の認証トークンを入力:"; -"notification_settings_room_rule_title" = "ルーム: '%@'"; +"settings_enter_validation_token_for" = "%@の認証トークンを入力:"; +"notification_settings_room_rule_title" = "ルーム:'%@'"; // Devices -"device_details_title" = "セッション情報\n"; -"device_details_name" = "名前\n"; +"device_details_title" = "セッションの情報\n"; +"device_details_name" = "公開端末名\n"; "device_details_identifier" = "ID\n"; -"device_details_last_seen" = "最終接続日\n"; +"device_details_last_seen" = "直近のオンライン日時\n"; "device_details_last_seen_format" = "%@ @ %@\n"; -"device_details_rename_prompt_message" = "セッションの公開名は、あなたとやり取りする人々に対して表示されます"; +"device_details_rename_prompt_message" = "セッションの公開名は、あなたとやり取りする連絡先に対して表示されます"; "device_details_delete_prompt_title" = "認証"; "device_details_delete_prompt_message" = "この操作には、追加の認証が必要です。\n続行するには、パスワードを入力してください。"; // Encryption information @@ -1277,12 +1277,12 @@ "room_event_encryption_info_event_decryption_error" = "復号化エラー\n"; "room_event_encryption_info_event_unencrypted" = "暗号化されていません"; "room_event_encryption_info_event_none" = "なし"; -"room_event_encryption_info_device" = "\n送信者セッション情報\n"; +"room_event_encryption_info_device" = "\n送信者のセッションの情報\n"; "room_event_encryption_info_device_unknown" = "不明なセッション\n"; -"room_event_encryption_info_device_name" = "名前\n"; +"room_event_encryption_info_device_name" = "公開端末名\n"; "room_event_encryption_info_device_id" = "ID\n"; "room_event_encryption_info_device_verification" = "認証\n"; -"room_event_encryption_info_device_fingerprint" = "Ed25519 fingerprint\n"; +"room_event_encryption_info_device_fingerprint" = "Ed25519 フィンガープリント\n"; "room_event_encryption_info_device_verified" = "認証済"; "room_event_encryption_info_device_not_verified" = "認証されていません"; "room_event_encryption_info_device_blocked" = "ブラックリストに追加済"; @@ -1295,29 +1295,29 @@ "room_event_encryption_verify_ok" = "認証"; // Account "account_save_changes" = "変更を保存"; -"account_link_email" = "リンクメール"; -"account_linked_emails" = "リンクされたメール"; +"account_link_email" = "電子メールをリンク"; +"account_linked_emails" = "リンクした電子メール"; "account_email_validation_title" = "認証の保留中"; -"account_email_validation_message" = "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。"; -"account_email_validation_error" = "メールアドレスを認証できません。メールを確認して、記載されているリンクをクリックしてください。その後、「続行する」をクリックしてください"; +"account_email_validation_message" = "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。"; +"account_email_validation_error" = "メールアドレスを認証できません。メールを確認して、記載されているリンクをクリックしてください。完了したら「続行する」をクリックしてください"; "account_msisdn_validation_title" = "認証の保留中"; "account_msisdn_validation_message" = "SMSで認証番号を送りました。以下にその番号を入力してください。"; "account_msisdn_validation_error" = "電話番号を認証できません。"; "account_error_display_name_change_failed" = "表示名の変更に失敗しました"; "account_error_picture_change_failed" = "画像の変更に失敗しました"; "account_error_matrix_session_is_not_opened" = "Matrixセッションが開かれていません"; -"account_error_email_wrong_title" = "無効な電子メールアドレス"; +"account_error_email_wrong_title" = "無効なメールアドレス"; "account_error_email_wrong_description" = "メールアドレスの形式が正しくありません"; "account_error_msisdn_wrong_title" = "無効な電話番号"; "account_error_msisdn_wrong_description" = "電話番号の形式が正しくありません"; // Room creation -"room_creation_name_title" = "ルーム名:"; -"room_creation_name_placeholder" = "(例 ランチグループ)"; -"room_creation_alias_title" = "ルームの別名:"; -"room_creation_alias_placeholder" = "(例 #foo:example.org)"; -"room_creation_alias_placeholder_with_homeserver" = "(例 #foo%@)"; -"room_creation_participants_title" = "参加者:"; -"room_creation_participants_placeholder" = "(例 @bob:homeserver1; @john:homeserver2…)"; +"room_creation_name_title" = "ルーム名:"; +"room_creation_name_placeholder" = "(例 ランチグループ)"; +"room_creation_alias_title" = "ルームの別名:"; +"room_creation_alias_placeholder" = "(例 #foo:example.org)"; +"room_creation_alias_placeholder_with_homeserver" = "(例 #foo%@)"; +"room_creation_participants_title" = "参加者:"; +"room_creation_participants_placeholder" = "(例 @bob:homeserver1; @john:homeserver2…)"; // Room "room_please_select" = "ルームを選択してください"; "room_error_join_failed_title" = "ルームに参加できませんでした"; @@ -1326,14 +1326,14 @@ "room_error_topic_edition_not_authorized" = "このルームのトピックを編集する権限がありません"; "room_error_cannot_load_timeline" = "タイムラインの読み込みに失敗しました"; "room_error_timeline_event_not_found_title" = "タイムラインの位置を読み込めませんでした"; -"room_error_timeline_event_not_found" = "アプリケーションがこのルームのタイムラインに特定のポイントをロードしようとしましたが、それを見つけることができませんでした"; -"room_left" = "あなたはルームを出ました"; -"room_no_power_to_create_conference_call" = "このルームで会議を開始するために招待する権限が必要です"; -"room_no_conference_call_in_encrypted_rooms" = "暗号化された会議室では会議通話はサポートされません"; +"room_error_timeline_event_not_found" = "このルームのタイムラインに特定のポイントを読み込もうとしましたが、見つけられませんでした"; +"room_left" = "ルームから退出しました"; +"room_no_power_to_create_conference_call" = "このルームで会議を開始するには、招待するための権限が必要です"; +"room_no_conference_call_in_encrypted_rooms" = "暗号化されたルームでは、グループ通話はサポートされません"; // Reply to message "message_reply_to_sender_sent_an_image" = "画像を送信しました。"; -"message_reply_to_sender_sent_a_video" = "動画を送りました。"; -"message_reply_to_sender_sent_an_audio_file" = "オーディオファイルを送信しました。"; +"message_reply_to_sender_sent_a_video" = "動画を送信しました。"; +"message_reply_to_sender_sent_an_audio_file" = "音声ファイルを送信しました。"; "message_reply_to_sender_sent_a_file" = "ファイルを送信しました。"; "message_reply_to_message_to_reply_to_prefix" = "返信先"; // Room members @@ -1345,15 +1345,15 @@ "attachment_small" = "小:%@"; "attachment_medium" = "中:%@"; "attachment_large" = "大:%@"; -"attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; -"attachment_cancel_upload" = "アップロードをキャンセルしますか?"; -"attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; +"attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; +"attachment_cancel_upload" = "アップロードをキャンセルしますか?"; +"attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; "attachment_multiselection_original" = "実際のサイズ"; -"attachment_e2e_keys_file_prompt" = "このファイルには、Matrixクライアントからエクスポートされた暗号鍵が含まれています。\nファイルの内容を表示するか、ファイル内の鍵をインポートしますか?"; +"attachment_e2e_keys_file_prompt" = "このファイルには、Matrixのクライアントからエクスポートされた暗号鍵が含まれています。\nファイルの内容を表示するか、ファイル内の鍵をインポートしますか?"; "attachment_e2e_keys_import" = "インポート…"; // Contacts "contact_mx_users" = "Matrixユーザー"; -"contact_local_contacts" = "ローカルの連絡先"; +"contact_local_contacts" = "端末の連絡先"; // Groups // Search "search_no_results" = "結果がありません"; @@ -1365,19 +1365,19 @@ "format_time_d" = "日"; // E2E import "e2e_import_room_keys" = "ルームの暗号鍵をインポート"; -"e2e_import_prompt" = "このプロセスでは、以前に別のMatrixクライアントからエクスポートした暗号鍵をインポートできます。 これにより、他のクライアントが解読できる全てのメッセージを解読することができます。\nエクスポートした暗号鍵のファイルは、パスフレーズで保護されています。 ファイルを復号化するには、パスフレーズをここに入力する必要があります。"; +"e2e_import_prompt" = "このプロセスでは、以前に別のMatrixのクライアントからエクスポートした暗号鍵をインポートできます。 これにより、他のクライアントが解読できる全てのメッセージを解読することができます。\nエクスポートした暗号鍵のファイルは、パスフレーズで保護されています。 ファイルを復号化するには、パスフレーズをここに入力する必要があります。"; "e2e_import" = "インポート"; "e2e_passphrase_enter" = "パスフレーズを入力"; // E2E export "e2e_export_room_keys" = "ルームの暗号鍵をエクスポート"; -"e2e_export_prompt" = "このプロセスでは、暗号化されたルームで受信したメッセージの鍵をローカルファイルにエクスポートできます。 そのファイルを別のMatrixクライアントにインポートすると、クライアントはこれらのメッセージを復号化することができます。\nエクスポートしたファイルを使えば、誰でも暗号化されたメッセージを復号化できるので、ファイルを安全に保つように注意する必要があります。"; +"e2e_export_prompt" = "このプロセスでは、暗号化されたルームで受信したメッセージの鍵をローカルファイルにエクスポートできます。 そのファイルを別のMatrixのクライアントにインポートすると、クライアントはこれらのメッセージを復号化することができます。\nエクスポートしたファイルを使うと、誰でも暗号化されたメッセージを復号化できるため、ファイルを安全に保つように注意する必要があります。"; "e2e_export" = "エクスポート"; "e2e_passphrase_confirm" = "パスフレーズを確認"; "e2e_passphrase_empty" = "パスフレーズは空であってはいけません"; -"e2e_passphrase_not_match" = "パスフレーズは一致する必要があります"; +"e2e_passphrase_not_match" = "パスフレーズが一致していません"; "e2e_passphrase_create" = "パスフレーズの作成"; // Others -"user_id_title" = "ユーザーID:"; +"user_id_title" = "ユーザーID:"; "offline" = "オフライン"; "unsent" = "未送信"; "error" = "エラー"; @@ -1388,38 +1388,38 @@ "public" = "公開"; "power_level" = "権限レベル"; "network_error_not_reachable" = "ネットワーク接続を確認してください"; -"user_id_placeholder" = "例: @bob:homeserver"; -"ssl_homeserver_url" = "ホームサーバーのURL: %@"; +"user_id_placeholder" = "例:@bob:homeserver"; +"ssl_homeserver_url" = "ホームサーバーのURL:%@"; // Permissions "camera_access_not_granted_for_call" = "ビデオ通話にはカメラへのアクセスが必要ですが、%@にはカメラを使用する権限がありません"; "microphone_access_not_granted_for_call" = "通話にはマイクへのアクセスが必要ですが、%@にはマイクを使用する権限がありません"; -"local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはそのアクセス権限がありません"; -"local_contacts_access_discovery_warning_title" = "ユーザーの探索"; +"local_contacts_access_not_granted" = "ローカルの連絡先からユーザーを探すには連絡先にアクセスする必要がありますが、%@にはアクセス権限がありません"; +"local_contacts_access_discovery_warning_title" = "ユーザーを探す"; "local_contacts_access_discovery_warning" = "Matrixを既に使用している連絡先を見つけるため、%@は電話帳にあるメールアドレスと電話番号を、あなたが選択したMatrixのIDサーバーに送信することができます。サポートしている場合、個人データは送信前にハッシュ化されます。詳細はIDサーバーのプライバシーポリシーを確認してください。"; // Country picker "country_picker_title" = "国を選択"; // Language picker "language_picker_title" = "言語を選択"; -"language_picker_default_language" = "既定値 (%@)"; +"language_picker_default_language" = "既定値(%@)"; "notice_room_invite" = "%@が%@を招待しました"; "notice_room_third_party_invite" = "%@が%@にルームへの招待を送りました"; "notice_room_third_party_registered_invite" = "%@が%@の招待を受け入れました"; "notice_room_join" = "%@が参加しました"; "notice_room_leave" = "%@が退出しました"; "notice_room_reject" = "%@が招待を拒否しました"; -"notice_room_kick" = "%@が%@を追い出しました"; -"notice_room_unban" = "%@が%@を追放解除しました"; +"notice_room_kick" = "%@が%@を追放しました"; +"notice_room_unban" = "%@が%@のブロックを解除しました"; "notice_room_ban" = "%@が%@をブロックしました"; "notice_room_withdraw" = "%@が%@の招待を取り下げました"; -"notice_room_reason" = ". 理由: %@"; +"notice_room_reason" = "。理由:%@"; "notice_avatar_url_changed" = "%@がアバターを変更しました"; "notice_display_name_set" = "%@が表示名を%@に設定しました"; "notice_display_name_changed_from" = "%@が表示名を%@から%@に変更しました"; "notice_display_name_removed" = "%@が表示名を削除しました"; "notice_topic_changed" = "%@がトピックを「%@」に変更しました。"; "notice_room_name_changed" = "%@がルーム名を%@に変更しました。"; -"notice_placed_voice_call" = "%@が電話をかけました"; -"notice_placed_video_call" = "%@がビデオ電話をかけました"; +"notice_placed_voice_call" = "%@が音声通話を発信しました"; +"notice_placed_video_call" = "%@がビデオ通話を発信しました"; "notice_answered_video_call" = "%@が電話に出ました"; "notice_ended_video_call" = "%@が通話を終了しました"; "notice_conference_call_request" = "%@がVoIP会議をリクエストしました"; @@ -1431,7 +1431,7 @@ "resend" = "再送信"; "redact" = "削除"; "share" = "共有"; -"set_power_level" = "権限レベル"; +"set_power_level" = "権限レベルを設定"; "delete" = "削除"; // actions "action_logout" = "ログアウト"; @@ -1443,7 +1443,7 @@ "membership_ban" = "ブロックしました"; "num_members_one" = "%@人のユーザー"; "num_members_other" = "%@人のユーザー"; -"kick" = "チャットから追放"; +"kick" = "会話から追放"; "ban" = "ブロック"; "unban" = "ブロック解除"; "message_unsaved_changes" = "保存されていない変更があります。 退出すると変更は取り消されます。"; @@ -1452,24 +1452,24 @@ "login_error_must_start_http" = "URLは http[s]:// で始まる必要があります"; // room details dialog screen // contacts list screen -"invitation_message" = "matrixでチャットしましょう。 ウェブサイト http://matrix.org を開いてください。"; +"invitation_message" = "matrixでチャットしましょう。 詳細はウェブサイト http://matrix.org で確認してください。"; // Settings screen -"settings_title_config" = "構成"; +"settings_title_config" = "設定"; "settings_title_notifications" = "通知"; // Notification settings screen "notification_settings_disable_all" = "全ての通知を無効にする"; "notification_settings_enable_notifications" = "通知を有効にする"; "notification_settings_enable_notifications_warning" = "現在、全ての端末で全ての通知が無効になっています。"; -"notification_settings_global_info" = "通知設定はユーザーアカウントに保存され、デスクトップ通知を含む全てのクライアント間で共有されます。\n\nルールは順番に適用されます。 一致する最初のルールは、メッセージの結果を定義します。\nだから:単語ごとの通知は、送信者ごとの通知よりも重要なルームごとの通知よりも重要です。\n同じ種類の複数のルールの場合、一致するリストの最初のルールが優先されます。"; +"notification_settings_global_info" = "通知設定はユーザーアカウントに保存され、デスクトップ通知を含む全てのクライアント間で共有されます。\n\nルールは順番に適用されます。 一致する最初のルールは、メッセージの結果を定義します。\nしたがって、単語単位の通知はルーム単位の通知よりも優先され、ルーム単位の通知は、送信者単位の通知よりも優先されます。\n同じ種類の複数のルールに関しては、一致するリストの最初のルールが優先されます。"; "notification_settings_per_word_notifications" = "単語単位の通知"; -"notification_settings_per_word_info" = "単語は大文字と小文字を区別せずに一致させ、*ワイルドカードを含めることができます。 従って:\nfooは、区切り文字で囲まれた文字列foo(例 句読点や空白、行の開始/終了)と一致します。\nfoo*は、fooで始まる単語に一致します。\n*foo*は、3文字のfooを含む単語に一致します。"; +"notification_settings_per_word_info" = "単語は大文字と小文字を区別せずに一致させ、*ワイルドカードを含めることができます。 よって、\nfooは、区切り文字で囲まれた文字列foo(例 句読点や空白、行の開始/終了)と一致します。\nfoo*は、fooで始まる単語に一致します。\n*foo*は、3文字のfooを含む単語に一致します。"; "notification_settings_always_notify" = "常に通知"; "notification_settings_never_notify" = "通知しない"; "notification_settings_word_to_match" = "一致する単語"; "notification_settings_highlight" = "ハイライト"; "notification_settings_custom_sound" = "カスタムサウンド"; -"notification_settings_per_room_notifications" = "1ルームあたりの通知"; -"notification_settings_per_sender_notifications" = "送信者ごとの通知"; +"notification_settings_per_room_notifications" = "ルーム単位の通知"; +"notification_settings_per_sender_notifications" = "送信者単位の通知"; "notification_settings_sender_hint" = "@user:domain.com"; "notification_settings_select_room" = "ルームを選択"; "notification_settings_other_alerts" = "その他のアラート"; @@ -1481,7 +1481,7 @@ "notification_settings_receive_a_call" = "通話を受信したときに通知"; "notification_settings_suppress_from_bots" = "ボットからの通知を抑制"; "notification_settings_by_default" = "既定値では…"; -"notification_settings_notify_all_other" = "他の全てのメッセージ/ルームについて通知"; +"notification_settings_notify_all_other" = "他の全てのメッセージまたはルームについて通知"; // gcm section // call string "call_waiting" = "待機中..."; @@ -1497,14 +1497,14 @@ "ssl_remain_offline" = "無視"; "ssl_fingerprint_hash" = "フィンガープリント(%@):"; "ssl_could_not_verify" = "リモートサーバーのIDを認証できませんでした。"; -"ssl_cert_not_trust" = "これは、誰かがあなたのトラフィックを悪意を持って傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味します。"; -"ssl_cert_new_account_expl" = "サーバー管理者がこれが予期されると述べた場合は、以下の指紋が提供された指紋と一致することを確認してください。"; -"ssl_unexpected_existing_expl" = "証明書は、お使いの携帯電話にて信頼されたものから変更されました。 これは非常に珍しいことです。 この新しい証明書に同意しないことをお勧めします。"; -"ssl_expected_existing_expl" = "証明書が以前に信頼されたものから信頼されていないものに変更されました。 サーバーが証明書を更新した可能性があります。 予想される指紋については、サーバー管理者にお問い合わせください。"; -"ssl_only_accept" = "サーバー管理者が上記のものと一致する指紋を発行した場合にのみ、証明書を受け入れてください。"; -"unignore" = "無視しない"; -"notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化をオンにしました。"; -"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %@)をオンにしました。"; +"ssl_cert_not_trust" = "これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。"; +"ssl_cert_new_account_expl" = "サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。"; +"ssl_unexpected_existing_expl" = "証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。"; +"ssl_expected_existing_expl" = "証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。"; +"ssl_only_accept" = "サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。"; +"unignore" = "無視を解除"; +"notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化を有効にしました。"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %@)を有効にしました。"; "device_details_rename_prompt_title" = "セッション名"; "account_error_push_not_allowed" = "通知は許可されていません"; "notice_room_third_party_revoked_invite" = "%@が%@のルームへの招待を取り消しました"; @@ -1513,7 +1513,7 @@ "notice_room_invite_you" = "%@があなたを招待しました"; "notice_room_join_by_you" = "参加しました"; "notice_room_leave_by_you" = "退出しました"; -"notice_room_kick_by_you" = "%@をキックしました"; +"notice_room_kick_by_you" = "%@を追放しました"; "notice_room_unban_by_you" = "%@のブロックを解除しました"; "notice_room_ban_by_you" = "%@をブロックしました"; "notice_avatar_url_changed_by_you" = "アバターを変更しました"; @@ -1521,9 +1521,9 @@ "notice_display_name_changed_from_by_you" = "表示名を%@から%@に変更しました"; "notice_display_name_removed_by_you" = "表示名を削除しました"; "notice_topic_changed_by_you" = "トピックを「%@」に変更しました。"; -"notice_room_name_changed_by_you" = "ルームの名前を%@に変更しました。"; -"notice_placed_voice_call_by_you" = "音声通話を開始しました"; -"notice_placed_video_call_by_you" = "ビデオ通話を開始しました"; +"notice_room_name_changed_by_you" = "ルーム名を%@に変更しました。"; +"notice_placed_voice_call_by_you" = "音声通話を発信しました"; +"notice_placed_video_call_by_you" = "ビデオ通話を発信しました"; "notice_answered_video_call_by_you" = "電話に出ました"; "notice_ended_video_call_by_you" = "通話を終了しました"; "notice_conference_call_request_by_you" = "VoIP会議をリクエストしました"; @@ -1531,8 +1531,8 @@ "notice_room_topic_removed_by_you" = "トピックを削除しました"; "notice_profile_change_redacted_by_you" = "プロフィール %@を更新しました"; "notice_room_created_by_you" = "ルームを作成し設定しました。"; -"notice_encryption_enabled_ok_by_you" = "あなたはエンドツーエンド暗号化をオンにしました。"; -"notice_redaction_by_you" = "イベントを編集しました (id: %@)"; +"notice_encryption_enabled_ok_by_you" = "エンドツーエンド暗号化を有効にしました。"; +"notice_redaction_by_you" = "イベントを編集しました(id:%@)"; "resume_call" = "再開"; "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@が今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@が今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; @@ -1575,12 +1575,12 @@ // Mark: - Polls "poll_edit_form_create_poll" = "アンケートを作成"; -"poll_timeline_vote_not_registered_subtitle" = "申し訳ありませんが投票が登録されていません、再度お試しください"; -"poll_timeline_vote_not_registered_title" = "投票が登録されていません"; +"poll_timeline_vote_not_registered_subtitle" = "投票できませんでした。もう一度やり直してください"; +"poll_timeline_vote_not_registered_title" = "投票できませんでした"; "poll_timeline_total_final_results" = "合計%lu票の投票に基づく最終結果"; "poll_timeline_total_final_results_one_vote" = "合計1票の投票に基づく最終結果"; -"poll_timeline_total_votes_not_voted" = "合計%lu票、投票すると結果を確認できます"; -"poll_timeline_total_one_vote_not_voted" = "合計1票、投票すると結果を確認できます"; +"poll_timeline_total_votes_not_voted" = "合計%lu票。投票すると結果を確認できます"; +"poll_timeline_total_one_vote_not_voted" = "合計1票。投票すると結果を確認できます"; "poll_timeline_total_votes" = "合計%lu票"; "poll_timeline_total_one_vote" = "合計1票"; "biometrics_cant_unlocked_alert_message_retry" = "再試行"; @@ -1619,8 +1619,8 @@ "spaces_creation_footer" = "この設定は後から変更できます"; "onboarding_display_name_hint" = "この設定は後から変更できます"; "spaces_creation_visibility_title" = "作成するスペースの種類を選択してください"; -"space_public_join_rule_detail" = "誰でも参加可能、コミュニティー向け"; -"space_private_join_rule_detail" = "招待者のみ参加可能、個人やチーム向け"; +"space_public_join_rule_detail" = "誰でも参加可能。コミュニティー向け"; +"space_private_join_rule_detail" = "招待者のみ参加可能。個人やチーム向け"; "onboarding_use_case_title" = "誰と最もよく会話しますか?"; "onboarding_splash_page_4_message" = "Elementは職場利用にも最適です。世界で最も安全な組織によって信頼されています。"; "onboarding_splash_page_4_title_no_pun" = "あなたのチームのメッセージングに。"; @@ -1643,19 +1643,19 @@ "spaces_creation_post_process_creating_space_task" = "%@を作成しています"; "side_menu_coach_message" = "右にスワイプまたはタップで全てのルームが表示されます"; "spaces_creation_post_process_creating_space" = "スペースを作成しています"; -"spaces_creation_add_rooms_message" = "このスペースはあなた専用のため、他の人に通知されることはありません。この設定は後から変更できます。"; -"spaces_creation_add_rooms_title" = "どれを追加しますか?"; -"spaces_creation_sharing_type_me_and_teammates_detail" = "あなたとチームメイトの非公開のスペース"; +"spaces_creation_add_rooms_message" = "これはあなた専用のスペースで、他の人からは見えません。後からルームや会話を追加することもできます。"; +"spaces_creation_add_rooms_title" = "何を追加しますか?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "自分とチームメイトの非公開のスペース"; "spaces_creation_sharing_type_me_and_teammates_title" = "自分とチームメイト"; "spaces_creation_sharing_type_just_me_detail" = "ルームを整理するための非公開のスペース"; "spaces_creation_sharing_type_just_me_title" = "自分専用"; -"spaces_creation_sharing_type_message" = "参加者を選択してください%@。この設定は後から変更できます。"; +"spaces_creation_sharing_type_message" = "適切な人が %@ アクセスできることを確認してください。この設定は後から変更できます。"; "spaces_creation_settings_message" = "詳細を入力してください。この設定は後から変更できます。"; -"spaces_creation_address_default_message" = "スペースは以下のように表記されます\n%@"; +"spaces_creation_address_default_message" = "スペースは以下で閲覧可能になります\n%@"; "space_settings_current_address_message" = "スペースは以下で閲覧できます\n%@"; -"space_topic" = "説明文"; +"space_topic" = "詳細"; "spaces_creation_cancel_message" = "これまでの設定は失われます。"; -"spaces_creation_cancel_title" = "スペースの作成を停止しますか?"; +"spaces_creation_cancel_title" = "スペースの作成を中止しますか?"; "create_room_section_footer_type_private" = "招待した人のみが検索・参加できます。"; // MARK: - Searchable Directory View Controller @@ -1716,7 +1716,7 @@ // Room suggestion Settings "room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; "room_details_promote_room_suggest_title" = "スペースのメンバーへのおすすめ"; -"settings_default" = "既定の通知"; +"settings_default" = "通知のデフォルト"; "pin_protection_reset_alert_action_reset" = "リセット"; "authentication_recaptcha_title" = "あなたは人間ですか?"; "authentication_verify_msisdn_waiting_button" = "コードを再送信"; @@ -1737,7 +1737,7 @@ // MARK: Password Validation "password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; "space_selector_empty_view_title" = "まだスペースがありません"; -"all_chats_empty_list_placeholder_title" = "未読はありません"; +"all_chats_empty_list_placeholder_title" = "未読はありません。"; "all_chats_empty_unreads_placeholder_message" = "未読のメッセージがある場合は、ここに表示されます。"; "room_notifs_settings_account_settings" = "アカウントの設定"; "room_access_settings_screen_upgrade_alert_upgrading" = "ルームをアップグレードしています"; @@ -1770,12 +1770,12 @@ "space_selector_empty_view_information" = "スペースは、ルームと連絡先をまとめる方法です。はじめに、スペースを作成しましょう。"; "all_chats_all_filter" = "全て"; "all_chats_edit_layout_recents" = "履歴"; -"all_chats_edit_layout_unreads" = "未読"; -"all_chats_section_title" = "チャット"; +"all_chats_edit_layout_unreads" = "未読あり"; +"all_chats_section_title" = "会話"; // Mark: - All Chats -"all_chats_title" = "全てのチャット"; +"all_chats_title" = "全ての会話"; "location_sharing_live_loading" = "位置情報(ライブ)を読み込んでいます…"; "location_sharing_live_list_item_stop_sharing_action" = "停止"; "location_sharing_live_list_item_current_user_display_name" = "あなた"; @@ -1809,7 +1809,7 @@ /* Note: The word "don't" is formatted in bold */ "analytics_prompt_point_2" = "私たちは、情報を第三者と共有することはありません"; /* Note: The word "don't" is formatted in bold */ -"analytics_prompt_point_1" = "私たちは、アカウントのデータを記録したり分析したりすることはありません"; +"analytics_prompt_point_1" = "私たちは、アカウントのいかなるデータも記録したり分析したりすることはありません"; "analytics_prompt_terms_link_upgrade" = "ここ"; "call_jitsi_unable_to_start" = "グループ通話を開始できません"; "network_offline_message" = "オフラインです。接続を確認してください。"; @@ -1865,7 +1865,7 @@ "threads_beta_information_link" = "詳細を表示"; "threads_beta_title" = "スレッド"; "threads_notice_done" = "了解"; -"threads_notice_title" = "スレッドは正式版になりました🎉"; +"threads_notice_title" = "スレッド機能は正式版になりました🎉"; "message_from_a_thread" = "スレッドから"; "room_accessibility_record_voice_message" = "音声メッセージを録音"; "room_event_copy_link_info" = "リンクをクリップボードにコピーしました。"; @@ -2180,13 +2180,13 @@ "notice_room_third_party_registered_invite_by_you" = "%@の招待を受け入れました"; "notice_room_reject_by_you" = "招待を拒否しました"; "notice_room_withdraw_by_you" = "%@の招待を取り下げました"; -"notice_declined_video_call_by_you" = "着信を拒否しました"; -"notice_room_history_visible_to_anyone_by_you" = "今後のルーム履歴を「誰でも」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_by_you" = "今後のルーム履歴を「メンバーのみ」閲覧可能に設定しました。"; +"notice_declined_video_call_by_you" = "通話を拒否しました"; +"notice_room_history_visible_to_anyone_by_you" = "今後のルームの履歴を「誰でも」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_by_you" = "今後のルームの履歴を「メンバーのみ」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_by_you_for_dm" = "今後のメッセージを「メンバーのみ」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point_by_you" = "今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "今後のメッセージを「全員 (招待された時点以降)」閲覧可能に設定しました。"; -"notice_room_history_visible_to_members_from_joined_point_by_you" = "今後のルーム履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "今後のルームの履歴を「メンバーのみ (参加した時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; "call_more_actions_audio_use_device" = "端末のスピーカー"; "call_more_actions_transfer" = "転送"; @@ -2299,7 +2299,7 @@ // Device -"device_verification_verified_title" = "認証されました!"; +"device_verification_verified_title" = "認証しました!"; // Device @@ -2376,10 +2376,10 @@ "key_backup_recover_from_recovery_key_info" = "セキュリティーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます"; "call_video_with_user" = "%@とのビデオ通話"; "call_more_actions_hold" = "保留"; -"notice_encryption_enabled_unknown_algorithm_by_you" = "エンドツーエンド暗号化(認識されていないアルゴリズム %@)をオンにしました。"; +"notice_encryption_enabled_unknown_algorithm_by_you" = "エンドツーエンド暗号化(認識されていないアルゴリズム %@)を有効にしました。"; "notice_room_name_removed_by_you_for_dm" = "ルーム名を削除しました"; "notice_room_third_party_revoked_invite_by_you" = "%@のルームへの招待を取り消しました"; -"notice_declined_video_call" = "%@は着信を拒否しました"; +"notice_declined_video_call" = "%@が通話を拒否しました"; "attachment_size_prompt_message" = "これは設定から無効にできます。"; "message_reply_to_sender_sent_their_location" = "位置情報を共有しました。"; "message_reply_to_sender_sent_a_voice_message" = "音声メッセージを送信しました。"; @@ -2432,7 +2432,7 @@ "all_chats_edit_menu_leave_space" = "%@から退出"; "all_chats_user_menu_accessibility_label" = "ユーザーメニュー"; "room_recents_recently_viewed_section" = "最近表示したルーム"; -"all_chats_empty_space_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。"; +"all_chats_empty_space_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。"; "all_chats_edit_layout_sorting_options_title" = "メッセージを並び替える"; "all_chats_edit_layout_add_filters_title" = "メッセージを絞り込む"; "version_check_modal_action_title_supported" = "了解"; @@ -2493,7 +2493,7 @@ // MARK: - Space Creation -"spaces_creation_hint" = "スペースは、ルームや連絡先をグループ化する新しい方法です。"; +"spaces_creation_hint" = "スペースは、ルームや連絡先をまとめる新しい方法です。"; "spaces_add_space" = "スペースを追加"; "spaces_add_room" = "ルームを追加"; "spaces_invite_people" = "連絡先を招待"; @@ -2505,7 +2505,7 @@ "spaces_explore_rooms_room_number" = "%@個のルーム"; "leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; "leave_space_only_action" = "どのルームからも退出しない"; -"threads_discourage_information_2" = "\n\nスレッドを有効にしてよろしいですか?"; +"threads_discourage_information_2" = "\n\nスレッド機能を有効にしてよろしいですか?"; "room_no_privileges_to_create_group_call" = "通話を開始するには管理者あるいはモデレーターである必要があります。"; "contacts_address_book_permission_denied_alert_message" = "連絡先を有効にするには、端末の設定画面を開いてください。"; "contacts_address_book_permission_denied_alert_title" = "連絡先が無効です"; @@ -2534,7 +2534,7 @@ "room_accessibility_record_voice_message_hint" = "2回続けてタップし長押しすると録音。"; "room_preview_decline_invitation_options" = "招待を拒否するか、このユーザーを無視しますか?"; "threads_beta_information" = "スレッド機能を使って、会話をまとめましょう。\n\nスレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 "; -"threads_notice_information" = "実験期間中に作成されたスレッドは通常の返信として表示されます

スレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。"; +"threads_notice_information" = "実験期間中に作成されたスレッドは通常の返信として表示されます

スレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。"; "threads_empty_info_my" = "既存のスレッドに返信するか、メッセージをタップし「スレッド」から新しいスレッドを開始。"; "room_accessibility_thread_more" = "その他"; "room_first_message_placeholder" = "最初のメッセージを送信…"; @@ -2553,11 +2553,11 @@ "authentication_qr_login_display_title" = "端末をリンク"; /* The placeholder will show the full Matrix ID that has been entered. */ "authentication_registration_username_footer_available" = "他の人は %@ であなたを見つけることができます"; -"authentication_server_selection_register_message" = "あなたのサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます"; +"authentication_server_selection_register_message" = "あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます"; "authentication_server_info_title_login" = "会話が実施される場所"; -"authentication_server_info_title" = "会話が実施される場所"; +"authentication_server_info_title" = "アカウントを作成するサーバー"; "onboarding_avatar_message" = "表示名にプロフィール画像を追加しましょう"; -"all_chats_edit_layout_add_filters_message" = "自動的にメッセージをあなたが選択したカテゴリーにフィルタリング"; +"all_chats_edit_layout_add_filters_message" = "あなたが選択したカテゴリーにメッセージを自動的にフィルタリング"; "all_chats_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。"; "home_empty_view_information" = "チーム、友達、組織向けのオールインワンの安全なチャットアプリです。以下の+ボタンを押すと、連絡先とルームを追加できます。"; "all_chats_empty_view_title" = "%@\nは空です。"; @@ -2587,7 +2587,7 @@ "call_remote_holded" = "%@が通話を保留しました"; "call_holded" = "通話を保留しました"; "call_more_actions_unhold" = "再開"; -"user_session_rename_session_description" = "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。\n\n相手はあなたとやり取りしていることを確かめることができますが、あなたがここに入力するセッション名は相手に対して表示されます。"; +"user_session_rename_session_description" = "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。\n\nセッションの一覧から、相手はあなたとやり取りしていることを確かめることができます。なお、あなたがここに入力するセッション名は相手に対して表示されます。"; "user_session_unverified_session_description" = "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。"; "user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。"; "user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。"; @@ -2648,7 +2648,7 @@ "spaces_creation_empty_room_name_error" = "名称が必要です"; "space_settings_update_failed_message" = "スペースの設定の更新に失敗しました。再試行しますか?"; "spaces_coming_soon_title" = "近日公開"; -"spaces_explore_rooms_format" = "%@を探索"; +"spaces_explore_rooms_format" = "%@を探す"; "spaces_create_subspace_title" = "サブスペースを作成"; "space_beta_announce_title" = "スペースは近日公開"; "space_beta_announce_badge" = "ベータ版"; @@ -2753,7 +2753,7 @@ "key_verification_verified_this_session_information" = "保護されたメッセージをこの端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; "key_verification_verified_new_session_information" = "保護されたメッセージを新しい端末で読むことができます。また、他のユーザーもこの端末を信頼することができます。"; "key_verification_verified_other_session_information" = "保護されたメッセージを他のセッションで読むことができます。また、他のユーザーもこのセッションを信頼することができます。"; -"call_consulting_with_user" = "%@に相談しています"; +"call_consulting_with_user" = "%@と相談しています"; "room_displayname_more_than_two_members" = "%@とその他%@人"; "notice_error_unformattable_event" = "** メッセージを描画できません。不具合を報告してください"; "wysiwyg_composer_format_action_un_indent" = "インデントを減らす"; @@ -2800,7 +2800,7 @@ "spaces_creation_new_rooms_title" = "どのような議論を行いますか?"; "spaces_subspace_creation_visibility_message" = "作成したスペースは%@に追加されます。"; "spaces_feature_not_available" = "この機能はまだ利用できません。当面は、コンピューターで%@によりこれを行うことができます。"; -"spaces_no_member_found_detail" = "%@のメンバー以外の人を探していますか?当面は、ウェブ版またはデスクトップ版で招待することができます。"; +"spaces_no_member_found_detail" = "%@のメンバー以外の人を探していますか?当面は、ウェブ版またはデスクトップ版で招待できます。"; "spaces_coming_soon_detail" = "この機能はまだ実装されていません。当面は、コンピューターで%@によりこれを行うことができます。"; "spaces_invites_coming_soon_title" = "招待は近日公開"; "spaces_add_rooms_coming_soon_title" = "ルームの追加は近日公開"; @@ -2808,14 +2808,14 @@ "leave_space_message_admin_warning" = "あなたはこのスペースの管理者です。退出する前に、管理者の権限を別のメンバーに移譲してください。"; "leave_space_message" = "%@から退出してよろしいですか?このスペースの全てのルームとスペースからも退出しますか?"; "spaces_add_subspace_title" = "%@内にスペースを作成"; -"space_feature_unavailable_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。\n\n近日公開予定です。別のプラットフォームでスペースに参加すると、ここで参加するどのルームにもアクセスすることができます。"; -"space_beta_announce_information" = "スペースは、ルームや連絡先をグループ化する新しい方法です。iOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます。"; +"space_feature_unavailable_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。\n\n近日公開予定です。別のプラットフォームでスペースに参加すると、ここで参加するどのルームにもアクセスすることができます。"; +"space_beta_announce_information" = "スペースは、ルームや連絡先をまとめる新しい方法です。iOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます。"; "space_feature_unavailable_subtitle" = "スペースはiOS版ではまだ使用できませんが、ウェブ版とデスクトップ版では使用できます"; "space_beta_announce_subtitle" = "コミュニティー機能の新しいバージョン"; "space_invite_not_enough_permission" = "このスペースにユーザーを招待する権限がありません"; "room_invite_not_enough_permission" = "このルームにユーザーを招待する権限がありません"; "room_invite_to_room_option_detail" = "%@のメンバーにはなりません。"; -"room_invite_to_space_option_detail" = "%@を探索することはできますが、%@のメンバーにはなりません。"; +"room_invite_to_space_option_detail" = "%@を探すことはできますが、%@のメンバーにはなりません。"; // MARK: - Room invite From ce597c9c815067e28c5e3bdf8df16cb20795c517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 4 Feb 2023 08:41:40 +0000 Subject: [PATCH 450/468] Translated using Weblate (Estonian) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 530e2f1e8..04a4197cc 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2658,9 +2658,9 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Läbiva krüptimise versioon 2.0 on kasutusel (väljalülitamiseks pead välja logima)"; -"settings_labs_confirm_crypto_sdk" = "Selle valikuga võtad kasutusele uue, kiirema ja töökindlama läbiva krüptimise lahenduse, mis on kirjutatud programmeerimiskeeles Rust. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Kas sa soovid jätkata?"; -"settings_labs_enable_crypto_sdk" = "Võta kasutusele läbiva krüptimise versioon 2.0"; +"settings_labs_disable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine (väljalülitamiseks pead välja logima)"; +"settings_labs_confirm_crypto_sdk" = "Palun arvesta, et see funktsionaalsus on alles katseline ja ei pruugi toimida eesmärgipäraselt. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Jätka ettevaatlikult ja omal äranägemisel."; +"settings_labs_enable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine"; "poll_history_load_more" = "Laadi veel küsitlusi"; "poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; "poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi"; From f1ed863ee35166dc1f5da855f755e4b703f54d76 Mon Sep 17 00:00:00 2001 From: keda82 Date: Mon, 6 Feb 2023 19:49:54 +0000 Subject: [PATCH 451/468] Translated using Weblate (Swedish) Currently translated at 99.7% (2371 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 1862efe67..1f7e35448 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2658,6 +2658,6 @@ "launch_loading_migrating_data" = "Migrerar data\n%@ %%"; "room_details_polls" = "Omröstningshistorik"; "settings_labs_disable_crypto_sdk" = "Krypto-SDK är aktiverad. För att inaktivera, vänligen installera om appen"; -"settings_labs_confirm_crypto_sdk" = "Den här åtgärden kan inte ångras"; +"settings_labs_confirm_crypto_sdk" = "Vänligen notera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att gå tillbaka, logga ut och logga sedan in igen. Använd på egen risk."; "settings_labs_enable_crypto_sdk" = "Aktivera ny Rust-baserad krypto-SDK"; "accessibility_selected" = "vald"; From 3454d2858ffbd34a6b7c027bd64e808413ddc51a Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 6 Feb 2023 19:47:41 +0000 Subject: [PATCH 452/468] Translated using Weblate (Swedish) Currently translated at 99.7% (2371 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 1f7e35448..13ee80174 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2659,5 +2659,6 @@ "room_details_polls" = "Omröstningshistorik"; "settings_labs_disable_crypto_sdk" = "Krypto-SDK är aktiverad. För att inaktivera, vänligen installera om appen"; "settings_labs_confirm_crypto_sdk" = "Vänligen notera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att gå tillbaka, logga ut och logga sedan in igen. Använd på egen risk."; -"settings_labs_enable_crypto_sdk" = "Aktivera ny Rust-baserad krypto-SDK"; +"settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust"; "accessibility_selected" = "vald"; +"settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök att växla dina alternativ igen."; From 2840f7d3abe76ce03b55123109711ec66ca5fdd7 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Tue, 7 Feb 2023 09:48:43 +0000 Subject: [PATCH 453/468] Translated using Weblate (French) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 4ea0610d6..18612cab1 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -2729,9 +2729,9 @@ "launch_loading_migrating_data" = "Migration des données\n%@ %%"; "key_backup_recover_from_private_key_progress" = "%@%% Fini"; "room_details_polls" = "Historique des sondages"; -"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout 2.0 (se déconnecter pour désactiver)"; -"settings_labs_confirm_crypto_sdk" = "Cette option activera un nouvel engin pour le chiffrement de bout en bout, plus rapide et plus fiable, écrit en Rust. Une fois activé vous devrez vous déconnecter pour le désactiver. Voulez-vous continuer?"; -"settings_labs_enable_crypto_sdk" = "Chiffrement de bout en bout 2.0"; +"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout avec Rust (se déconnecter pour désactiver)"; +"settings_labs_confirm_crypto_sdk" = "Cette option activera le nouveau moteur de chiffrement de bout en bout, plus rapide et plus fiable, écrit en Rust. Une fois activé vous devrez vous déconnecter pour le désactiver. Voulez-vous continuer ?"; +"settings_labs_enable_crypto_sdk" = "Chiffrement de bout en bout en Rust"; "settings_push_rules_error" = "Nous avons rencontré une erreur lors de la mise à jours de vos préférences de notification. Veuillez réactiver l'option."; "password_policy_pwd_in_dict_error" = "Ce mot de passe a été trouvé dans un dictionnaire, et son usage n'est donc pas autorisé."; "password_policy_weak_pwd_error" = "Ce mot de passe est trop faible. Il doit contenir au moins 8 caractères, dont au moins une majuscule, une minuscule, un chiffre et un caractère spécial."; From 63a8d4e83a382c3b153ad915936c1475c3729e86 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 7 Feb 2023 09:39:21 +0000 Subject: [PATCH 454/468] Translated using Weblate (Japanese) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index efb3b3776..9202162b8 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -583,7 +583,7 @@ "create_room_title" = "新しいルーム"; "create_room_enable_encryption" = "暗号化を有効にする"; "room_details_room_name_for_dm" = "名前"; -"room_participants_security_information_room_encrypted_for_dm" = "ここで送受信されるメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; +"room_participants_security_information_room_encrypted_for_dm" = "ここでのメッセージはエンドツーエンドで暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; "room_participants_security_information_room_not_encrypted_for_dm" = "ここでのメッセージはエンドツーエンドで暗号化されていません。"; // Mark: - Room creation introduction cell @@ -684,7 +684,7 @@ "identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して接続解除"; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー %@ で個人データを共有しています。\n\n接続を解除する前に、メールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; "identity_server_settings_alert_disconnect_button" = "接続を解除"; -"identity_server_settings_alert_disconnect" = "IDサーバー %@ との接続を解除しますか?"; +"identity_server_settings_alert_disconnect" = "IDサーバー %@ から切断しますか?"; "identity_server_settings_alert_disconnect_title" = "IDサーバーから接続を解除"; "identity_server_settings_alert_change" = "IDサーバー %1$@ を切断し、代わりに %2$@ に接続しますか?"; "identity_server_settings_alert_change_title" = "IDサーバーを変更"; @@ -719,7 +719,7 @@ "room_multiple_typing_notification" = "%@とその他のメンバー"; "external_link_confirmation_message" = "リンク %@ は別のサイトに移動します:%@\n\n続行してよろしいですか?"; "room_event_action_delete_confirmation_title" = "未送信のメッセージを削除"; -"room_unsent_messages_cancel_message" = "このルームにある未送信のメッセージを全て削除してもよろしいですか?"; +"room_unsent_messages_cancel_message" = "このルームの全ての未送信のメッセージを削除してよろしいですか?"; "room_unsent_messages_cancel_title" = "未送信のメッセージを削除"; "room_message_replying_to" = "%@に返信しています"; "room_message_editing" = "編集中"; @@ -751,11 +751,11 @@ // Errors "error_user_already_logged_in" = "他のホームサーバーに接続しようとしているようです。サインアウトしますか?"; -"social_login_button_title_sign_up" = "%@で登録"; +"social_login_button_title_sign_up" = "%@でサインアップ"; "social_login_button_title_sign_in" = "%@でサインイン"; "social_login_button_title_continue" = "%@で続行"; -"social_login_list_title_sign_up" = "もしくは"; -"social_login_list_title_sign_in" = "もしくは"; +"social_login_list_title_sign_up" = "または"; +"social_login_list_title_sign_in" = "または"; // Social login @@ -1240,7 +1240,7 @@ "notice_redaction" = "%@はイベントを編集しました(id:%@)"; "notice_error_unsupported_event" = "サポートされていないイベント"; "notice_error_unexpected_event" = "予期しないイベント"; -"notice_error_unknown_event_type" = "不明なイベントタイプ"; +"notice_error_unknown_event_type" = "イベントの種類が不明です"; "notice_room_history_visible_to_anyone" = "%@が今後のルームの履歴を「誰でも」閲覧可能に設定しました。"; "notice_room_history_visible_to_members" = "%@が今後のルームの履歴を「メンバーのみ」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point" = "%@が今後のルームの履歴を「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; @@ -1438,9 +1438,9 @@ "create_room" = "ルームを作成"; "login" = "ログイン"; "create_account" = "アカウントを作成"; -"membership_invite" = "招待しました"; -"membership_leave" = "退出しました"; -"membership_ban" = "ブロックしました"; +"membership_invite" = "招待済"; +"membership_leave" = "退出済"; +"membership_ban" = "ブロック済"; "num_members_one" = "%@人のユーザー"; "num_members_other" = "%@人のユーザー"; "kick" = "会話から追放"; @@ -1590,7 +1590,7 @@ "side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; -"onboarding_splash_login_button_title" = "既にアカウントを持っています"; +"onboarding_splash_login_button_title" = "既にアカウントがあります"; // Onboarding "onboarding_splash_register_button_title" = "アカウントを作成"; @@ -1616,8 +1616,8 @@ "spaces_add_space_title" = "スペースを作成"; "spaces_creation_address" = "アドレス"; "spaces_creation_visibility_message" = "既存のスペースに参加するには、招待が必要です。"; -"spaces_creation_footer" = "この設定は後から変更できます"; -"onboarding_display_name_hint" = "この設定は後から変更できます"; +"spaces_creation_footer" = "これは後から変更できます"; +"onboarding_display_name_hint" = "これは後から変更できます"; "spaces_creation_visibility_title" = "作成するスペースの種類を選択してください"; "space_public_join_rule_detail" = "誰でも参加可能。コミュニティー向け"; "space_private_join_rule_detail" = "招待者のみ参加可能。個人やチーム向け"; @@ -1650,7 +1650,7 @@ "spaces_creation_sharing_type_just_me_detail" = "ルームを整理するための非公開のスペース"; "spaces_creation_sharing_type_just_me_title" = "自分専用"; "spaces_creation_sharing_type_message" = "適切な人が %@ アクセスできることを確認してください。この設定は後から変更できます。"; -"spaces_creation_settings_message" = "詳細を入力してください。この設定は後から変更できます。"; +"spaces_creation_settings_message" = "詳細を入力してください。これはいつでも変更できます。"; "spaces_creation_address_default_message" = "スペースは以下で閲覧可能になります\n%@"; "space_settings_current_address_message" = "スペースは以下で閲覧できます\n%@"; "space_topic" = "詳細"; @@ -1826,7 +1826,7 @@ "room_notifs_settings_manage_notifications" = "通知は%@で管理できます"; "room_access_settings_screen_upgrade_alert_auto_invite_switch" = "メンバーを新しいルームに自動的に招待"; "room_access_settings_screen_upgrade_alert_note" = "アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。"; -"room_access_settings_screen_upgrade_alert_message_no_param" = "親のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; +"room_access_settings_screen_upgrade_alert_message_no_param" = "上位のスペースに属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; "room_access_settings_screen_upgrade_alert_message" = "%@に属する人は、誰でもこのルームを検索し、参加することができます。手動で全員を招待する必要はありません。これはルームの設定からいつでも変更できます。"; // Room Access Settings @@ -1911,7 +1911,7 @@ "authentication_choose_password_input_title" = "パスワードを選択"; "authentication_forgot_password_waiting_button" = "電子メールを再送信"; /* The placeholder will show the email address that was entered. */ -"authentication_forgot_password_waiting_message" = "%@に送信された手順に従ってください"; +"authentication_forgot_password_waiting_message" = "%@に送信された手順に従ってください。"; "authentication_forgot_password_waiting_title" = "電子メールを確認してください。"; "authentication_forgot_password_text_field_placeholder" = "メールアドレス"; /* The placeholder will show the homeserver's domain */ @@ -1920,7 +1920,7 @@ "authentication_verify_email_waiting_button" = "電子メールを再送信"; "authentication_verify_email_waiting_hint" = "電子メールが届いていませんか?"; /* The placeholder will show the email address that was entered. */ -"authentication_verify_email_waiting_message" = "%@に送信された手順に従ってください"; +"authentication_verify_email_waiting_message" = "%@に送信された手順に従ってください。"; "authentication_verify_email_waiting_title" = "メールアドレスを認証してください。"; "authentication_verify_email_text_field_placeholder" = "メールアドレス"; /* The placeholder will show the homeserver's domain */ @@ -1992,7 +1992,7 @@ // Banner "secure_backup_setup_banner_title" = "セキュアバックアップ"; -"secure_key_backup_setup_cancel_alert_message" = "いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。\n\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。"; +"secure_key_backup_setup_cancel_alert_message" = "いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。\n\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。"; // Cancel @@ -2291,7 +2291,7 @@ // User -"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者に解読することはできません。"; +"key_verification_verified_user_information" = "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。"; "key_verification_verified_new_session_title" = "新しいセッションを認証しました!"; "device_verification_verified_got_it_button" = "了解"; @@ -2554,7 +2554,7 @@ /* The placeholder will show the full Matrix ID that has been entered. */ "authentication_registration_username_footer_available" = "他の人は %@ であなたを見つけることができます"; "authentication_server_selection_register_message" = "あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます"; -"authentication_server_info_title_login" = "会話が実施される場所"; +"authentication_server_info_title_login" = "アカウントにサインインするサーバー"; "authentication_server_info_title" = "アカウントを作成するサーバー"; "onboarding_avatar_message" = "表示名にプロフィール画像を追加しましょう"; "all_chats_edit_layout_add_filters_message" = "あなたが選択したカテゴリーにメッセージを自動的にフィルタリング"; @@ -2622,8 +2622,8 @@ // Analytics "analytics_prompt_title" = "%@の改善を手伝う"; -"event_formatter_call_active_video" = "実行中のビデオ通話"; -"event_formatter_call_active_voice" = "実行中の音声通話"; +"event_formatter_call_active_video" = "実施中のビデオ通話"; +"event_formatter_call_active_voice" = "実施中の音声通話"; "launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)"; "create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; "create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索・参加できます。"; From 1dcc5f1c80884923432669322853981969752607 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Mon, 6 Feb 2023 17:43:15 +0000 Subject: [PATCH 455/468] Translated using Weblate (Albanian) Currently translated at 99.6% (2369 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index a7a3038ae..4e6a4269e 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2704,6 +2704,10 @@ "launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%"; "key_backup_recover_from_private_key_progress" = "Plotësuar %@%%"; "room_details_polls" = "Historik pyetësorësh"; -"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj 2.0 (që ta çaktivizoni, dilni)"; -"settings_labs_confirm_crypto_sdk" = "Kjo mundësi do të aktivizojë për fshhehtëzim skaj-më-skaj një motor të ri, më të shpejtë dhe më të qëndrueshëm, të shkruar në Rust. Pasi të aktivizohet, do t’ju duhet të bëni daljen nga llogaria, që ta çaktivizoni. Doni të vazhdohet?"; -"settings_labs_enable_crypto_sdk" = "Fshehtëzim skaj-më-skaj 2.0"; +"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust (që ta çaktivizoni, dilni)"; +"settings_labs_confirm_crypto_sdk" = "Ju lutemi, kini parasysh se kjo veçori është ende në fazë eksperimentale, mund të mos funksionojë siç pritet dhe mundet, në potencial, të ketë pasojë të paparashikuara. Që ta prapaktheni këtë veçori, thjesht dilni nga llogaria dhe rihyni. Përdoreni me përgjegjësinë tuaj dhe me kujdes."; +"settings_labs_enable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust"; +"settings_push_rules_error" = "Ndodhi një gabim, kur përditësoheshin parapëlqimet tuaja për njoftime. JU lutemi, provoni të aktivizoni mundësi tuaj sërish."; +"wysiwyg_composer_format_action_un_indent" = "Zvogëlo shmangie kryeradhë"; +"wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe"; +"poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore"; From 5bf85217c3c4ba920d05d76fa3e33b474db6d7d2 Mon Sep 17 00:00:00 2001 From: keda82 Date: Mon, 6 Feb 2023 19:51:52 +0000 Subject: [PATCH 456/468] Translated using Weblate (Swedish) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 13ee80174..26e6eacfa 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2662,3 +2662,9 @@ "settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust"; "accessibility_selected" = "vald"; "settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök att växla dina alternativ igen."; +"wysiwyg_composer_format_action_un_indent" = "Minska indrag"; +"wysiwyg_composer_format_action_indent" = "Öka indrag"; +"poll_history_detail_view_in_timeline" = "Visa omröstning i tidslinje"; +"voice_broadcast_playback_unable_to_decrypt" = "Kunde inte avkryptera denna röstsändning."; +"home_context_menu_mark_as_unread" = "Markera som oläst"; +"key_backup_recover_from_private_key_progress" = "%@%% Färdig"; From 02cb377aa9ed0088984e88683bcda77841617a8a Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 6 Feb 2023 19:51:09 +0000 Subject: [PATCH 457/468] Translated using Weblate (Swedish) Currently translated at 100.0% (2378 of 2378 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 26e6eacfa..a8fa6cf8b 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2657,8 +2657,8 @@ "launch_loading_migrating_data" = "Migrerar data\n%@ %%"; "room_details_polls" = "Omröstningshistorik"; -"settings_labs_disable_crypto_sdk" = "Krypto-SDK är aktiverad. För att inaktivera, vänligen installera om appen"; -"settings_labs_confirm_crypto_sdk" = "Vänligen notera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att gå tillbaka, logga ut och logga sedan in igen. Använd på egen risk."; +"settings_labs_disable_crypto_sdk" = "Totalsträckskryptering i Rust (logga ut för att stänga av)"; +"settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk."; "settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust"; "accessibility_selected" = "vald"; "settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök att växla dina alternativ igen."; From 77099dc770f60ff7fdcd61a1516304ee3fdf9db2 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Feb 2023 10:48:37 +0000 Subject: [PATCH 458/468] Fix placeholder mismatches. --- Riot/Assets/ja.lproj/Vector.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 9202162b8..30c415fd8 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1504,7 +1504,7 @@ "ssl_only_accept" = "サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。"; "unignore" = "無視を解除"; "notice_encryption_enabled_ok" = "%@がエンドツーエンド暗号化を有効にしました。"; -"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %@)を有効にしました。"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@がエンドツーエンド暗号化(認識されていないアルゴリズム %2$@)を有効にしました。"; "device_details_rename_prompt_title" = "セッション名"; "account_error_push_not_allowed" = "通知は許可されていません"; "notice_room_third_party_revoked_invite" = "%@が%@のルームへの招待を取り消しました"; @@ -1856,7 +1856,7 @@ "settings_labs_enable_threads" = "メッセージのスレッド機能"; "settings_labs_enabled_polls" = "アンケート"; "settings_ui_show_redactions_in_room_history" = "削除されたメッセージに関する通知を表示"; -"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は %@ を許可(IPアドレスが通話中に共有されます)。"; +"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバック用の通話アシストサーバーを提供していない場合は %@ を許可(IPアドレスが通話中に共有されます)。"; "settings_callkit_info" = "ロック画面に着信を表示。%@の着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; "settings_notifications_disabled_alert_title" = "通知が無効です"; "threads_discourage_information_1" = "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。 "; @@ -1897,7 +1897,7 @@ "authentication_qr_login_start_title" = "QRコードをスキャン"; "authentication_terms_policy_url_error" = "選択した運営方針が見つかりませんでした。後でもう一度やり直してください。"; /* The placeholder will show the homeserver's domain */ -"authentication_terms_message" = "%sの利用規約と運営方針を確認してください"; +"authentication_terms_message" = "%@の利用規約と運営方針を確認してください"; "authentication_verify_msisdn_invalid_phone_number" = "電話番号が不正です"; /* The placeholder will show the phone number that was entered. */ "authentication_verify_msisdn_waiting_message" = "コードが%@に送信されました"; @@ -2390,7 +2390,7 @@ /* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ "user_session_item_details" = "%1$@ · %2$@"; -"user_other_session_selected_count" = "%d件選択済"; +"user_other_session_selected_count" = "%@件選択済"; "user_session_inactive_session_description" = "非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。\n\n使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。"; "user_session_permanently_unverified_session_description" = "このセッションは暗号化をサポートしていないため、認証できません。\n\nこのセッションでは、暗号化が有効になっているルームに参加することができません。\n\nセキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。"; "user_sessions_overview_security_recommendations_unverified_info" = "未認証のセッションを認証するか、サインアウトしてください。"; From 011c8e1794481ddf402e2e79e5642a57027cfae7 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Feb 2023 12:57:52 +0000 Subject: [PATCH 459/468] changelog.d: Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index 2330eb1b8..d910bb9e6 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.25.0' +$matrixSDKVersion = '= 0.25.1' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..628b34c40 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)). \ No newline at end of file From 2649e8cd625b729c0eeabf78b555ef18807853d5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Feb 2023 12:57:52 +0000 Subject: [PATCH 460/468] version++ --- CHANGES.md | 28 ++++++++++++++++++++++++++++ changelog.d/6597.change | 1 - changelog.d/7189.change | 1 - changelog.d/7253.feature | 1 - changelog.d/7298.change | 1 - changelog.d/7311.change | 1 - changelog.d/7316.change | 1 - changelog.d/pr-7293.change | 1 - changelog.d/pr-7303.change | 1 - changelog.d/pr-7310.change | 1 - changelog.d/pr-7314.change | 1 - changelog.d/pr-7319.change | 1 - changelog.d/pr-7320.change | 1 - changelog.d/pr-7323.change | 1 - changelog.d/pr-7324.change | 1 - changelog.d/pr-7332.change | 1 - changelog.d/pr-7333.change | 1 - changelog.d/pr-7335.change | 1 - changelog.d/pr-7341.change | 1 - changelog.d/x-nolink-0.change | 1 - 20 files changed, 28 insertions(+), 19 deletions(-) delete mode 100644 changelog.d/6597.change delete mode 100644 changelog.d/7189.change delete mode 100644 changelog.d/7253.feature delete mode 100644 changelog.d/7298.change delete mode 100644 changelog.d/7311.change delete mode 100644 changelog.d/7316.change delete mode 100644 changelog.d/pr-7293.change delete mode 100644 changelog.d/pr-7303.change delete mode 100644 changelog.d/pr-7310.change delete mode 100644 changelog.d/pr-7314.change delete mode 100644 changelog.d/pr-7319.change delete mode 100644 changelog.d/pr-7320.change delete mode 100644 changelog.d/pr-7323.change delete mode 100644 changelog.d/pr-7324.change delete mode 100644 changelog.d/pr-7332.change delete mode 100644 changelog.d/pr-7333.change delete mode 100644 changelog.d/pr-7335.change delete mode 100644 changelog.d/pr-7341.change delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index c24df14b8..af14f69c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +## Changes in 1.10.1 (2023-02-07) + +✨ Features + +- Add mark as unread option for rooms ([#7253](https://github.com/vector-im/element-ios/issues/7253)) + +🙌 Improvements + +- Polls: add logic for fetching poll histories in rooms. ([#7293](https://github.com/vector-im/element-ios/pull/7293)) +- Poll: add a feature to load more polls in the poll history. ([#7303](https://github.com/vector-im/element-ios/pull/7303)) +- CryptoV2: Generate Crypto SDK store key ([#7310](https://github.com/vector-im/element-ios/pull/7310)) +- Poll: added poll detail in poll list hisotry with navigation to timeline ([#7314](https://github.com/vector-im/element-ios/pull/7314)) +- Backup: Display backup import progress ([#7319](https://github.com/vector-im/element-ios/pull/7319)) +- Polls: sync push rules with the one of normal messages. ([#7320](https://github.com/vector-im/element-ios/pull/7320)) +- CryptoV2: Reset Crypto SDK on logout ([#7323](https://github.com/vector-im/element-ios/pull/7323)) +- Polls: add error handling when syncing push rules with the ones of normal messages. ([#7324](https://github.com/vector-im/element-ios/pull/7324)) +- CryptoV2: Refresh notification service on crypto change ([#7332](https://github.com/vector-im/element-ios/pull/7332)) +- CryptoV2: Enable Crypto SDK for production ([#7333](https://github.com/vector-im/element-ios/pull/7333)) +- Polls: add automatic synchronization logic for poll push rules. ([#7335](https://github.com/vector-im/element-ios/pull/7335)) +- Polls: update poll history UI. ([#7341](https://github.com/vector-im/element-ios/pull/7341)) +- Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)). +- Hide the presence info if the presence status is unknown. ([#6597](https://github.com/vector-im/element-ios/issues/6597)) +- Inform the user about decryption errors during a voice broadcast. ([#7189](https://github.com/vector-im/element-ios/issues/7189)) +- App Layout: Removed the onboarding flow ([#7298](https://github.com/vector-im/element-ios/issues/7298)) +- Improve error handling during a voice broadcast playback. ([#7311](https://github.com/vector-im/element-ios/issues/7311)) +- Labs: Rich text editor: enable list items indentation ([#7316](https://github.com/vector-im/element-ios/issues/7316)) + + ## Changes in 1.10.0 (2023-02-02) 🙌 Improvements diff --git a/changelog.d/6597.change b/changelog.d/6597.change deleted file mode 100644 index 46ca176a7..000000000 --- a/changelog.d/6597.change +++ /dev/null @@ -1 +0,0 @@ -Hide the presence info if the presence status is unknown. diff --git a/changelog.d/7189.change b/changelog.d/7189.change deleted file mode 100644 index 98ffa1e03..000000000 --- a/changelog.d/7189.change +++ /dev/null @@ -1 +0,0 @@ -Inform the user about decryption errors during a voice broadcast. diff --git a/changelog.d/7253.feature b/changelog.d/7253.feature deleted file mode 100644 index 9bf47a493..000000000 --- a/changelog.d/7253.feature +++ /dev/null @@ -1 +0,0 @@ -Add mark as unread option for rooms \ No newline at end of file diff --git a/changelog.d/7298.change b/changelog.d/7298.change deleted file mode 100644 index a7f7a74ce..000000000 --- a/changelog.d/7298.change +++ /dev/null @@ -1 +0,0 @@ -App Layout: Removed the onboarding flow diff --git a/changelog.d/7311.change b/changelog.d/7311.change deleted file mode 100644 index d9b992237..000000000 --- a/changelog.d/7311.change +++ /dev/null @@ -1 +0,0 @@ -Improve error handling during a voice broadcast playback. diff --git a/changelog.d/7316.change b/changelog.d/7316.change deleted file mode 100644 index 4ef97e1bd..000000000 --- a/changelog.d/7316.change +++ /dev/null @@ -1 +0,0 @@ -Labs: Rich text editor: enable list items indentation diff --git a/changelog.d/pr-7293.change b/changelog.d/pr-7293.change deleted file mode 100644 index 4fe2717d2..000000000 --- a/changelog.d/pr-7293.change +++ /dev/null @@ -1 +0,0 @@ -Polls: add logic for fetching poll histories in rooms. diff --git a/changelog.d/pr-7303.change b/changelog.d/pr-7303.change deleted file mode 100644 index fc6bbdb3a..000000000 --- a/changelog.d/pr-7303.change +++ /dev/null @@ -1 +0,0 @@ -Poll: add a feature to load more polls in the poll history. diff --git a/changelog.d/pr-7310.change b/changelog.d/pr-7310.change deleted file mode 100644 index 4ba5e9ee1..000000000 --- a/changelog.d/pr-7310.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Generate Crypto SDK store key diff --git a/changelog.d/pr-7314.change b/changelog.d/pr-7314.change deleted file mode 100644 index fd3dc46e9..000000000 --- a/changelog.d/pr-7314.change +++ /dev/null @@ -1 +0,0 @@ -Poll: added poll detail in poll list hisotry with navigation to timeline diff --git a/changelog.d/pr-7319.change b/changelog.d/pr-7319.change deleted file mode 100644 index 187b315b5..000000000 --- a/changelog.d/pr-7319.change +++ /dev/null @@ -1 +0,0 @@ -Backup: Display backup import progress diff --git a/changelog.d/pr-7320.change b/changelog.d/pr-7320.change deleted file mode 100644 index 3d34c84e2..000000000 --- a/changelog.d/pr-7320.change +++ /dev/null @@ -1 +0,0 @@ -Polls: sync push rules with the one of normal messages. diff --git a/changelog.d/pr-7323.change b/changelog.d/pr-7323.change deleted file mode 100644 index 308cf2813..000000000 --- a/changelog.d/pr-7323.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Reset Crypto SDK on logout diff --git a/changelog.d/pr-7324.change b/changelog.d/pr-7324.change deleted file mode 100644 index 1d7133eb8..000000000 --- a/changelog.d/pr-7324.change +++ /dev/null @@ -1 +0,0 @@ -Polls: add error handling when syncing push rules with the ones of normal messages. diff --git a/changelog.d/pr-7332.change b/changelog.d/pr-7332.change deleted file mode 100644 index 94a5bdc89..000000000 --- a/changelog.d/pr-7332.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Refresh notification service on crypto change diff --git a/changelog.d/pr-7333.change b/changelog.d/pr-7333.change deleted file mode 100644 index fbf81e873..000000000 --- a/changelog.d/pr-7333.change +++ /dev/null @@ -1 +0,0 @@ -CryptoV2: Enable Crypto SDK for production diff --git a/changelog.d/pr-7335.change b/changelog.d/pr-7335.change deleted file mode 100644 index 62512e930..000000000 --- a/changelog.d/pr-7335.change +++ /dev/null @@ -1 +0,0 @@ -Polls: add automatic synchronization logic for poll push rules. diff --git a/changelog.d/pr-7341.change b/changelog.d/pr-7341.change deleted file mode 100644 index 2129cec32..000000000 --- a/changelog.d/pr-7341.change +++ /dev/null @@ -1 +0,0 @@ -Polls: update poll history UI. diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 628b34c40..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)). \ No newline at end of file From 1bcd8efba0cc133e7f8ce941e70e219c94377b6d Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Feb 2023 15:11:52 +0000 Subject: [PATCH 461/468] finish version++ --- Podfile.lock | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index a8c7f04f8..e1d507e8d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -21,23 +21,6 @@ PODS: - Down (0.11.0) - DSBottomSheet (0.3.0) - DSWaveformImage (6.1.1) - - DTCoreText (1.6.26): - - DTCoreText/Core (= 1.6.26) - - DTFoundation/Core (~> 1.7.5) - - DTFoundation/DTAnimatedGIF (~> 1.7.5) - - DTFoundation/DTHTMLParser (~> 1.7.5) - - DTFoundation/UIKit (~> 1.7.5) - - DTCoreText/Core (1.6.26): - - DTFoundation/Core (~> 1.7.5) - - DTFoundation/DTAnimatedGIF (~> 1.7.5) - - DTFoundation/DTHTMLParser (~> 1.7.5) - - DTFoundation/UIKit (~> 1.7.5) - - DTFoundation/Core (1.7.18) - - DTFoundation/DTAnimatedGIF (1.7.18) - - DTFoundation/DTHTMLParser (1.7.18): - - DTFoundation/Core - - DTFoundation/UIKit (1.7.18): - - DTFoundation/Core - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - GBDeviceInfo (7.1.0): @@ -55,9 +38,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.25.0): - - MatrixSDK/Core (= 0.25.0) - - MatrixSDK/Core (0.25.0): + - MatrixSDK (0.25.1): + - MatrixSDK/Core (= 0.25.1) + - MatrixSDK/Core (0.25.1): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -65,7 +48,7 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.25.0): + - MatrixSDK/JingleCallStack (0.25.1): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - MatrixSDKCrypto (0.2.0) @@ -112,7 +95,6 @@ DEPENDENCIES: - Down (~> 0.11.0) - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) - - DTCoreText (~> 1.6.25) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 7.1.0) @@ -120,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.25.0) - - MatrixSDK/JingleCallStack (= 0.25.0) + - MatrixSDK (= 0.25.1) + - MatrixSDK/JingleCallStack (= 0.25.1) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -148,8 +130,6 @@ SPEC REPOS: - Down - DSBottomSheet - DSWaveformImage - - DTCoreText - - DTFoundation - FLEX - FlowCommoniOS - GBDeviceInfo @@ -203,8 +183,6 @@ SPEC CHECKSUMS: Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce - DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce - DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 @@ -218,7 +196,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: a9d05e760434eff941bbb35164cffb01b3f94b63 + MatrixSDK: 823c5c2ef8b8a769c30fa62e1be8ec801e6312e7 MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f @@ -239,6 +217,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 916221b3e9512715d5e1e1e310a0aa0552e1f0f1 +PODFILE CHECKSUM: becc7a1d080df477982664af957cdc02ff843c56 COCOAPODS: 1.11.3 From 1a33d6976d44a3c3fd9a9f528fe273ac5e109ea8 Mon Sep 17 00:00:00 2001 From: Flescio Date: Thu, 9 Feb 2023 09:01:16 +0100 Subject: [PATCH 462/468] "Mark as unread" dot appears on rooms that are actually unread, not marked as such (#7352) * fix green dot only to appear for marked action --- .../Common/Recents/Views/RecentTableViewCell.m | 15 +++++++++------ .../MatrixKit/Models/RoomList/MXKRecentCellData.m | 8 ++++++-- .../Models/RoomList/MXKRecentCellDataStoring.h | 1 + changelog.d/7530.bugfix | 1 + 4 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 changelog.d/7530.bugfix diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index d1ac3d914..a21996333 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -92,12 +92,17 @@ self.lastEventDecriptionLabelTrailingConstraint.constant = self.unsentImageView.hidden ? 10 : 30; // Notify unreads and bing - if (roomCellData.hasUnread) + if (roomCellData.isRoomMarkedAsUnread) { - + self.missedNotifAndUnreadBadgeBgView.hidden = NO; + self.missedNotifAndUnreadBadgeBgView.backgroundColor = ThemeService.shared.theme.tintColor; + self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 20; + } + else if (roomCellData.hasUnread) + { + self.missedNotifAndUnreadIndicator.hidden = NO; if (0 < roomCellData.notificationCount) { - self.missedNotifAndUnreadIndicator.hidden = NO; self.missedNotifAndUnreadIndicator.backgroundColor = roomCellData.highlightCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor; self.missedNotifAndUnreadBadgeBgView.hidden = NO; @@ -110,9 +115,7 @@ } else { - self.missedNotifAndUnreadBadgeBgView.hidden = NO; - self.missedNotifAndUnreadBadgeBgView.backgroundColor = ThemeService.shared.theme.tintColor; - self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 20; + self.missedNotifAndUnreadIndicator.backgroundColor = ThemeService.shared.theme.unreadRoomIndentColor; } // Use bold font for the room title diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m index 4e6ebbe80..474494d59 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m @@ -63,8 +63,12 @@ - (BOOL)hasUnread { - bool isRoomUnread = [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId]; - return (roomSummary.localUnreadEventCount != 0 || isRoomUnread); + return (roomSummary.localUnreadEventCount != 0); +} + +- (BOOL)isRoomMarkedAsUnread +{ + return [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId];; } - (NSString *)roomIdentifier diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h index 7185ae4eb..3c417c1fa 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellDataStoring.h @@ -50,6 +50,7 @@ @property (nonatomic, readonly) NSString *lastEventDate; @property (nonatomic, readonly) BOOL hasUnread; +@property (nonatomic, readonly) BOOL isRoomMarkedAsUnread; @property (nonatomic, readonly) NSUInteger notificationCount; @property (nonatomic, readonly) NSUInteger highlightCount; @property (nonatomic, readonly) NSString *notificationCountStringValue; diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix new file mode 100644 index 000000000..7309798a9 --- /dev/null +++ b/changelog.d/7530.bugfix @@ -0,0 +1 @@ +Fixes #7350 - Fix green dot only to appear for marked action \ No newline at end of file From 599e20ea01b3ece43ced1b3fdeddb2104734f384 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 8 Feb 2023 10:39:51 +0000 Subject: [PATCH 463/468] Fix some crashes --- .../AllChats/AllChatsViewController.swift | 28 +++++++++++-------- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 25 +++++++++++++---- .../MXRoomNotificationSettingsService.swift | 18 +++++++----- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index a424c8346..2ec3bbe8b 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -196,7 +196,7 @@ class AllChatsViewController: HomeViewController { searchController.isActive = false guard let spaceId = spaceId else { - self.dataSource?.currentSpace = nil + dataSource?.currentSpace = nil updateUI() return @@ -207,7 +207,7 @@ class AllChatsViewController: HomeViewController { return } - self.dataSource.currentSpace = space + dataSource?.currentSpace = space updateUI() self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) @@ -288,7 +288,7 @@ class AllChatsViewController: HomeViewController { @objc private func showSpaceSelectorAction(sender: AnyObject) { Analytics.shared.viewRoomTrigger = .roomList - let currentSpaceId = self.dataSource.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId + let currentSpaceId = dataSource?.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true) spaceSelectorBridgePresenter.present(from: self, animated: true) spaceSelectorBridgePresenter.delegate = self @@ -310,7 +310,7 @@ class AllChatsViewController: HomeViewController { return super.tableView(tableView, numberOfRowsInSection: section) } - return dataSource.tableView(tableView, numberOfRowsInSection: section) + return dataSource?.tableView(tableView, numberOfRowsInSection: section) ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -318,6 +318,10 @@ class AllChatsViewController: HomeViewController { return super.tableView(tableView, cellForRowAt: indexPath) } + guard let dataSource = dataSource else { + MXLog.failure("Missing data source") + return UITableViewCell() + } return dataSource.tableView(tableView, cellForRowAt: indexPath) } @@ -328,7 +332,7 @@ class AllChatsViewController: HomeViewController { return super.tableView(tableView, heightForRowAt: indexPath) } - return dataSource.cellHeight(at: indexPath) + return dataSource?.cellHeight(at: indexPath) ?? 0 } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -583,7 +587,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceInvite() { - guard let session = mainSession, let spaceRoom = dataSource.currentSpace?.room else { + guard let session = mainSession, let spaceRoom = dataSource?.currentSpace?.room else { return } @@ -595,7 +599,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceMembers() { - guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else { + guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } @@ -609,7 +613,7 @@ class AllChatsViewController: HomeViewController { } private func showSpaceSettings() { - guard let session = mainSession, let spaceId = dataSource.currentSpace?.spaceId else { + guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else { return } @@ -630,7 +634,7 @@ class AllChatsViewController: HomeViewController { } private func showLeaveSpace() { - guard let session = mainSession, let spaceSummary = dataSource.currentSpace?.summary else { + guard let session = mainSession, let spaceSummary = dataSource?.currentSpace?.summary else { return } @@ -714,11 +718,11 @@ extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePrese extension AllChatsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let searchText = searchController.searchBar.text, !searchText.isEmpty else { - self.dataSource.search(withPatterns: nil) + self.dataSource?.search(withPatterns: nil) return } - self.dataSource.search(withPatterns: [searchText]) + self.dataSource?.search(withPatterns: [searchText]) } } @@ -754,7 +758,7 @@ extension AllChatsViewController: AllChatsEditActionProviderDelegate { case .startChat: startChat() case .createSpace: - showCreateSpace(parentSpaceId: dataSource.currentSpace?.spaceId) + showCreateSpace(parentSpaceId: dataSource?.currentSpace?.spaceId) } } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 5f33af838..0998122ae 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2150,7 +2150,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } @@ -2177,7 +2180,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { NSURL *localFileURL = [NSURL URLWithString:localFilePath]; if (![NSFileManager.defaultManager fileExistsAtPath:localFilePath]) { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidLocalFilePath userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidLocalFilePath userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend voice message, invalid file path.", self); return; } @@ -2247,7 +2253,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } @@ -2259,13 +2268,19 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType); } } else { - failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + if (failure) + { + failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]); + } MXLogWarning(@"[MXKRoomDataSource][%p] MXKRoomDataSource: Warning - Only resend of MXEventTypeRoomMessage is allowed. Event.type: %@", self, event.type); } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift index e9d192b49..caaa45a53 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift @@ -30,6 +30,10 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy private var observers: [ObjectIdentifier] = [] + private var notificationCenter: MXNotificationCenter? { + room.mxSession?.notificationCenter + } + // MARK: Public var notificationState: RoomNotificationState { @@ -166,7 +170,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.addRoomRule( + notificationCenter?.addRoomRule( room.roomId, notify: false, sound: false, @@ -184,7 +188,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.addOverrideRule( + notificationCenter?.addOverrideRule( withId: roomId, conditions: [["kind": "event_match", "key": "room_id", "pattern": roomId]], notify: false, @@ -196,11 +200,11 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy private func removePushRule(rule: MXPushRule, completion: @escaping Completion) { handleUpdateCallback(completion) { [weak self] in guard let self = self else { return true } - return self.room.mxSession.notificationCenter.rule(byId: rule.ruleId) == nil + return self.notificationCenter?.rule(byId: rule.ruleId) == nil } handleFailureCallback(completion) - room.mxSession.notificationCenter.removeRule(rule) + notificationCenter?.removeRule(rule) } private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) { @@ -210,7 +214,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } handleFailureCallback(completion) - room.mxSession.notificationCenter.enableRule(rule, isEnabled: true) + notificationCenter?.enableRule(rule, isEnabled: true) } private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) { @@ -283,14 +287,14 @@ private extension MXRoom { } var overridePushRule: MXPushRule? { - guard let overrideRules = mxSession.notificationCenter.rules.global.override else { + guard let overrideRules = mxSession?.notificationCenter?.rules?.global?.override else { return nil } return getRoomRule(from: overrideRules) } var roomPushRule: MXPushRule? { - guard let roomRules = mxSession.notificationCenter.rules.global.room else { + guard let roomRules = mxSession?.notificationCenter?.rules?.global?.room else { return nil } return getRoomRule(from: roomRules) From 89da453433d8bd637f2d3914cfcf824ae9067865 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 8 Feb 2023 15:28:19 +0000 Subject: [PATCH 464/468] More crashes --- Riot/Categories/MXBugReportRestClient+Riot.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index 93eff76aa..2c4649869 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -46,10 +46,10 @@ extension MXBugReportRestClient { // User info (TODO: handle multi-account and find a way to expose them in rageshake API) var userInfo = [String: String]() let mainAccount = MXKAccountManager.shared().accounts.first - if let userId = mainAccount?.mxSession.myUser.userId { + if let userId = mainAccount?.mxSession?.myUser?.userId { userInfo["user_id"] = userId } - if let deviceId = mainAccount?.mxSession.matrixRestClient.credentials.deviceId { + if let deviceId = mainAccount?.mxSession?.myDeviceId { userInfo["device_id"] = deviceId } From 0588f5bca72baa2205d53e9085706d5dd17326c0 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 8 Feb 2023 16:07:48 +0000 Subject: [PATCH 465/468] Crashes in verification view models --- ...erificationSelfVerifyWaitCoordinator.swift | 5 +- ...yVerificationSelfVerifyWaitViewModel.swift | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift index 88c2537db..68dcab40e 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift @@ -25,7 +25,6 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW // MARK: Private - private let session: MXSession private var keyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWaitViewModelType private let keyVerificationSelfVerifyWaitViewController: KeyVerificationSelfVerifyWaitViewController private let cancellable: Bool @@ -40,9 +39,7 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW // MARK: - Setup init(session: MXSession, isNewSignIn: Bool, cancellable: Bool) { - self.session = session - - let keyVerificationSelfVerifyWaitViewModel = KeyVerificationSelfVerifyWaitViewModel(session: self.session, isNewSignIn: isNewSignIn) + let keyVerificationSelfVerifyWaitViewModel = KeyVerificationSelfVerifyWaitViewModel(session: session, isNewSignIn: isNewSignIn) let keyVerificationSelfVerifyWaitViewController = KeyVerificationSelfVerifyWaitViewController.instantiate(with: keyVerificationSelfVerifyWaitViewModel, cancellable: cancellable) self.keyVerificationSelfVerifyWaitViewModel = keyVerificationSelfVerifyWaitViewModel self.keyVerificationSelfVerifyWaitViewController = keyVerificationSelfVerifyWaitViewController diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift index b064d4f84..5d4830bed 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift @@ -26,11 +26,19 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai private let session: MXSession private let keyVerificationService: KeyVerificationService - private let verificationManager: MXKeyVerificationManager + private let verificationManager: MXKeyVerificationManager? private let isNewSignIn: Bool - private var secretsRecoveryAvailability: SecretsRecoveryAvailability + private var secretsRecoveryAvailability: SecretsRecoveryAvailability? private var keyVerificationRequest: MXKeyVerificationRequest? + private var myUserId: String { + guard let userId = session.myUserId else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] userId is missing") + return "" + } + return userId + } + // MARK: Public weak var viewDelegate: KeyVerificationSelfVerifyWaitViewModelViewDelegate? @@ -40,10 +48,10 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai init(session: MXSession, isNewSignIn: Bool) { self.session = session - self.verificationManager = session.crypto.keyVerificationManager + self.verificationManager = session.crypto?.keyVerificationManager self.keyVerificationService = KeyVerificationService() self.isNewSignIn = isNewSignIn - self.secretsRecoveryAvailability = session.crypto.recoveryService.vc_availability + self.secretsRecoveryAvailability = session.crypto?.recoveryService.vc_availability } deinit { @@ -59,9 +67,16 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai case .cancel: self.cancel() case .recoverSecrets: - switch self.secretsRecoveryAvailability { + guard let availability = secretsRecoveryAvailability else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] process: secretsRecoveryAvailability not set") + self.cancel() + return + } + + switch availability { case .notAvailable: - fatalError("Should not happen: When recovery is not available button is hidden") + MXLog.error("Should not happen: When recovery is not available button is hidden") + self.cancel() case .available(let secretsRecoveryMode): self.coordinatorDelegate?.keyVerificationSelfVerifyWaitViewModel(self, wantsToRecoverSecretsWith: secretsRecoveryMode) } @@ -71,12 +86,16 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai // MARK: - Private private func loadData() { + guard let verificationManager = verificationManager else { + MXLog.failure("Verification manager is not set") + return + } if !self.isNewSignIn { MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices") let keyVerificationService = KeyVerificationService() - self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in + verificationManager.requestVerificationByToDevice(withUserId: self.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in guard let self = self else { return } @@ -103,7 +122,7 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting") let keyVerificationService = KeyVerificationService() - self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in + verificationManager.requestVerificationByToDevice(withUserId: self.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in guard let self = self else { return } @@ -132,12 +151,18 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai } private func continueLoadData() { + guard let verificationManager = verificationManager, let recoveryService = session.crypto?.recoveryService else { + MXLog.error("[KeyVerificationSelfVerifyWaitViewModel] continueLoadData: Missing dependencies") + return + } + // update availability again - self.secretsRecoveryAvailability = session.crypto.recoveryService.vc_availability + let availability = recoveryService.vc_availability + self.secretsRecoveryAvailability = availability - let viewData = KeyVerificationSelfVerifyWaitViewData(isNewSignIn: self.isNewSignIn, secretsRecoveryAvailability: self.secretsRecoveryAvailability) + let viewData = KeyVerificationSelfVerifyWaitViewData(isNewSignIn: self.isNewSignIn, secretsRecoveryAvailability: availability) - self.registerKeyVerificationManagerNewRequestNotification(for: self.verificationManager) + self.registerKeyVerificationManagerNewRequestNotification(for: verificationManager) self.update(viewState: .loaded(viewData)) self.registerTransactionDidStateChangeNotification() self.registerKeyVerificationRequestChangeNotification() @@ -251,7 +276,7 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai @objc private func transactionDidStateChange(notification: Notification) { guard let sasTransaction = notification.object as? MXSASTransaction, - sasTransaction.isIncoming, sasTransaction.otherUserId == self.session.myUserId else { + sasTransaction.isIncoming, sasTransaction.otherUserId == self.myUserId else { return } self.sasTransactionDidStateChange(sasTransaction) From 6ec3cc7b32a524e53646a1b9d033a79eb83d7e33 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 10 Feb 2023 11:16:35 +0000 Subject: [PATCH 466/468] version++ --- CHANGES.md | 7 +++++++ Config/AppVersion.xcconfig | 4 ++-- changelog.d/7530.bugfix | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/7530.bugfix diff --git a/CHANGES.md b/CHANGES.md index af14f69c5..35fea8834 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +## Changes in 1.10.2 (2023-02-10) + +🐛 Bugfixes + +- Fixes #7350 - Fix green dot only to appear for marked action ([#7530](https://github.com/vector-im/element-ios/issues/7530)) + + ## Changes in 1.10.1 (2023-02-07) ✨ Features diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index f1efc9679..20e1da599 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.1 -CURRENT_PROJECT_VERSION = 1.10.1 +MARKETING_VERSION = 1.10.2 +CURRENT_PROJECT_VERSION = 1.10.2 diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix deleted file mode 100644 index 7309798a9..000000000 --- a/changelog.d/7530.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes #7350 - Fix green dot only to appear for marked action \ No newline at end of file From 19d8481be45dc4f589bba6314baaf71af6af07c5 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 10 Feb 2023 12:05:58 +0000 Subject: [PATCH 467/468] Attempt fixing Alpha builds on releases. --- .github/workflows/release-alpha.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index e4f4671de..4f6a1b3e3 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -4,9 +4,7 @@ on: # Triggers the workflow on any pull request pull_request: - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: + types: [ labeled, synchronize, opened, reopened ] env: # Make the git branch for a PR available to our Fastfile @@ -14,11 +12,9 @@ env: jobs: build: - # Don't run for forks as secrets are unavailable. - if: | - github.event.pull_request.head.repo.full_name == github.repository && - (github.event_name == 'push' || - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build'))) + # Only run for PRs that contain the trigger label. The action will fail for forks due to + # missing secrets, but there's no need to handle this as it won't run automatically. + if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build') name: Release runs-on: macos-12 From f852b10a55e2a0bd4962152ea97fa86f1ac20c07 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 10 Feb 2023 13:39:17 +0000 Subject: [PATCH 468/468] finish version++

|! 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 Vo!gB!+WMdW3NotF)skkR{|~Qu5q$sv diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json deleted file mode 100644 index fd0b40307..000000000 --- a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "all_chats_onboarding3.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "all_chats_onboarding3@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "all_chats_onboarding3@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png b/Riot/Assets/Images.xcassets/AllChatsOnboarding/all_chats_onboarding3.imageset/all_chats_onboarding3.png deleted file mode 100644 index 274db9f56999840795857b19935cc3532839ecfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30534 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