From 7b728c14f837808707b61467d819224195a5d67c Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 18 Apr 2023 11:06:25 +0100 Subject: [PATCH 01/52] Deprecate MXLegacyCrypto --- Config/CommonConfiguration.swift | 25 +- Config/Configurable.swift | 3 - Riot/Assets/en.lproj/Vector.strings | 3 - .../MXBugReportRestClient+Riot.swift | 1 - Riot/Experiments/CryptoSDKFeature.swift | 116 --------- Riot/Generated/Strings.swift | 12 - Riot/Modules/Analytics/Analytics.swift | 2 +- .../Analytics/SentryMonitoringClient.swift | 3 - Riot/Modules/Application/LegacyAppDelegate.m | 242 +----------------- .../AuthenticationCoordinator.swift | 9 +- .../LegacyAuthenticationCoordinator.swift | 9 +- .../SessionVerificationListener.swift | 15 +- Riot/Modules/Call/CallViewController.m | 28 +- .../AllChats/AllChatsViewController.swift | 3 +- .../LaunchLoading/LaunchLoadingView.swift | 3 - .../MatrixKit/Models/Account/MXKAccount.m | 10 +- Riot/Modules/Room/RoomViewController.m | 17 +- .../RoomKeyRequestViewController.h | 62 ----- .../RoomKeyRequestViewController.m | 195 -------------- .../Modules/Settings/SettingsViewController.m | 44 +--- .../UserDevices/UsersDevicesViewController.m | 18 +- RiotNSE/NotificationService.swift | 14 +- RiotShareExtension/Shared/ShareManager.m | 5 - .../Service/MatrixSDK/QRLoginService.swift | 11 - .../Experiments/CryptoSDKFeatureTests.swift | 79 ------ .../SendMessage/SendMessageIntentHandler.m | 6 - changelog.d/pr-7508.change | 1 + 27 files changed, 39 insertions(+), 897 deletions(-) delete mode 100644 Riot/Experiments/CryptoSDKFeature.swift delete mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h delete mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m delete mode 100644 RiotTests/Experiments/CryptoSDKFeatureTests.swift create mode 100644 changelog.d/pr-7508.change diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index b00f18831..4a2c05785 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -92,8 +92,7 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - // Configure Crypto SDK feature deciding which crypto module to use - sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared + sdkOptions.cryptoMigrationDelegate = self } private func makeASCIIUserAgent() -> String? { @@ -168,14 +167,16 @@ class CommonConfiguration: NSObject, Configurable { if RiotSettings.shared.allowStunServerFallback, let stunServerFallback = BuildSettings.stunServerFallbackUrlString { callManager.fallbackSTUNServer = stunServerFallback } - } - - - // MARK: - Per loaded matrix session settings - - func setupSettingsWhenLoaded(for matrixSession: MXSession) { - // Do not warn for unknown devices. We have cross-signing now - (matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false - } - + } +} + +extension CommonConfiguration: MXCryptoV2MigrationDelegate { + var needsVerificationUpgrade: Bool { + get { + RiotSettings.shared.showVerificationUpgradeAlert + } + set { + RiotSettings.shared.showVerificationUpgradeAlert = newValue + } + } } diff --git a/Config/Configurable.swift b/Config/Configurable.swift index acfb97605..2f1c46a03 100644 --- a/Config/Configurable.swift +++ b/Config/Configurable.swift @@ -24,7 +24,4 @@ import MatrixSDK // MARK: - Per matrix session settings func setupSettings(for matrixSession: MXSession) - - // MARK: - Per loaded matrix session settings - func setupSettingsWhenLoaded(for matrixSession: MXSession) } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index d88b99b9d..c1099f168 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -807,9 +807,6 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; -"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; -"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index fef876f92..b836f1ab4 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -70,7 +70,6 @@ extension MXBugReportRestClient { // SDKs userInfo["matrix_sdk_version"] = MatrixSDKVersion - userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId if let crypto = mainAccount?.mxSession?.crypto { userInfo["crypto_module_version"] = crypto.version } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift deleted file mode 100644 index e52fc637b..000000000 --- a/Riot/Experiments/CryptoSDKFeature.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// Copyright 2023 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import MatrixSDKCrypto - -/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status -/// of `CryptoSDK`, and which uses feature flags to control rollout availability. -/// -/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. -/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, -/// it is not available to all users because it requires data tracking user consent. Remote therefore -/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually -/// targetting all users, but each target change requires new app release. -/// -/// Additionally users can manually enable this feature from the settings if they are not already in the -/// feature group. -@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { - @objc static let shared = CryptoSDKFeature() - - var isEnabled: Bool { - RiotSettings.shared.enableCryptoSDK - } - - var needsVerificationUpgrade: Bool { - get { - return RiotSettings.shared.showVerificationUpgradeAlert - } - set { - RiotSettings.shared.showVerificationUpgradeAlert = newValue - } - } - - private static let FeatureName = "ios-crypto-sdk" - private static let FeatureNameV2 = "ios-crypto-sdk-v2" - - private let remoteFeature: RemoteFeaturesClientProtocol - private let localFeature: PhasedRolloutFeature - - init( - remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared, - localTargetPercentage: Double = 1 - ) { - self.remoteFeature = remoteFeature - self.localFeature = PhasedRolloutFeature( - name: Self.FeatureName, - targetPercentage: localTargetPercentage - ) - } - - func enable() { - RiotSettings.shared.enableCryptoSDK = true - Analytics.shared.trackCryptoSDKEnabled() - - MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") - } - - func enableIfAvailable(forUserId userId: String!) { - guard !isEnabled else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") - return - } - - guard let userId else { - MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") - return - } - - guard isFeatureEnabled(userId: userId) else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") - return - } - - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") - enable() - } - - @objc func canManuallyEnable(forUserId userId: String!) -> Bool { - guard let userId else { - MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") - return false - } - - // User can manually enable only if not already within the automatic feature group - return !isFeatureEnabled(userId: userId) - } - - @objc func reset() { - RiotSettings.shared.enableCryptoSDK = false - MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") - } - - private func isFeatureEnabled(userId: String) -> Bool { - // This feature includes app version with a bug, and thus will not be rolled out to 100% users - remoteFeature.isFeatureEnabled(Self.FeatureName) - - // Second version of the remote feature with a bugfix and released eventually to 100% users - || remoteFeature.isFeatureEnabled(Self.FeatureNameV2) - - // Local feature - || localFeature.isEnabled(userId: userId) - } -} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index c02a605c6..0cdd03d2d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7647,18 +7647,10 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. - public static var settingsLabsConfirmCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") - } /// Create conference calls with jitsi public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// Rust end-to-end encryption (log out to disable) - public static var settingsLabsDisableCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") - } /// End-to-End Encryption public static var settingsLabsE2eEncryption: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") @@ -7671,10 +7663,6 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// Rust end-to-end encryption - public static var settingsLabsEnableCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") - } /// Live location sharing - share current location (active development, and temporarily, locations persist in room history) public static var settingsLabsEnableLiveLocationSharing: String { return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index c48b447e5..1a30841b9 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -274,7 +274,7 @@ extension Analytics { func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { let event = AnalyticsEvent.Error( context: context, - cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, + cryptoModule: .Rust, domain: .E2EE, name: reason.errorName ) diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift index 78450551b..54933a7ab 100644 --- a/Riot/Modules/Analytics/SentryMonitoringClient.swift +++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift @@ -46,9 +46,6 @@ struct SentryMonitoringClient { if let message = event.message?.formatted { event.fingerprint = [message] } - event.tags = [ - "crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId - ] MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") return event } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 208cb46eb..8678f5ab8 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -33,7 +33,6 @@ #import "ContactDetailsViewController.h" #import "BugReportViewController.h" -#import "RoomKeyRequestViewController.h" #import "DecryptionFailureTracker.h" #import "Tools.h" @@ -114,11 +113,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni id roomKeyRequestObserver; id roomKeyRequestCancellationObserver; - /** - If any the currently displayed sharing key dialog - */ - RoomKeyRequestViewController *roomKeyRequestViewController; - /** Incoming key verification requests observers */ @@ -1823,8 +1817,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // start the call service [self.callPresenter start]; - [self.configuration setupSettingsWhenLoadedFor:mxSession]; - // Register to user new device sign in notification [self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession]; @@ -1833,8 +1825,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register to new key verification request [self registerNewRequestNotificationForSession:mxSession]; - [self checkLocalPrivateKeysInSession:mxSession]; - [self.pushNotificationService checkPushKitPushersInSession:mxSession]; } else if (mxSession.state == MXSessionStateRunning) @@ -2031,9 +2021,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // If any, disable the no VoIP support workaround [self disableNoVoIPOnMatrixSession:mxSession]; - // Disable listening of incoming key share requests - [self disableRoomKeyRequestObserver:mxSession]; - // Disable listening of incoming key verification requests [self disableIncomingKeyVerificationObserver:mxSession]; @@ -2183,9 +2170,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; - // Reset Crypto SDK configuration (labs flag for which crypto module to use) - [CryptoSDKFeature.shared reset]; - // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; @@ -2296,11 +2280,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni case MXSessionStateSyncInProgress: // Stay in launching during the first server sync if the store is empty. isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); - - if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists && [mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - [(MXLegacyCrypto *)mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; - } break; case MXSessionStateRunning: self.clearingCache = NO; @@ -2360,7 +2339,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // This is the time to check existing requests MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); - [self checkPendingRoomKeyRequests]; [self checkPendingIncomingKeyVerificationsInSession:mainSession]; // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state @@ -2369,9 +2347,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); - // Enable listening of incoming key share requests - [self enableRoomKeyRequestObserver:mainSession]; - // Enable listening of incoming key verification requests [self enableIncomingKeyVerificationObserver:mainSession]; } @@ -2397,16 +2372,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] showLaunchAnimation"); - LaunchLoadingView *launchLoadingView; - if (MXSDKOptions.sharedInstance.enableStartupProgress) - { - MXSession *mainSession = self.mxSessions.firstObject; - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; - } - else - { - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil]; - } + MXSession *mainSession = self.mxSessions.firstObject; + LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; launchLoadingView.frame = window.bounds; [launchLoadingView updateWithTheme:ThemeService.shared.theme]; @@ -2520,38 +2487,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #endif } -- (void)checkLocalPrivateKeysInSession:(MXSession*)mxSession -{ - if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - return; - } - MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; - - MXRecoveryService *recoveryService = mxSession.crypto.recoveryService; - NSUInteger keysCount = 0; - if ([recoveryService hasSecretWithSecretId:MXSecretId.keyBackup]) - { - keysCount++; - } - if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningUserSigning]) - { - keysCount++; - } - if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningSelfSigning]) - { - keysCount++; - } - - if ((keysCount > 0 && keysCount < 3) - || (mxSession.crypto.crossSigning.canTrustCrossSigning && !mxSession.crypto.crossSigning.canCrossSign)) - { - // We should have 3 of them. If not, request them again as mitigation - MXLogDebug(@"[AppDelegate] checkLocalPrivateKeysInSession: request keys because keysCount = %@", @(keysCount)); - [crypto requestAllPrivateKeys]; - } -} - - (void)authenticationDidComplete { [self handleAppState]; @@ -3461,173 +3396,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } - -#pragma mark - Incoming room key requests handling - -- (void)enableRoomKeyRequestObserver:(MXSession*)mxSession -{ - roomKeyRequestObserver = - [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestNotification - object:mxSession.crypto - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification *notif) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - }]; - - roomKeyRequestCancellationObserver = - [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestCancellationNotification - object:mxSession.crypto - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification *notif) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - }]; -} - -- (void)disableRoomKeyRequestObserver:(MXSession*)mxSession -{ - if (roomKeyRequestObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestObserver]; - roomKeyRequestObserver = nil; - } - - if (roomKeyRequestCancellationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestCancellationObserver]; - roomKeyRequestCancellationObserver = nil; - } -} - -// Check if a key share dialog must be displayed for the given session -- (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession -{ - if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession called while the app is not active. Ignore it."); - return; - } - - if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Only legacy crypto allows manually accepting/rejecting key requests"); - return; - } - MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; - - MXWeakify(self); - [crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { - - MXStrongifyAndReturnIfNil(self); - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@", - crypto.crossSigning.state, - @(pendingKeyRequests.count), - self->roomKeyRequestViewController ? @"YES" : @"NO"); - - if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) - { - if (self->roomKeyRequestViewController) - { - // Check if the current RoomKeyRequestViewController is still valid - MXSession *currentMXSession = self->roomKeyRequestViewController.mxSession; - NSString *currentUser = self->roomKeyRequestViewController.device.userId; - NSString *currentDevice = self->roomKeyRequestViewController.device.deviceId; - - NSArray *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser]; - - if (currentMXSession == mxSession && currentPendingRequest.count == 0) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog"); - - // The key request has been probably cancelled, remove the popup - [self->roomKeyRequestViewController hide]; - self->roomKeyRequestViewController = nil; - } - } - } - - if (!self->roomKeyRequestViewController && pendingKeyRequests.count) - { - // Pick the first coming user/device pair - NSString *userId = pendingKeyRequests.userIds.firstObject; - NSString *deviceId = [pendingKeyRequests deviceIdsForUser:userId].firstObject; - - // Give the client a chance to refresh the device list - MXWeakify(self); - [crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { - - MXStrongifyAndReturnIfNil(self); - MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId]; - if (deviceInfo) - { - if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) - { - BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); - - void (^openDialog)(void) = ^void() - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); - - self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{ - - self->roomKeyRequestViewController = nil; - - // Check next pending key request, if any - [self checkPendingRoomKeyRequests]; - }]; - - [self->roomKeyRequestViewController show]; - }; - - // If the device was new before, it's not any more. - if (wasNewDevice) - { - [crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; - } - else - { - openDialog(); - } - } - else if (deviceInfo.trustLevel.isVerified) - { - [crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - else - { - [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - } - else - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId); - [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - } failure:^(NSError *error) { - // Retry later - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Failed to download device keys. Retry"); - [self checkPendingRoomKeyRequests]; - }]; - } - }]; -} - -// Check all opened MXSessions for key share dialog -- (void)checkPendingRoomKeyRequests -{ - for (MXSession *mxSession in mxSessionArray) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - } -} - #pragma mark - Incoming key verification handling - (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession @@ -3785,12 +3553,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { - id crypto = coordinatorBridgePresenter.session.crypto; - if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled)) - { - MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); - [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; - } [self dismissKeyVerificationCoordinatorBridgePresenter]; } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 9f2e7083b..a245147cd 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -613,8 +613,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc /// Replace the contents of the navigation router with a loading animation. private func showLoadingAnimation() { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation @@ -759,12 +758,6 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate { // MARK: - KeyVerificationCoordinatorDelegate extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, - !backup.hasPrivateKeyInCryptoStore || !backup.enabled { - MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) - } - navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index d6270edae..4aea0b8b9 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -106,8 +106,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator // MARK: - Private private func showLoadingAnimation() { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation @@ -220,12 +219,6 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate // MARK: - KeyVerificationCoordinatorDelegate extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, - !backup.hasPrivateKeyInCryptoStore || !backup.enabled { - MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) - } - navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift index 214c76695..ffefd839a 100644 --- a/Riot/Modules/Authentication/SessionVerificationListener.swift +++ b/Riot/Modules/Authentication/SessionVerificationListener.swift @@ -68,14 +68,7 @@ class SessionVerificationListener { return } - if session.state == .storeDataReady { - if let crypto = session.crypto as? MXLegacyCrypto { - // Do not make key share requests while the "Complete security" is not complete. - // If the device is self-verified, the SDK will restore the existing key backup. - // Then, it will re-enable outgoing key share requests - crypto.setOutgoingKeyRequestsEnabled(false, onComplete: nil) - } - } else if session.state == .running { + if session.state == .running { unregisterSessionStateChangeNotification() if let crypto = session.crypto { @@ -101,7 +94,6 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } else { @@ -111,12 +103,10 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } } else { - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } case .crossSigningExists: @@ -124,13 +114,10 @@ class SessionVerificationListener { self.completion?(.needsVerification) default: MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") - - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } failure: { [weak self] error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self?.completion?(.authenticationIsComplete) } } else { diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 3e8227e7c..680d330fa 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -370,28 +370,16 @@ CallAudioRouteMenuViewDelegate> { typeof(self) self = weakSelf; self->currentAlert = nil; - - // Acknowledge the existence of all devices - [self startActivityIndicator]; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + + // Retry the call + if (call.isIncoming) { - MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices"); - return; + [call answer]; + } + else + { + [call callWithVideo:call.isVideoCall]; } - [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{ - - [self stopActivityIndicator]; - - // Retry the call - if (call.isIncoming) - { - [call answer]; - } - else - { - [call callWithVideo:call.isVideoCall]; - } - }]; } }]]; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index ea96873ba..6108d01c2 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -988,8 +988,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { let title: String let message: String - if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature, - feature.isEnabled && feature.needsVerificationUpgrade { + if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true { title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage } else { diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift index 8398c659d..c4cdee422 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift @@ -69,9 +69,6 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { extension LaunchLoadingView: MXSessionStartupProgressDelegate { func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) { - guard MXSDKOptions.sharedInstance().enableStartupProgress else { - return - } update(with: state) } diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 548442ab7..1d0382375 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -946,15 +946,7 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; [MXKRoomDataSourceManager removeSharedManagerForMatrixSession:mxSession]; if (clearStore) - { - // Force a reload of device keys at the next session start, unless we are just about to migrate - // all data and device keys into CryptoSDK. - // This will fix potential UISIs other peoples receive for our messages. - if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK) - { - [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; - } - + { // Clean other stores [mxSession.scanManager deleteAllAntivirusScans]; [mxSession.aggregations resetData]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 398e12e8f..70b8d974c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6356,21 +6356,10 @@ static CGSize kThreadListBarButtonItemImageSize; self->currentAlert = nil; // Acknowledge the existence of all devices - [self startActivityIndicator]; + self->unknownDevices = nil; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices"); - return; - } - [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{ - - self->unknownDevices = nil; - [self stopActivityIndicator]; - - // And resend pending messages - [self resendAllUnsentMessages]; - }]; + // And resend pending messages + [self resendAllUnsentMessages]; } }]]; diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h deleted file mode 100644 index e9db3a583..000000000 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2017 Vector Creations 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 - -#import - -/** - The `RoomKeyRequestViewController` display a modal dialog at the top of the - application asking the user if he wants to share room keys with a user's device. - For the moment, the user is himself. - */ -@interface RoomKeyRequestViewController : NSObject - -/** - The UIAlertController instance which handles the dialog. - */ -@property (nonatomic, readonly) UIAlertController *alertController; - -@property (nonatomic, readonly) MXSession *mxSession; -@property (nonatomic, readonly) MXDeviceInfo *device; - -/** - Initialise an `RoomKeyRequestViewController` instance. - - @param deviceInfo the device to share keys to. - @param wasNewDevice flag indicating whether this is the first time we meet the device. - @param session the related matrix session. - @param crypto the related (legacy) crypto module - @param onComplete a block called when the the dialog is closed. - @return the newly created instance. - */ -- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo - wasNewDevice:(BOOL)wasNewDevice - andMatrixSession:(MXSession*)session - crypto:(MXLegacyCrypto *)crypto - onComplete:(void (^)(void))onComplete; - -/** - Show the dialog in a modal way. - */ -- (void)show; - -/** - Hide the dialog. - */ -- (void)hide; - -@end diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m deleted file mode 100644 index 6f638bd78..000000000 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m +++ /dev/null @@ -1,195 +0,0 @@ -/* - Copyright 2017 Vector Creations 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 "RoomKeyRequestViewController.h" - -#import "GeneratedInterface-Swift.h" - -@interface RoomKeyRequestViewController () -{ - void (^onComplete)(void); - - KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter; - - BOOL wasNewDevice; -} - -@property (nonatomic, strong) MXLegacyCrypto *crypto; - -@end - -@implementation RoomKeyRequestViewController - -- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo - wasNewDevice:(BOOL)theWasNewDevice - andMatrixSession:(MXSession *)session - crypto:(MXLegacyCrypto *)crypto - onComplete:(void (^)(void))onCompleteBlock -{ - self = [super init]; - if (self) - { - _mxSession = session; - _crypto = crypto; - _device = deviceInfo; - wasNewDevice = theWasNewDevice; - onComplete = onCompleteBlock; - } - return self; -} - -- (void)show -{ - // Show it modally on the root view controller - UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; - if (rootViewController) - { - NSString *title = [VectorL10n e2eRoomKeyRequestTitle]; - NSString *message; - if (wasNewDevice) - { - message = [VectorL10n e2eRoomKeyRequestMessageNewDevice:_device.displayName]; - } - else - { - message = [VectorL10n e2eRoomKeyRequestMessage:_device.displayName]; - } - - _alertController = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - __weak typeof(self) weakSelf = self; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestStartVerification] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - [self showVerificationView]; - } - }]]; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestShareWithoutVerifying] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - - // Accept the received requests from this device - [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - }]]; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestIgnoreRequest] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - - // Ignore all pending requests from this device - [self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - }]]; - - [rootViewController presentViewController:_alertController animated:YES completion:nil]; - } -} - -- (void)hide -{ - if (_alertController) - { - [_alertController dismissViewControllerAnimated:YES completion:nil]; - _alertController = nil; - } -} - - -- (void)showVerificationView -{ - // Show it modally on the root view controller - UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; - if (rootViewController) - { - keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:_mxSession]; - keyVerificationCoordinatorBridgePresenter.delegate = self; - - [keyVerificationCoordinatorBridgePresenter presentFrom:rootViewController otherUserId:_device.userId otherDeviceId:_device.deviceId animated:YES]; - } -} - -#pragma mark - DeviceVerificationCoordinatorBridgePresenterDelegate - -- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId -{ - [self dismissKeyVerificationCoordinatorBridgePresenter]; -} - -- (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [self dismissKeyVerificationCoordinatorBridgePresenter]; -} - -- (void)dismissKeyVerificationCoordinatorBridgePresenter -{ - [keyVerificationCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - keyVerificationCoordinatorBridgePresenter = nil; - - // Check device new status - [self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { - - MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId]; - if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified) - { - // Accept the received requests from this device - // As the device is now verified, all other key requests will be automatically accepted. - [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - else - { - // Come back to self.alertController - ie, reopen it - [self show]; - } - } failure:^(NSError *error) { - - // Should not happen (the device is in the crypto db) - [self show]; - }]; -} - -@end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 244e28be0..055841f3f 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -176,8 +176,7 @@ 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_CRYPTO_SDK + LABS_ENABLE_VOICE_BROADCAST }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -588,11 +587,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId]) - { - [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; - } - [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; @@ -2587,18 +2581,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside]; - cell = labelAndSwitchCell; - } - else if (row == LABS_ENABLE_CRYPTO_SDK) - { - MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; - labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; - labelAndSwitchCell.mxkSwitch.on = isEnabled; - [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; - cell = labelAndSwitchCell; } } @@ -3372,30 +3354,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -- (void)enableCryptoSDKFeature:(UISwitch *)sender -{ - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk - message:VectorL10n.settingsLabsConfirmCryptoSdk - preferredStyle:UIAlertControllerStyleAlert]; - - MXWeakify(self); - [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - self->currentAlert = nil; - - [sender setOn:NO animated:YES]; - }]]; - - [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - [CryptoSDKFeature.shared enable]; - [[AppDelegate theDelegate] reloadMatrixSessions:YES]; - }]]; - - [self presentViewController:confirmationAlert animated:YES completion:nil]; - currentAlert = confirmationAlert; -} - - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 3b5b8c9a8..fcd7bd567 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -273,22 +273,12 @@ - (IBAction)onDone:(id)sender { // Acknowledge the existence of all devices before leaving this screen - [self startActivityIndicator]; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + [self dismissViewControllerAnimated:YES completion:nil]; + + if (self->onCompleteBlock) { - MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices"); - return; + self->onCompleteBlock(YES); } - [(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{ - - [self stopActivityIndicator]; - [self dismissViewControllerAnimated:YES completion:nil]; - - if (self->onCompleteBlock) - { - self->onCompleteBlock(YES); - } - }]; } - (IBAction)onCancel:(id)sender diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 0c7257dba..5880165e8 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,7 +41,6 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? - private var isCryptoSDKEnabled = false /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -196,13 +195,12 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() NotificationService.backgroundSyncService = MXBackgroundSyncService( withCredentials: userAccount.mxCredentials, - isCryptoSDKEnabled: isCryptoSDKEnabled, persistTokenDataHandler: { persistTokenDataHandler in MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in @@ -219,16 +217,6 @@ class NotificationService: UNNotificationServiceExtension { } } - /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require - /// rebuilding `MXBackgroundSyncService` - private func hasChangedCryptoSDK() -> Bool { - guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else { - return false - } - isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK - return true - } - /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index 22d0063be..0e1c74cf7 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -102,11 +102,6 @@ static MXSession *fakeSession; [session setStore:self.fileStore success:^{ MXStrongifyAndReturnIfNil(session); - if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now - } - self.selectedRooms = [NSMutableArray array]; for (NSString *roomIdentifier in roomIdentifiers) { MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 9c9100087..9cf127bb7 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -267,17 +267,6 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) -// MXLog.debug("[QRLoginService] Session created without E2EE support. Inform the interlocutor of finishing") -// guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .success)), -// case .success = await rendezvousService.send(data: requestData) else { -// await teardownRendezvous(state: .failed(error: .rendezvousFailed)) -// return -// } -// -// MXLog.debug("[QRLoginService] Login flow finished, returning session") -// state = .completed(session: session, securityCompleted: false) -// return - let cryptoResult = await withCheckedContinuation { continuation in session.enableCrypto(true) { response in continuation.resume(returning: response) diff --git a/RiotTests/Experiments/CryptoSDKFeatureTests.swift b/RiotTests/Experiments/CryptoSDKFeatureTests.swift deleted file mode 100644 index a512b71c6..000000000 --- a/RiotTests/Experiments/CryptoSDKFeatureTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright 2023 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import XCTest -@testable import Element - -class CryptoSDKFeatureTests: XCTestCase { - class RemoteFeatureClient: RemoteFeaturesClientProtocol { - var isEnabled = false - func isFeatureEnabled(_ feature: String) -> Bool { - isEnabled - } - } - - var remote: RemoteFeatureClient! - var feature: CryptoSDKFeature! - - override func setUp() { - RiotSettings.shared.enableCryptoSDK = false - remote = RemoteFeatureClient() - feature = CryptoSDKFeature(remoteFeature: remote, localTargetPercentage: 0) - } - - override func tearDown() { - RiotSettings.shared.enableCryptoSDK = false - } - - func test_disabledByDefault() { - XCTAssertFalse(feature.isEnabled) - } - - func test_enable() { - feature.enable() - XCTAssertTrue(feature.isEnabled) - } - - func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() { - feature.enable() - remote.isEnabled = false - - feature.enableIfAvailable(forUserId: "alice") - - XCTAssertTrue(feature.isEnabled) - } - - func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() { - remote.isEnabled = false - feature.enableIfAvailable(forUserId: "alice") - XCTAssertFalse(feature.isEnabled) - } - - func test_canManuallyEnable() { - remote.isEnabled = false - XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice")) - - remote.isEnabled = true - XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice")) - } - - func test_reset() { - feature.enable() - feature.reset() - XCTAssertFalse(RiotSettings.shared.enableCryptoSDK) - } -} diff --git a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m index 34ebb66e9..5bc037790 100644 --- a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m +++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m @@ -117,12 +117,6 @@ self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session]; - // Do not warn for unknown devices. We have cross-signing now - if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; - } - MXWeakify(self); [self.selectedRoom sendTextMessage:intent.content threadId:nil diff --git a/changelog.d/pr-7508.change b/changelog.d/pr-7508.change new file mode 100644 index 000000000..dbe206b34 --- /dev/null +++ b/changelog.d/pr-7508.change @@ -0,0 +1 @@ +Crypto: Deprecate MXLegacyCrypto From a2984b0eccf925452c84a74bf353335d82e44b56 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 20:11:40 +0100 Subject: [PATCH 02/52] 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 5f74246b2..3e986e4b1 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.11 -CURRENT_PROJECT_VERSION = 1.10.11 +MARKETING_VERSION = 1.10.12 +CURRENT_PROJECT_VERSION = 1.10.12 From e3a32e1c5fefbfc52a7cfc5a9101134e1e93b12f Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 13 Mar 2023 16:18:11 +0000 Subject: [PATCH 03/52] Refactor encryption trust level --- Riot/Categories/MXRoom+Riot.m | 30 +-- Riot/Categories/MXRoomSummary+Riot.h | 12 +- Riot/Categories/MXRoomSummary+Riot.m | 29 +-- .../EncryptionInfo/EncryptionInfoView.h | 0 .../EncryptionInfo/EncryptionInfoView.m | 0 .../EncryptionInfo/EncryptionInfoView.xib | 0 .../Encryption/EncryptionTrustLevel.swift | 68 +++++++ ...EncryptionTrustLevelBadgeImageHelper.swift | 0 .../Encryption/RoomEncryptionTrustLevel.h | 25 +++ .../UserEncryptionTrustLevel.h | 0 Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + .../RiotShareExtension-Bridging-Header.h | 2 + RiotShareExtension/target.yml | 1 + .../EncryptionTrustLevelTests.swift | 177 ++++++++++++++++++ 14 files changed, 286 insertions(+), 59 deletions(-) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.h (100%) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.m (100%) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.xib (100%) create mode 100644 Riot/Modules/Encryption/EncryptionTrustLevel.swift rename Riot/{Utils => Modules/Encryption}/EncryptionTrustLevelBadgeImageHelper.swift (100%) create mode 100644 Riot/Modules/Encryption/RoomEncryptionTrustLevel.h rename Riot/Modules/{Room/Members/Detail => Encryption}/UserEncryptionTrustLevel.h (100%) create mode 100644 RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index 04538ba80..b81da1759 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -20,7 +20,7 @@ #import "AvatarGenerator.h" #import "MatrixKit.h" - +#import "GeneratedInterface-Swift.h" #import @implementation MXRoom (Riot) @@ -331,30 +331,10 @@ { [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) { - UserEncryptionTrustLevel userEncryptionTrustLevel; - double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted; - - if (trustedDevicesPercentage >= 1.0) - { - userEncryptionTrustLevel = UserEncryptionTrustLevelTrusted; - } - else if (trustedDevicesPercentage == 0.0) - { - // Verify if the user has the user has cross-signing enabled - if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId]) - { - userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified; - } - else - { - userEncryptionTrustLevel = UserEncryptionTrustLevelNoCrossSigning; - } - } - else - { - userEncryptionTrustLevel = UserEncryptionTrustLevelWarning; - } - + MXCrossSigningInfo *crossSigningInfo = [self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId]; + EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init]; + UserEncryptionTrustLevel userEncryptionTrustLevel = [encryption userTrustLevelWithCrossSigning:crossSigningInfo + trustedDevicesProgress:usersTrustLevelSummary.trustedDevicesProgress]; onComplete(userEncryptionTrustLevel); } failure:^(NSError *error) { diff --git a/Riot/Categories/MXRoomSummary+Riot.h b/Riot/Categories/MXRoomSummary+Riot.h index d25cdee5f..324a7f369 100644 --- a/Riot/Categories/MXRoomSummary+Riot.h +++ b/Riot/Categories/MXRoomSummary+Riot.h @@ -15,17 +15,7 @@ */ #import "MatrixKit.h" - -/** - RoomEncryptionTrustLevel represents the trust level in an encrypted room. - */ -typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) { - RoomEncryptionTrustLevelTrusted, - RoomEncryptionTrustLevelWarning, - RoomEncryptionTrustLevelNormal, - RoomEncryptionTrustLevelUnknown -}; - +#import "RoomEncryptionTrustLevel.h" /** Define a `MXRoomSummary` category at Riot level. diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index c6a55a230..b2c1eeb40 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -33,32 +33,15 @@ - (RoomEncryptionTrustLevel)roomEncryptionTrustLevel { - RoomEncryptionTrustLevel roomEncryptionTrustLevel = RoomEncryptionTrustLevelUnknown; - if (self.trust) + MXUsersTrustLevelSummary *trust = self.trust; + if (!trust) { - double trustedUsersPercentage = self.trust.trustedUsersProgress.fractionCompleted; - double trustedDevicesPercentage = self.trust.trustedDevicesProgress.fractionCompleted; - - if (trustedUsersPercentage >= 1.0) - { - if (trustedDevicesPercentage >= 1.0) - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelTrusted; - } - else - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelWarning; - } - } - else - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelNormal; - } - - roomEncryptionTrustLevel = roomEncryptionTrustLevel; + MXLogError(@"[MXRoomSummary] roomEncryptionTrustLevel: trust is missing"); + return RoomEncryptionTrustLevelUnknown; } - return roomEncryptionTrustLevel; + EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init]; + return [encryption roomTrustLevelWithSummary:trust]; } - (BOOL)isJoined diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.h b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.h similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.h rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.h diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.m b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.m similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.m rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.m diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.xib b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.xib similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.xib rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.xib diff --git a/Riot/Modules/Encryption/EncryptionTrustLevel.swift b/Riot/Modules/Encryption/EncryptionTrustLevel.swift new file mode 100644 index 000000000..275d74ffc --- /dev/null +++ b/Riot/Modules/Encryption/EncryptionTrustLevel.swift @@ -0,0 +1,68 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Object responsible for calculating user and room trust level +/// +/// For legacy reasons, the trust of multiple items is represented as `Progress` object, +/// where `completedUnitCount` represents the number of trusted users / devices. +@objc class EncryptionTrustLevel: NSObject { + struct TrustSummary { + let totalCount: Int64 + let trustedCount: Int64 + let areAllTrusted: Bool + + init(progress: Progress) { + totalCount = max(progress.totalUnitCount, progress.completedUnitCount) + trustedCount = progress.completedUnitCount + areAllTrusted = trustedCount == totalCount + } + } + + + /// Calculate trust level for a single user given their cross-signing info + @objc func userTrustLevel( + crossSigning: MXCrossSigningInfo?, + trustedDevicesProgress: Progress + ) -> UserEncryptionTrustLevel { + let devices = TrustSummary(progress: trustedDevicesProgress) + + // If we could cross-sign but we haven't, the user is simply not verified + if let crossSigning, !crossSigning.trustLevel.isVerified { + return .notVerified + + // If we cannot cross-sign the user (legacy behaviour) and have not signed + // any devices manually, the user is not verified + } else if crossSigning == nil && devices.trustedCount == 0 { + return .notVerified + } + + // In all other cases we check devices for trust level + return devices.areAllTrusted ? .trusted : .warning + } + + /// Calculate trust level for a room given trust level of users and their devices + @objc func roomTrustLevel(summary: MXUsersTrustLevelSummary) -> RoomEncryptionTrustLevel { + let users = TrustSummary(progress: summary.trustedUsersProgress) + let devices = TrustSummary(progress: summary.trustedDevicesProgress) + + guard users.totalCount > 0 && users.areAllTrusted else { + return .normal + } + return devices.areAllTrusted ? .trusted : .warning + } +} diff --git a/Riot/Utils/EncryptionTrustLevelBadgeImageHelper.swift b/Riot/Modules/Encryption/EncryptionTrustLevelBadgeImageHelper.swift similarity index 100% rename from Riot/Utils/EncryptionTrustLevelBadgeImageHelper.swift rename to Riot/Modules/Encryption/EncryptionTrustLevelBadgeImageHelper.swift diff --git a/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h b/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h new file mode 100644 index 000000000..a942f5360 --- /dev/null +++ b/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h @@ -0,0 +1,25 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + RoomEncryptionTrustLevel represents the trust level in an encrypted room. + */ +typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) { + RoomEncryptionTrustLevelTrusted, + RoomEncryptionTrustLevelWarning, + RoomEncryptionTrustLevelNormal, + RoomEncryptionTrustLevelUnknown +}; diff --git a/Riot/Modules/Room/Members/Detail/UserEncryptionTrustLevel.h b/Riot/Modules/Encryption/UserEncryptionTrustLevel.h similarity index 100% rename from Riot/Modules/Room/Members/Detail/UserEncryptionTrustLevel.h rename to Riot/Modules/Encryption/UserEncryptionTrustLevel.h diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index e86152e1c..296545a4e 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -18,6 +18,7 @@ #import "RoomBubbleCellData.h" #import "MXKRoomBubbleTableViewCell+Riot.h" #import "UserEncryptionTrustLevel.h" +#import "RoomEncryptionTrustLevel.h" #import "RoomReactionsViewSizer.h" #import "RoomEncryptedDataBubbleCell.h" #import "LegacyAppDelegate.h" diff --git a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h index 9a7cf3af1..618849c4d 100644 --- a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h +++ b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h @@ -6,6 +6,8 @@ #import "AvatarGenerator.h" #import "BuildInfo.h" #import "ShareItemSender.h" +#import "UserEncryptionTrustLevel.h" +#import "RoomEncryptionTrustLevel.h" // MatrixKit imports #import "MatrixKit-Bridging-Header.h" diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index eaf51ce3c..b289f234b 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -87,3 +87,4 @@ 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/Modules/Encryption/EncryptionTrustLevel.swift diff --git a/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift b/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift new file mode 100644 index 000000000..f038ba5e2 --- /dev/null +++ b/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift @@ -0,0 +1,177 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import Element +@testable import MatrixSDK + +class EncryptionTrustLevelTests: XCTestCase { + + var encryption: EncryptionTrustLevel! + override func setUp() { + encryption = EncryptionTrustLevel() + } + + // MARK: - Helpers + + func makeCrossSigning(isVerified: Bool) -> MXCrossSigningInfo { + return .init( + userIdentity: .init( + identity: .other( + userId: "Bob", + masterKey: "MSK", + selfSigningKey: "SSK" + ), + isVerified: isVerified + ) + ) + } + + func makeProgress(trusted: Int, total: Int) -> Progress { + let progress = Progress(totalUnitCount: Int64(total)) + progress.completedUnitCount = Int64(trusted) + return progress + } + + // MARK: - Users + + func test_userTrustLevel_whenCrossSigningDisabled() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .notVerified), + (makeProgress(trusted: 0, total: 2), .notVerified), + (makeProgress(trusted: 1, total: 2), .warning), + (makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 5), .trusted) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: nil, + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + func test_userTrustLevel_whenCrossSigningNotVerified() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .notVerified), + (makeProgress(trusted: 0, total: 2), .notVerified), + (makeProgress(trusted: 1, total: 2), .notVerified), + (makeProgress(trusted: 3, total: 4), .notVerified), + (makeProgress(trusted: 5, total: 5), .notVerified), + (makeProgress(trusted: 10, total: 5), .notVerified) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: makeCrossSigning(isVerified: false), + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + func test_userTrustLevel_whenCrossSigningVerified() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .trusted), + (makeProgress(trusted: 0, total: 2), .warning), + (makeProgress(trusted: 1, total: 2), .warning), + (makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 5), .trusted) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: makeCrossSigning(isVerified: true), + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + // MARK: - Rooms + + func test_roomTrustLevel() { + let usersDevicesToTrustLevel: [(Progress, Progress, RoomEncryptionTrustLevel)] = [ + // No users verified + (makeProgress(trusted: 0, total: 0), makeProgress(trusted: 0, total: 0), .normal), + + // Only some users verified + (makeProgress(trusted: 0, total: 1), makeProgress(trusted: 0, total: 1), .normal), + (makeProgress(trusted: 3, total: 4), makeProgress(trusted: 5, total: 5), .normal), + (makeProgress(trusted: 3, total: 4), makeProgress(trusted: 5, total: 5), .normal), + + // All users verified + (makeProgress(trusted: 2, total: 2), makeProgress(trusted: 0, total: 0), .trusted), + (makeProgress(trusted: 3, total: 3), makeProgress(trusted: 0, total: 1), .warning), + (makeProgress(trusted: 3, total: 3), makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 4, total: 4), makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 4), makeProgress(trusted: 10, total: 5), .trusted), + ] + + for (users, devices, expected) in usersDevicesToTrustLevel { + let trustLevel = encryption.roomTrustLevel( + summary: MXUsersTrustLevelSummary( + trustedUsersProgress: users, + andTrustedDevicesProgress: devices + ) + ) + XCTAssertEqual(trustLevel, expected, "\(users.completedUnitCount)/\(users.totalUnitCount) trusted users(s), \(devices.completedUnitCount)/\(devices.totalUnitCount) trusted device(s)") + } + } +} + +extension UserEncryptionTrustLevel: CustomStringConvertible { + public var description: String { + switch self { + case .trusted: + return "trusted" + case .warning: + return "warning" + case .notVerified: + return "notVerified" + case .noCrossSigning: + return "noCrossSigning" + case .none: + return "none" + case .unknown: + return "unknown" + @unknown default: + return "unknown" + } + } +} + +extension RoomEncryptionTrustLevel: CustomStringConvertible { + public var description: String { + switch self { + case .trusted: + return "trusted" + case .warning: + return "warning" + case .normal: + return "normal" + case .unknown: + return "unknown" + @unknown default: + return "unknown" + } + } +} From 6179453c577c30d69dd476b0bf44d1297648a530 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 18 Apr 2023 11:37:34 +0200 Subject: [PATCH 04/52] Fix: Calculation of the frame for a bubble component --- .../MXKRoomBubbleTableViewCell+Riot.m | 38 ++++++++++++------- .../Models/Room/MXKRoomBubbleCellData.h | 9 +++++ .../Models/Room/MXKRoomBubbleCellData.m | 17 +++++++-- ...eOutgoingWithoutSenderInfoBubbleCell.swift | 9 +++++ changelog.d/pr-7512.bugfix | 1 + 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 changelog.d/pr-7512.bugfix diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index a7bfd69f1..907dd5ff2 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -600,36 +600,47 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = } else if (roomBubbleTableViewCell.messageTextView) { + // Force the textView used underneath to layout its frame properly + [roomBubbleTableViewCell setNeedsLayout]; + [roomBubbleTableViewCell layoutIfNeeded]; + + // Compute the height CGFloat textMessageHeight = 0; - if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]]) { RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData; if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage) { - textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage]; + // Get the width of messageTextView to compute the needed height + CGFloat maxTextWidth = CGRectGetWidth(roomBubbleTableViewCell.messageTextView.bounds); + + // Compute text message height + textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage withMaxWidth:maxTextWidth]; } } - - selectedComponentPositionY = selectedComponent.position.y; - + + // Get the messageText frame in the cell content view (as the messageTextView may be inside a stackView and not directly a child of the tableViewCell) + UITextView *messageTextView = roomBubbleTableViewCell.messageTextView; + CGRect messageTextViewFrame = [messageTextView convertRect:messageTextView.bounds toView:roomBubbleTableViewCell.contentView]; + if (textMessageHeight > 0) { selectedComponentHeight = textMessageHeight; } else { - selectedComponentHeight = roomBubbleTableViewCell.frame.size.height - selectedComponentPositionY; + // if we don't have a height, use the messageTextView height without the text container vertical insets to stay aligned with the text. + selectedComponentHeight = CGRectGetHeight(messageTextViewFrame) - messageTextView.textContainerInset.top - messageTextView.textContainerInset.bottom; } - // Force the textView used underneath to layout its frame properly - [roomBubbleTableViewCell setNeedsLayout]; - [roomBubbleTableViewCell layoutIfNeeded]; - - selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y; + // Get the vertical position of the messageTextView relative to the contentView + selectedComponenContentViewYOffset = CGRectGetMinY(messageTextViewFrame); + + // Get the position of the component inside the messageTextView + selectedComponentPositionY = selectedComponent.position.y; } - + if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView) { CGFloat x = 0; @@ -801,8 +812,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = - (void)addTickView:(UIView *)tickView atIndex:(NSInteger)index { - CGRect componentFrame = [self componentFrameInContentViewForIndex: index]; - + CGRect componentFrame = [self componentFrameInContentViewForIndex:index]; tickView.frame = CGRectMake(self.contentView.bounds.size.width - tickView.frame.size.width - 2 * PlainRoomCellLayoutConstants.readReceiptsViewRightMargin, CGRectGetMaxY(componentFrame) - tickView.frame.size.height, tickView.frame.size.width, tickView.frame.size.height); [self.contentView addSubview:tickView]; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h index e934567b7..df9d12900 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h @@ -144,6 +144,15 @@ */ - (CGFloat)rawTextHeight:(NSAttributedString*)attributedText; +/** + Return the raw height of the provided text by removing any vertical margin/inset and constraining the width. + + @param attributedText the attributed text to measure + @param maxTextViewWidth the maximum text width + @return the computed height + */ +- (CGFloat)rawTextHeight:(NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth; + /** Return the content size of a text view initialized with the provided attributed text. CAUTION: This method runs only on main thread. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index 53b084c3a..c9a13d979 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -500,23 +500,34 @@ // Return the raw height of the provided text by removing any margin - (CGFloat)rawTextHeight: (NSAttributedString*)attributedText +{ + return [self rawTextHeight:attributedText withMaxWidth:_maxTextViewWidth]; +} + +// Return the raw height of the provided text by removing any vertical margin/inset and constraining the width. +- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth { __block CGSize textSize; if ([NSThread currentThread] != [NSThread mainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ - textSize = [self textContentSize:attributedText removeVerticalInset:YES]; + textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth]; }); } else { - textSize = [self textContentSize:attributedText removeVerticalInset:YES]; + textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth]; } return textSize.height; } - (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset +{ + return [self textContentSize:attributedText removeVerticalInset:removeVerticalInset maxTextViewWidth:_maxTextViewWidth]; +} + +- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset maxTextViewWidth:(CGFloat)maxTextViewWidth { static UITextView* measurementTextView = nil; static UITextView* measurementTextViewWithoutInset = nil; @@ -536,7 +547,7 @@ // Select the right text view for measurement UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView); - selectedTextView.frame = CGRectMake(0, 0, _maxTextViewWidth, 0); + selectedTextView.frame = CGRectMake(0, 0, maxTextViewWidth, 0); selectedTextView.attributedText = attributedText; // Force the layout manager to layout the text, fixes problems starting iOS 16 diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift index de15e91d3..f3f00f12f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift @@ -35,6 +35,15 @@ class TextMessageOutgoingWithoutSenderInfoBubbleCell: TextMessageBaseBubbleCell, self.textMessageContentView?.bubbleBackgroundView?.backgroundColor = theme.roomCellOutgoingBubbleBackgroundColor } + override func render(_ cellData: MXKCellData!) { + // This cell displays an outgoing message without any sender information. + // However, we need to set the following properties to our cellData, otherwise, to make room for the timestamp, a whitespace could be added when calculating the position of the components. + // If we don't, the component frame calculation will not work for this cell. + (cellData as? RoomBubbleCellData)?.shouldHideSenderName = false + (cellData as? RoomBubbleCellData)?.shouldHideSenderInformation = false + super.render(cellData) + } + // MARK: - Private private func setupBubbleConstraints() { diff --git a/changelog.d/pr-7512.bugfix b/changelog.d/pr-7512.bugfix new file mode 100644 index 000000000..1c6d3a98d --- /dev/null +++ b/changelog.d/pr-7512.bugfix @@ -0,0 +1 @@ +Fix the position of the send confirmation icon. From cacf97233acb514c2444ff4575e47bace003cff7 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 10:45:40 +0200 Subject: [PATCH 05/52] Add basic slash commands support to UserSuggestion module --- Riot/Modules/Room/RoomViewController.m | 8 ++ Riot/Modules/Room/RoomViewController.swift | 16 ++++ .../WysiwygInputToolbarView.swift | 4 + .../UserSuggestionCoordinator.swift | 42 ++++++++- .../UserSuggestionCoordinatorBridge.swift | 5 + .../Service/UserSuggestionService.swift | 93 ++++++++++++++----- .../UserSuggestionServiceProtocol.swift | 11 ++- .../UserSuggestion/UserSuggestionModels.swift | 16 +++- .../UserSuggestionScreenState.swift | 13 ++- .../UserSuggestionViewModel.swift | 18 +++- .../View/UserSuggestionList.swift | 17 ++-- .../View/UserSuggestionListItem.swift | 43 +++++---- 12 files changed, 226 insertions(+), 60 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 70b8d974c..57a431d2e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8076,6 +8076,14 @@ static CGSize kThreadListBarButtonItemImageSize; [self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; } +- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator + didRequestCommand:(NSString *)command + textTrigger:(NSString *)textTrigger +{ + [self removeTriggerTextFromComposer:textTrigger]; + [self setCommand:command]; +} + - (void)removeTriggerTextFromComposer:(NSString *)textTrigger { RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 3fec13de9..c94111be4 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -58,6 +58,22 @@ extension RoomViewController { } } + @objc func setCommand(_ command: String) { + if let wysiwygInputToolbar, wysiwygInputToolbar.textFormattingEnabled { + wysiwygInputToolbar.command(command) + wysiwygInputToolbar.becomeFirstResponder() + } else { + guard let attributedText = inputToolbarView.attributedTextMessage else { return } + + let newAttributedString = NSMutableAttributedString(attributedString: attributedText) + newAttributedString.append(NSAttributedString(string: "\(command) ", + attributes: [.font: inputToolbarView.defaultFont])) + + inputToolbarView.attributedTextMessage = newAttributedString + inputToolbarView.becomeFirstResponder() + } + } + /// Send the formatted text message and its raw counterpart to the room /// diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index f3fc1111b..5700909fa 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -195,6 +195,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp name: member.displayname, mentionType: .user) } + + func command(_ command: String) { + self.wysiwygViewModel.setCommand(name: command) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index a2156cd89..1999e6c07 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -23,6 +23,7 @@ import WysiwygComposer protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } @@ -52,6 +53,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { private var userSuggestionService: UserSuggestionServiceProtocol private var userSuggestionViewModel: UserSuggestionViewModelProtocol private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider + private var commandProvider: UserSuggestionCoordinatorCommandProvider private var cancellables = Set() @@ -69,7 +71,8 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { self.parameters = parameters roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) - userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider) + commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) + userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) @@ -90,11 +93,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { return } - guard let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first else { - return + if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { + self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { + self.delegate?.userSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.userSuggestionService.currentTextTrigger) } - - self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) } } @@ -199,3 +202,32 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr roomMembers.map { RoomMembersProviderMember(userId: $0.userId, displayName: $0.displayname ?? "", avatarUrl: $0.avatarUrl ?? "") } } } + +private class UserSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { + private let room: MXRoom + private let userID: String + + var commands: [String] = [] + + init(room: MXRoom, userID: String) { + self.room = room + self.userID = userID + updateWithPowerLevels() + } + + func updateWithPowerLevels() { + // TODO: filter commands in terms of user power level ? + } + + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + self.commands = [ + "/ban", + "/invite", + "/join", + "/me" + ] + + // TODO: get real data + commands(self.commands.map { CommandsProviderCommand(name: $0) }) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index 0d1f6795e..ba1bc75ca 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -20,6 +20,7 @@ import Foundation protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?) + func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) } @@ -68,6 +69,10 @@ extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) } + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { + delegate?.userSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) + } + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index a790e2845..76d41e700 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -24,6 +24,10 @@ struct RoomMembersProviderMember { var avatarUrl: String } +struct CommandsProviderCommand { + var name: String +} + class UserSuggestionID: NSObject { /// A special case added for suggesting `@room` mentions. @objc static let room = "@room" @@ -34,26 +38,35 @@ protocol RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) } +protocol CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) +} + struct UserSuggestionServiceItem: UserSuggestionItemProtocol { let userId: String let displayName: String? let avatarUrl: String? } +struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol { + let name: String +} + class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Properties // MARK: Private private let roomMemberProvider: RoomMembersProviderProtocol + private let commandProvider: CommandsProviderProtocol - private var suggestionItems: [UserSuggestionItemProtocol] = [] + private var suggestionItems: [SuggestionItem] = [] private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public - var items = CurrentValueSubject<[UserSuggestionItemProtocol], Never>([]) + var items = CurrentValueSubject<[SuggestionItem], Never>([]) var currentTextTrigger: String? { currentTextTriggerSubject.value @@ -61,8 +74,11 @@ class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Setup - init(roomMemberProvider: RoomMembersProviderProtocol, shouldDebounce: Bool = true) { + init(roomMemberProvider: RoomMembersProviderProtocol, + commandProvider: CommandsProviderProtocol, + shouldDebounce: Bool = true) { self.roomMemberProvider = roomMemberProvider + self.commandProvider = commandProvider if shouldDebounce { currentTextTriggerSubject @@ -83,7 +99,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard let textMessage = textMessage, textMessage.count > 0, let lastComponent = textMessage.components(separatedBy: .whitespaces).last, - lastComponent.prefix(while: { $0 == "@" }).count == 1 // Partial username should start with one and only one "@" character + lastComponent.prefix(while: { $0 == "@" || $0 == "/" }).count == 1 // Partial username should start with one and only one "@" character else { items.send([]) currentTextTriggerSubject.send(nil) @@ -94,13 +110,22 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { - guard let suggestionPattern, suggestionPattern.key == .at else { + guard let suggestionPattern else { items.send([]) currentTextTriggerSubject.send(nil) return } - currentTextTriggerSubject.send("@" + suggestionPattern.text) + switch suggestionPattern.key { + case .at: + currentTextTriggerSubject.send("@" + suggestionPattern.text) + case .hash: + // No room suggestion support yet + items.send([]) + currentTextTriggerSubject.send(nil) + case .slash: + currentTextTriggerSubject.send("/" + suggestionPattern.text) + } } // MARK: - Private @@ -109,24 +134,48 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard var partialName = textTrigger else { return } - - partialName.removeFirst() // remove the '@' prefix - - roomMemberProvider.fetchMembers { [weak self] members in - guard let self = self else { - return + + switch partialName.first { + case "@": + partialName.removeFirst() // remove the '@' prefix + + roomMemberProvider.fetchMembers { [weak self] members in + guard let self = self else { + return + } + + self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in + SuggestionItem.user(value: UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) + } + + self.items.send(self.suggestionItems.filter { item in + guard case let .user(userSuggestion) = item else { return false } + + let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) + let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) + + return (containedInUsername || containedInDisplayName) + }) } - - self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in - UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl) + case "/": + // TODO: send all commands if only text is "/" + partialName.removeFirst() + + commandProvider.fetchCommands { [weak self] commands in + guard let self else { return } + + self.suggestionItems = commands.map { command in + SuggestionItem.command(value: CommandSuggestionServiceItem(name: command.name)) + } + + self.items.send(self.suggestionItems.filter { item in + guard case let .command(commandSuggestion) = item else { return false } + + return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + }) } - - self.items.send(self.suggestionItems.filter { userSuggestion in - let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) - - return (containedInUsername || containedInDisplayName) - }) + default: + return } } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 43006dbed..4b5787cff 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -24,8 +24,17 @@ protocol UserSuggestionItemProtocol: Avatarable { var avatarUrl: String? { get } } +protocol CommandSuggestionItemProtocol { + var name: String { get } +} + +enum SuggestionItem { + case command(value: CommandSuggestionItemProtocol) + case user(value: UserSuggestionItemProtocol) +} + protocol UserSuggestionServiceProtocol { - var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } + var items: CurrentValueSubject<[SuggestionItem], Never> { get } var currentTextTrigger: String? { get } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift index d4e984f88..dbaaf9295 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift @@ -24,10 +24,18 @@ enum UserSuggestionViewModelResult { case selectedItemWithIdentifier(String) } -struct UserSuggestionViewStateItem: Identifiable { - let id: String - let avatar: AvatarInputProtocol? - let displayName: String? +enum UserSuggestionViewStateItem: Identifiable { + case command(name: String) + case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) + + var id: String { + switch self { + case .command(let name): + return name + case .user(let id, _, _): + return id + } + } } struct UserSuggestionViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index 0a9395fa5..95aea9dbe 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -27,7 +27,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let service = UserSuggestionService(roomMemberProvider: self) + let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self) let listViewModel = UserSuggestionViewModel(userSuggestionService: service) let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in @@ -60,3 +60,14 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { } } } + +extension MockUserSuggestionScreenState: CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + commands([ + CommandsProviderCommand(name: "/ban"), + CommandsProviderCommand(name: "/invite"), + CommandsProviderCommand(name: "/join"), + CommandsProviderCommand(name: "/me") + ]) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift index 3999447b7..68d573bdf 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift @@ -40,14 +40,28 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo self.userSuggestionService = userSuggestionService let items = userSuggestionService.items.value.map { suggestionItem in - UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) + switch suggestionItem { + case .command(let commandSuggestionItem): + return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) + case .user(let userSuggestionItem): + return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, + avatar: userSuggestionItem, + displayName: userSuggestionItem.displayName) + } } super.init(initialViewState: UserSuggestionViewState(items: items)) userSuggestionService.items.sink { [weak self] items in self?.state.items = items.map { item in - UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) + switch item { + case .command(let commandSuggestionItem): + return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) + case .user(let userSuggestionItem): + return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, + avatar: userSuggestionItem, + displayName: userSuggestionItem.displayName) + } } }.store(in: &cancellables) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index e509a58b3..fe0c21761 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -51,9 +51,12 @@ struct UserSuggestionList: View { EmptyView() } else { ZStack { - UserSuggestionListItem(avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"), - displayName: "Prototype", - userId: "Prototype") + UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + id: "Prototype", + avatar: AvatarInput(mxContentUri: "", + matrixItemId: "", + displayName: "Prototype"), + displayName: "Prototype")) .background(ViewFrameReader(frame: $prototypeListItemFrame)) .hidden() if showBackgroundShadow { @@ -76,12 +79,8 @@ struct UserSuggestionList: View { Button { viewModel.send(viewAction: .selectedItem(item)) } label: { - UserSuggestionListItem( - avatar: item.avatar, - displayName: item.displayName, - userId: item.id - ) - .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) + UserSuggestionListItem(content: item) + .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) } } .listStyle(PlainListStyle()) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index 862e7573d..0175c2abe 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -25,26 +25,33 @@ struct UserSuggestionListItem: View { // MARK: Public - let avatar: AvatarInputProtocol? - let displayName: String? - let userId: String + let content: UserSuggestionViewStateItem var body: some View { HStack { - if let avatar = avatar { - AvatarImage(avatarData: avatar, size: .medium) - } - VStack(alignment: .leading) { - Text(displayName ?? "") + switch content { + case .command(let name): + Text(name) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "displayNameText") - .lineLimit(1) - Text(userId) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.tertiaryContent) - .accessibility(identifier: "userIdText") + .accessibility(identifier: "nameText") .lineLimit(1) + case .user(let userId, let avatar, let displayName): + if let avatar = avatar { + AvatarImage(avatarData: avatar, size: .medium) + } + VStack(alignment: .leading) { + Text(displayName ?? "") + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "displayNameText") + .lineLimit(1) + Text(userId) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "userIdText") + .lineLimit(1) + } } } } @@ -54,7 +61,11 @@ struct UserSuggestionListItem: View { struct UserSuggestionHeader_Previews: PreviewProvider { static var previews: some View { - UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") - .environmentObject(AvatarViewModel.withMockedServices()) + UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + id: "@alice:matrix.org", + avatar: MockAvatarInput.example, + displayName: "Alice" + )) + .environmentObject(AvatarViewModel.withMockedServices()) } } From d28010098a235bbe9f8584e84214672b6f712a4b Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 14:22:21 +0200 Subject: [PATCH 06/52] Rename `UserSuggestion` module as `CompletionSuggestion` --- Riot/Modules/Room/RoomViewController.h | 2 +- Riot/Modules/Room/RoomViewController.m | 56 ++++++------ Riot/Modules/Room/RoomViewController.xib | 24 ++---- .../Views/InputToolbar/RoomInputToolbarView.h | 4 +- .../WysiwygInputToolbarView.swift | 2 +- .../Modules/Common/Mock/MockAppScreens.swift | 2 +- .../CompletionSuggestionModels.swift} | 12 +-- .../CompletionSuggestionScreenState.swift} | 16 ++-- .../CompletionSuggestionViewModel.swift | 77 +++++++++++++++++ ...mpletionSuggestionViewModelProtocol.swift} | 10 +-- .../CompletionSuggestionCoordinator.swift} | 86 +++++++++---------- ...ompletionSuggestionCoordinatorBridge.swift | 79 +++++++++++++++++ .../CompletionSuggestionService.swift} | 26 +++--- ...CompletionSuggestionServiceProtocol.swift} | 16 ++-- .../UI/CompletionSuggestionUITests.swift} | 6 +- .../CompletionSuggestionServiceTests.swift} | 60 +++++++++---- .../View/CompletionSuggestionList.swift} | 12 +-- .../View/CompletionSuggestionListItem.swift} | 8 +- .../CompletionSuggestionListWithInput.swift} | 14 +-- .../Composer/MockComposerScreenState.swift | 8 +- .../Room/Composer/Model/ComposerModels.swift | 8 +- .../Modules/Room/Composer/View/Composer.swift | 8 +- .../UserSuggestionCoordinatorBridge.swift | 79 ----------------- .../UserSuggestionViewModel.swift | 77 ----------------- 24 files changed, 353 insertions(+), 339 deletions(-) rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionModels.swift => CompletionSuggestion/CompletionSuggestionModels.swift} (76%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionScreenState.swift => CompletionSuggestion/CompletionSuggestionScreenState.swift} (75%) create mode 100644 RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionViewModelProtocol.swift => CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift} (67%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Coordinator/UserSuggestionCoordinator.swift => CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift} (59%) create mode 100644 RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift rename RiotSwiftUI/Modules/Room/{UserSuggestion/Service/UserSuggestionService.swift => CompletionSuggestion/Service/CompletionSuggestionService.swift} (80%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Service/UserSuggestionServiceProtocol.swift => CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift} (71%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Test/UI/UserSuggestionUITests.swift => CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift} (79%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift => CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift} (61%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionList.swift => CompletionSuggestion/View/CompletionSuggestionList.swift} (92%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionListItem.swift => CompletionSuggestion/View/CompletionSuggestionListItem.swift} (89%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionListWithInput.swift => CompletionSuggestion/View/CompletionSuggestionListWithInput.swift} (75%) delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 072a882a6..6cc25bcfe 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -61,7 +61,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; // The preview header @property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; -@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *userSuggestionContainerHeightConstraint; +@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *completionSuggestionContainerHeightConstraint; // The jump to last unread banner @property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 57a431d2e..38f71f8d1 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate> { // The preview header @@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize; @property (nonatomic, strong) ShareManager *shareManager; @property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder; -@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; -@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; +@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; @@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize; [self setupActions]; - [self setupUserSuggestionViewIfNeeded]; + [self setupCompletionSuggestionViewIfNeeded]; [self.topBannersStackView vc_removeAllSubviews]; } @@ -1088,12 +1088,12 @@ static CGSize kThreadListBarButtonItemImageSize; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; _voiceMessageController.roomId = dataSource.roomId; - _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager + _completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager room:dataSource.room userID:self.roomDataSource.mxSession.myUserId]; - _userSuggestionCoordinator.delegate = self; + _completionSuggestionCoordinator.delegate = self; - [self setupUserSuggestionViewIfNeeded]; + [self setupCompletionSuggestionViewIfNeeded]; [self updateTopBanners]; } @@ -2726,13 +2726,13 @@ static CGSize kThreadListBarButtonItemImageSize; } } -- (void)setupUserSuggestionViewIfNeeded +- (void)setupCompletionSuggestionViewIfNeeded { if(!self.isViewLoaded) { return; } - UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable; if (!suggestionsViewController) { @@ -2742,12 +2742,12 @@ static CGSize kThreadListBarButtonItemImageSize; [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; [self addChildViewController:suggestionsViewController]; - [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + [self.completionSuggestionContainerView addSubview:suggestionsViewController.view]; - [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], - [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], - [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], - [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]]; [suggestionsViewController didMoveToParentViewController:self]; } @@ -5147,17 +5147,17 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView { - [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; + [self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage]; } - (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern { - [self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; + [self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern]; } -- (UserSuggestionViewModelContextWrapper *)userSuggestionContext +- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext { - return [self.userSuggestionCoordinator sharedContext]; + return [self.completionSuggestionCoordinator sharedContext]; } - (MXMediaManager *)mediaManager @@ -8059,9 +8059,9 @@ static CGSize kThreadListBarButtonItemImageSize; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; } -#pragma mark - UserSuggestionCoordinatorBridgeDelegate +#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didRequestMentionForMember:(MXRoomMember *)member textTrigger:(NSString *)textTrigger { @@ -8069,16 +8069,16 @@ static CGSize kThreadListBarButtonItemImageSize; [self mention:member]; } -- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator +- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator textTrigger:(NSString *)textTrigger { [self removeTriggerTextFromComposer:textTrigger]; - [self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; + [self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]]; } -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator - didRequestCommand:(NSString *)command - textTrigger:(NSString *)textTrigger +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator + didRequestCommand:(NSString *)command + textTrigger:(NSString *)textTrigger { [self removeTriggerTextFromComposer:textTrigger]; [self setCommand:command]; @@ -8097,11 +8097,11 @@ static CGSize kThreadListBarButtonItemImageSize; } } -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height { - if (self.userSuggestionContainerHeightConstraint.constant != height) + if (self.completionSuggestionContainerHeightConstraint.constant != height) { - self.userSuggestionContainerHeightConstraint.constant = height; + self.completionSuggestionContainerHeightConstraint.constant = height; [self.view layoutIfNeeded]; } diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index b7a62a8bf..cdb656508 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,8 @@ - + - - + @@ -13,6 +12,8 @@ + + @@ -32,8 +33,6 @@ - - @@ -48,20 +47,20 @@ - + - + - + - + @@ -237,11 +236,6 @@ - - - - - diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index df71790be..5bbdeaa51 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -22,7 +22,7 @@ @class RoomInputToolbarView; @class LinkActionWrapper; @class SuggestionPatternWrapper; -@class UserSuggestionViewModelContextWrapper; +@class CompletionSuggestionViewModelContextWrapper; /** Destination of the message in the composer @@ -84,7 +84,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; -- (UserSuggestionViewModelContextWrapper *)userSuggestionContext; +- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext; - (MXMediaManager *)mediaManager; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 5700909fa..9bc02c21e 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -223,7 +223,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp let composer = Composer( viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, - userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context, + completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context, resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 91bf25a51..0afe12c02 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -51,7 +51,7 @@ enum MockAppScreens { MockStaticLocationViewingScreenState.self, MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, - MockUserSuggestionScreenState.self, + MockCompletionSuggestionScreenState.self, MockPollEditFormScreenState.self, MockSpaceCreationEmailInvitesScreenState.self, MockSpaceSettingsScreenState.self, diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift similarity index 76% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift index dbaaf9295..91fc4ffeb 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift @@ -16,15 +16,15 @@ import Foundation -enum UserSuggestionViewAction { - case selectedItem(UserSuggestionViewStateItem) +enum CompletionSuggestionViewAction { + case selectedItem(CompletionSuggestionViewStateItem) } -enum UserSuggestionViewModelResult { +enum CompletionSuggestionViewModelResult { case selectedItemWithIdentifier(String) } -enum UserSuggestionViewStateItem: Identifiable { +enum CompletionSuggestionViewStateItem: Identifiable { case command(name: String) case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) @@ -38,6 +38,6 @@ enum UserSuggestionViewStateItem: Identifiable { } } -struct UserSuggestionViewState: BindableState { - var items: [UserSuggestionViewStateItem] +struct CompletionSuggestionViewState: BindableState { + var items: [CompletionSuggestionViewStateItem] } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift similarity index 75% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 95aea9dbe..1427c3f3f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -17,32 +17,32 @@ import Foundation import SwiftUI -enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { +enum MockCompletionSuggestionScreenState: MockScreenState, CaseIterable { case multipleResults private static var members: [RoomMembersProviderMember]! var screenType: Any.Type { - UserSuggestionList.self + CompletionSuggestionList.self } var screenView: ([Any], AnyView) { - let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self) - let listViewModel = UserSuggestionViewModel(userSuggestionService: service) + let service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self) + let listViewModel = CompletionSuggestionViewModel(completionSuggestionService: service) - let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in + let viewModel = CompletionSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in service.processTextMessage(textMessage) } return ( [service, listViewModel], - AnyView(UserSuggestionListWithInput(viewModel: viewModel) + AnyView(CompletionSuggestionListWithInput(viewModel: viewModel) .environmentObject(AvatarViewModel.withMockedServices())) ) } } -extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { +extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { var canMentionRoom: Bool { false } func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { @@ -61,7 +61,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { } } -extension MockUserSuggestionScreenState: CommandsProviderProtocol { +extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban"), diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift new file mode 100644 index 000000000..01c881970 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -0,0 +1,77 @@ +// +// 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 CompletionSuggestionViewModelType = StateStoreViewModel + +class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, CompletionSuggestionViewModelProtocol { + // MARK: - Properties + + // MARK: Private + + private let completionSuggestionService: CompletionSuggestionServiceProtocol + + // MARK: Public + + var sharedContext: CompletionSuggestionViewModelType.Context { + return self.context + } + + var completion: ((CompletionSuggestionViewModelResult) -> Void)? + + // MARK: - Setup + + init(completionSuggestionService: CompletionSuggestionServiceProtocol) { + self.completionSuggestionService = completionSuggestionService + + let items = completionSuggestionService.items.value.map { suggestionItem in + switch suggestionItem { + case .command(let completionSuggestionCommandItem): + return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + case .user(let completionSuggestionUserItem): + return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, + avatar: completionSuggestionUserItem, + displayName: completionSuggestionUserItem.displayName) + } + } + + super.init(initialViewState: CompletionSuggestionViewState(items: items)) + + completionSuggestionService.items.sink { [weak self] items in + self?.state.items = items.map { item in + switch item { + case .command(let completionSuggestionCommandItem): + return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + case .user(let completionSuggestionUserItem): + return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, + avatar: completionSuggestionUserItem, + displayName: completionSuggestionUserItem.displayName) + } + } + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: CompletionSuggestionViewAction) { + switch viewAction { + case .selectedItem(let item): + completion?(.selectedItemWithIdentifier(item.id)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift similarity index 67% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift index 33aa5bb79..d7c51909f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift @@ -16,10 +16,10 @@ import Foundation -protocol UserSuggestionViewModelProtocol { - /// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple - /// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` +protocol CompletionSuggestionViewModelProtocol { + /// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple + /// `CompletionSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` /// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data. - var sharedContext: UserSuggestionViewModelType.Context { get } - var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } + var sharedContext: CompletionSuggestionViewModelType.Context { get } + var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift similarity index 59% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 1999e6c07..8da2356fd 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -20,40 +20,40 @@ import SwiftUI import UIKit import WysiwygComposer -protocol UserSuggestionCoordinatorDelegate: AnyObject { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) - func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) +protocol CompletionSuggestionCoordinatorDelegate: AnyObject { + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } -struct UserSuggestionCoordinatorParameters { +struct CompletionSuggestionCoordinatorParameters { let mediaManager: MXMediaManager let room: MXRoom let userID: String } -/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c. -final class UserSuggestionViewModelContextWrapper: NSObject { - let context: UserSuggestionViewModelType.Context +/// Wrapper around `CompletionSuggestionViewModelType.Context` to pass it through obj-c. +final class CompletionSuggestionViewModelContextWrapper: NSObject { + let context: CompletionSuggestionViewModelType.Context - init(context: UserSuggestionViewModelType.Context) { + init(context: CompletionSuggestionViewModelType.Context) { self.context = context } } -final class UserSuggestionCoordinator: Coordinator, Presentable { +final class CompletionSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties // MARK: Private - private let parameters: UserSuggestionCoordinatorParameters + private let parameters: CompletionSuggestionCoordinatorParameters - private var userSuggestionHostingController: UIHostingController - private var userSuggestionService: UserSuggestionServiceProtocol - private var userSuggestionViewModel: UserSuggestionViewModelProtocol - private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider - private var commandProvider: UserSuggestionCoordinatorCommandProvider + private var completionSuggestionHostingController: UIHostingController + private var completionSuggestionService: CompletionSuggestionServiceProtocol + private var completionSuggestionViewModel: CompletionSuggestionViewModelProtocol + private var roomMemberProvider: CompletionSuggestionCoordinatorRoomMemberProvider + private var commandProvider: CompletionSuggestionCoordinatorCommandProvider private var cancellables = Set() @@ -63,57 +63,57 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? - weak var delegate: UserSuggestionCoordinatorDelegate? + weak var delegate: CompletionSuggestionCoordinatorDelegate? // MARK: - Setup - init(parameters: UserSuggestionCoordinatorParameters) { + init(parameters: CompletionSuggestionCoordinatorParameters) { self.parameters = parameters - roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) - commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) - userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) + roomMemberProvider = CompletionSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) + commandProvider = CompletionSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) + completionSuggestionService = CompletionSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) - let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: viewModel.context) + let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService) + let view = CompletionSuggestionList(viewModel: viewModel.context) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) - userSuggestionViewModel = viewModel - userSuggestionHostingController = VectorHostingController(rootView: view) + completionSuggestionViewModel = viewModel + completionSuggestionHostingController = VectorHostingController(rootView: view) - userSuggestionViewModel.completion = { [weak self] result in + completionSuggestionViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .selectedItemWithIdentifier(let identifier): - if identifier == UserSuggestionID.room { - self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger) + if identifier == CompletionSuggestionUserID.room { + self.delegate?.completionSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.completionSuggestionService.currentTextTrigger) return } if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { - self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) + self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { - self.delegate?.userSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.userSuggestionService.currentTextTrigger) + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } - userSuggestionService.items.sink { [weak self] _ in + completionSuggestionService.items.sink { [weak self] _ in guard let self = self else { return } - self.delegate?.userSuggestionCoordinator(self, + self.delegate?.completionSuggestionCoordinator(self, didUpdateViewHeight: self.calculateViewHeight()) }.store(in: &cancellables) } func processTextMessage(_ textMessage: String) { - userSuggestionService.processTextMessage(textMessage) + completionSuggestionService.processTextMessage(textMessage) } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { - userSuggestionService.processSuggestionPattern(suggestionPattern) + completionSuggestionService.processSuggestionPattern(suggestionPattern) } // MARK: - Public @@ -121,18 +121,18 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - userSuggestionHostingController + completionSuggestionHostingController } - func sharedContext() -> UserSuggestionViewModelContextWrapper { - UserSuggestionViewModelContextWrapper(context: userSuggestionViewModel.sharedContext) + func sharedContext() -> CompletionSuggestionViewModelContextWrapper { + CompletionSuggestionViewModelContextWrapper(context: completionSuggestionViewModel.sharedContext) } // MARK: - Private private func calculateViewHeight() -> CGFloat { - let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: viewModel.context) + let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService) + let view = CompletionSuggestionList(viewModel: viewModel.context) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) let controller = VectorHostingController(rootView: view) @@ -156,7 +156,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { } } -private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { +private class CompletionSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { private let room: MXRoom private let userID: String @@ -194,7 +194,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr self.roomMembers = joinedMembers members(self.roomMembersToProviderMembers(joinedMembers)) } failure: { error in - MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) + MXLog.error("[CompletionSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) } } @@ -203,7 +203,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr } } -private class UserSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { +private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { private let room: MXRoom private let userID: String diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift new file mode 100644 index 000000000..83a9ed94c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift @@ -0,0 +1,79 @@ +// +// 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 + +@objc +protocol CompletionSuggestionCoordinatorBridgeDelegate: AnyObject { + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinatorBridge, textTrigger: String?) + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) +} + +@objcMembers +final class CompletionSuggestionCoordinatorBridge: NSObject { + private var _completionSuggestionCoordinator: Any? + fileprivate var completionSuggestionCoordinator: CompletionSuggestionCoordinator { + _completionSuggestionCoordinator as! CompletionSuggestionCoordinator + } + + weak var delegate: CompletionSuggestionCoordinatorBridgeDelegate? + + init(mediaManager: MXMediaManager, room: MXRoom, userID: String) { + let parameters = CompletionSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID) + let completionSuggestionCoordinator = CompletionSuggestionCoordinator(parameters: parameters) + _completionSuggestionCoordinator = completionSuggestionCoordinator + + super.init() + + completionSuggestionCoordinator.delegate = self + } + + func processTextMessage(_ textMessage: String) { + completionSuggestionCoordinator.processTextMessage(textMessage) + } + + func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) { + completionSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern) + } + + func toPresentable() -> UIViewController? { + completionSuggestionCoordinator.toPresentable() + } + + func sharedContext() -> CompletionSuggestionViewModelContextWrapper { + completionSuggestionCoordinator.sharedContext() + } +} + +extension CompletionSuggestionCoordinatorBridge: CompletionSuggestionCoordinatorDelegate { + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) + } + + func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) + } + + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) + } + + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { + delegate?.completionSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift similarity index 80% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 76d41e700..0353b63d4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -28,7 +28,7 @@ struct CommandsProviderCommand { var name: String } -class UserSuggestionID: NSObject { +class CompletionSuggestionUserID: NSObject { /// A special case added for suggesting `@room` mentions. @objc static let room = "@room" } @@ -42,17 +42,17 @@ protocol CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) } -struct UserSuggestionServiceItem: UserSuggestionItemProtocol { +struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol { let userId: String let displayName: String? let avatarUrl: String? } -struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol { +struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol { let name: String } -class UserSuggestionService: UserSuggestionServiceProtocol { +class CompletionSuggestionService: CompletionSuggestionServiceProtocol { // MARK: - Properties // MARK: Private @@ -60,13 +60,13 @@ class UserSuggestionService: UserSuggestionServiceProtocol { private let roomMemberProvider: RoomMembersProviderProtocol private let commandProvider: CommandsProviderProtocol - private var suggestionItems: [SuggestionItem] = [] + private var suggestionItems: [CompletionSuggestionItem] = [] private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public - var items = CurrentValueSubject<[SuggestionItem], Never>([]) + var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([]) var currentTextTrigger: String? { currentTextTriggerSubject.value @@ -93,7 +93,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } } - // MARK: - UserSuggestionServiceProtocol + // MARK: - CompletionSuggestionServiceProtocol func processTextMessage(_ textMessage: String?) { guard let textMessage = textMessage, @@ -145,14 +145,14 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in - SuggestionItem.user(value: UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) + CompletionSuggestionItem.user(value: CompletionSuggestionServiceUserItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) } self.items.send(self.suggestionItems.filter { item in - guard case let .user(userSuggestion) = item else { return false } + guard case let .user(completionSuggestionUserItem) = item else { return false } - let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) + let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased()) + let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased()) return (containedInUsername || containedInDisplayName) }) @@ -165,7 +165,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard let self else { return } self.suggestionItems = commands.map { command in - SuggestionItem.command(value: CommandSuggestionServiceItem(name: command.name)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name)) } self.items.send(self.suggestionItems.filter { item in @@ -184,6 +184,6 @@ extension Array where Element == RoomMembersProviderMember { /// Returns the array with an additional member that represents an `@room` mention. func withRoom(_ canMentionRoom: Bool) -> Self { guard canMentionRoom else { return self } - return self + [RoomMembersProviderMember(userId: UserSuggestionID.room, displayName: "Everyone", avatarUrl: "")] + return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")] } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift similarity index 71% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift index 4b5787cff..4586e1294 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift @@ -18,23 +18,23 @@ import Combine import Foundation import WysiwygComposer -protocol UserSuggestionItemProtocol: Avatarable { +protocol CompletionSuggestionUserItemProtocol: Avatarable { var userId: String { get } var displayName: String? { get } var avatarUrl: String? { get } } -protocol CommandSuggestionItemProtocol { +protocol CompletionSuggestionCommandItemProtocol { var name: String { get } } -enum SuggestionItem { - case command(value: CommandSuggestionItemProtocol) - case user(value: UserSuggestionItemProtocol) +enum CompletionSuggestionItem { + case command(value: CompletionSuggestionCommandItemProtocol) + case user(value: CompletionSuggestionUserItemProtocol) } -protocol UserSuggestionServiceProtocol { - var items: CurrentValueSubject<[SuggestionItem], Never> { get } +protocol CompletionSuggestionServiceProtocol { + var items: CurrentValueSubject<[CompletionSuggestionItem], Never> { get } var currentTextTrigger: String? { get } @@ -44,7 +44,7 @@ protocol UserSuggestionServiceProtocol { // MARK: Avatarable -extension UserSuggestionItemProtocol { +extension CompletionSuggestionUserItemProtocol { var mxContentUri: String? { avatarUrl } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift similarity index 79% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift index f44744a9c..5ec9d4b9b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift @@ -17,9 +17,9 @@ import RiotSwiftUI import XCTest -class UserSuggestionUITests: MockScreenTestCase { - func testUserSuggestionScreen() throws { - app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title) +class CompletionSuggestionUITests: MockScreenTestCase { + func testCompletionSuggestionScreen() throws { + app.goToScreenWithIdentifier(MockCompletionSuggestionScreenState.multipleResults.title) let firstButton = app.buttons["displayNameText-userIdText"].firstMatch XCTAssert(firstButton.waitForExistence(timeout: 10)) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift similarity index 61% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 7ae0bfa39..636ba3355 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -19,51 +19,53 @@ import XCTest @testable import RiotSwiftUI -class UserSuggestionServiceTests: XCTestCase { - var service: UserSuggestionService! +class CompletionSuggestionServiceTests: XCTestCase { + var service: CompletionSuggestionService! var canMentionRoom = false override func setUp() { - service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) + service = CompletionSuggestionService(roomMemberProvider: self, + commandProvider: self, + shouldDebounce: false) canMentionRoom = false } func testAlice() { service.processTextMessage("@Al") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@al") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@ice") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@Alice") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@alice:matrix.org") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") } func testBob() { service.processTextMessage("@ob") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") service.processTextMessage("@ob:") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") service.processTextMessage("@b:matrix") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") } func testBoth() { service.processTextMessage("@:matrix") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") - XCTAssertEqual(service.items.value.last?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob") service.processTextMessage("@.org") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") - XCTAssertEqual(service.items.value.last?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob") } func testEmptyResult() { @@ -117,18 +119,18 @@ class UserSuggestionServiceTests: XCTestCase { } func testRoomWithPower() { - // Given a user without the power to mention a room. + // Given a user with the power to mention a room. canMentionRoom = true - // Given a user without the power to mention a room. + // Given a user with the power to mention a room. service.processTextMessage("@ro") // Then the completion for a room mention should be shown. - XCTAssertEqual(service.items.value.first?.userId, UserSuggestionID.room) + XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room) } } -extension UserSuggestionServiceTests: RoomMembersProviderProtocol { +extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { let users = [("Alice", "@alice:matrix.org"), ("Bob", "@bob:matrix.org")] @@ -138,3 +140,23 @@ extension UserSuggestionServiceTests: RoomMembersProviderProtocol { }) } } + +extension CompletionSuggestionServiceTests: CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + let commandList = ["/ban", "/invite", "/join", "/me"] + + commands(commandList.map { command in + CommandsProviderCommand(name: command) + }) + } +} + +extension CompletionSuggestionItem { + var asUser: CompletionSuggestionUserItemProtocol? { + if case let .user(value) = self { return value } else { return nil } + } + + var asCommand: CompletionSuggestionCommandItemProtocol? { + if case let .command(value) = self { return value } else { return nil } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift similarity index 92% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift index fe0c21761..02aef8a1f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserSuggestionList: View { +struct CompletionSuggestionList: View { private enum Constants { static let topPadding: CGFloat = 8.0 static let listItemPadding: CGFloat = 4.0 @@ -43,7 +43,7 @@ struct UserSuggestionList: View { // MARK: Public - @ObservedObject var viewModel: UserSuggestionViewModel.Context + @ObservedObject var viewModel: CompletionSuggestionViewModel.Context var showBackgroundShadow: Bool = true var body: some View { @@ -51,7 +51,7 @@ struct UserSuggestionList: View { EmptyView() } else { ZStack { - UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( id: "Prototype", avatar: AvatarInput(mxContentUri: "", matrixItemId: "", @@ -79,7 +79,7 @@ struct UserSuggestionList: View { Button { viewModel.send(viewAction: .selectedItem(item)) } label: { - UserSuggestionListItem(content: item) + CompletionSuggestionListItem(content: item) .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) } } @@ -134,8 +134,8 @@ private struct BackgroundView: View { // MARK: - Previews -struct UserSuggestion_Previews: PreviewProvider { - static let stateRenderer = MockUserSuggestionScreenState.stateRenderer +struct CompletionSuggestion_Previews: PreviewProvider { + static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift similarity index 89% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index 0175c2abe..c30ec5d89 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserSuggestionListItem: View { +struct CompletionSuggestionListItem: View { // MARK: - Properties // MARK: Private @@ -25,7 +25,7 @@ struct UserSuggestionListItem: View { // MARK: Public - let content: UserSuggestionViewStateItem + let content: CompletionSuggestionViewStateItem var body: some View { HStack { @@ -59,9 +59,9 @@ struct UserSuggestionListItem: View { // MARK: - Previews -struct UserSuggestionHeader_Previews: PreviewProvider { +struct CompletionSuggestionHeader_Previews: PreviewProvider { static var previews: some View { - UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( id: "@alice:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice" diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift similarity index 75% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift index 176be8ec4..0b1dd8e8a 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift @@ -16,24 +16,24 @@ import SwiftUI -struct UserSuggestionListWithInputViewModel { - let listViewModel: UserSuggestionViewModel +struct CompletionSuggestionListWithInputViewModel { + let listViewModel: CompletionSuggestionViewModel let callback: (String) -> Void } -struct UserSuggestionListWithInput: View { +struct CompletionSuggestionListWithInput: View { // MARK: - Properties // MARK: Private // MARK: Public - var viewModel: UserSuggestionListWithInputViewModel + var viewModel: CompletionSuggestionListWithInputViewModel @State private var inputText = "" var body: some View { VStack(spacing: 0.0) { - UserSuggestionList(viewModel: viewModel.listViewModel.context) + CompletionSuggestionList(viewModel: viewModel.listViewModel.context) TextField("Search for user", text: $inputText) .background(Color.white) .onChange(of: inputText, perform: viewModel.callback) @@ -48,8 +48,8 @@ struct UserSuggestionListWithInput: View { // MARK: - Previews -struct UserSuggestionListWithInput_Previews: PreviewProvider { - static let stateRenderer = MockUserSuggestionScreenState.stateRenderer +struct CompletionSuggestionListWithInput_Previews: PreviewProvider { + static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index 8b5327b14..79322b78a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -29,7 +29,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel - let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: [])) + let completionSuggestionViewModel = MockCompletionSuggestionViewModel(initialViewState: CompletionSuggestionViewState(items: [])) let bindings = ComposerBindings(focused: false) switch self { @@ -67,7 +67,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { Spacer() Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, - userSuggestionSharedContext: userSuggestionViewModel.context, + completionSuggestionSharedContext: completionSuggestionViewModel.context, resizeAnimationDuration: 0.1, sendMessageAction: { _ in }, showSendMediaActions: { }) @@ -82,6 +82,4 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { } } -private final class MockUserSuggestionViewModel: UserSuggestionViewModelType { - -} +private final class MockCompletionSuggestionViewModel: CompletionSuggestionViewModelType { } diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 6f7bab165..33d73ef4a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -257,11 +257,11 @@ final class SuggestionPatternWrapper: NSObject { } } -final class UserSuggestionViewModelWrapper: NSObject { - let userSuggestionViewModel: UserSuggestionViewModel +final class CompletionSuggestionViewModelWrapper: NSObject { + let completionSuggestionViewModel: CompletionSuggestionViewModel - init(_ userSuggestionViewModel: UserSuggestionViewModel) { - self.userSuggestionViewModel = userSuggestionViewModel + init(_ completionSuggestionViewModel: CompletionSuggestionViewModel) { + self.completionSuggestionViewModel = completionSuggestionViewModel super.init() } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index e4317a275..a74b0bb4d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -23,7 +23,7 @@ struct Composer: View { // MARK: Private @ObservedObject private var viewModel: ComposerViewModelType.Context @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel - private let userSuggestionSharedContext: UserSuggestionViewModelType.Context + private let completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context private let resizeAnimationDuration: Double private let sendMessageAction: (WysiwygComposerContent) -> Void @@ -223,13 +223,13 @@ struct Composer: View { init( viewModel: ComposerViewModelType.Context, wysiwygViewModel: WysiwygComposerViewModel, - userSuggestionSharedContext: UserSuggestionViewModelType.Context, + completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context, resizeAnimationDuration: Double, sendMessageAction: @escaping (WysiwygComposerContent) -> Void, showSendMediaActions: @escaping () -> Void) { self.viewModel = viewModel self.wysiwygViewModel = wysiwygViewModel - self.userSuggestionSharedContext = userSuggestionSharedContext + self.completionSuggestionSharedContext = completionSuggestionSharedContext self.resizeAnimationDuration = resizeAnimationDuration self.sendMessageAction = sendMessageAction self.showSendMediaActions = showSendMediaActions @@ -256,7 +256,7 @@ struct Composer: View { } } if wysiwygViewModel.maximised { - UserSuggestionList(viewModel: userSuggestionSharedContext, showBackgroundShadow: false) + CompletionSuggestionList(viewModel: completionSuggestionSharedContext, showBackgroundShadow: false) } } .frame(height: composerHeight) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift deleted file mode 100644 index ba1bc75ca..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ /dev/null @@ -1,79 +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 - -@objc -protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) - func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?) - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) -} - -@objcMembers -final class UserSuggestionCoordinatorBridge: NSObject { - private var _userSuggestionCoordinator: Any? - fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator { - _userSuggestionCoordinator as! UserSuggestionCoordinator - } - - weak var delegate: UserSuggestionCoordinatorBridgeDelegate? - - init(mediaManager: MXMediaManager, room: MXRoom, userID: String) { - let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID) - let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) - _userSuggestionCoordinator = userSuggestionCoordinator - - super.init() - - userSuggestionCoordinator.delegate = self - } - - func processTextMessage(_ textMessage: String) { - userSuggestionCoordinator.processTextMessage(textMessage) - } - - func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) { - userSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern) - } - - func toPresentable() -> UIViewController? { - userSuggestionCoordinator.toPresentable() - } - - func sharedContext() -> UserSuggestionViewModelContextWrapper { - userSuggestionCoordinator.sharedContext() - } -} - -extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) - } - - func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) - } - - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) - } - - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { - delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) - } -} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift deleted file mode 100644 index 68d573bdf..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ /dev/null @@ -1,77 +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 UserSuggestionViewModelType = StateStoreViewModel - -class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { - // MARK: - Properties - - // MARK: Private - - private let userSuggestionService: UserSuggestionServiceProtocol - - // MARK: Public - - var sharedContext: UserSuggestionViewModelType.Context { - return self.context - } - - var completion: ((UserSuggestionViewModelResult) -> Void)? - - // MARK: - Setup - - init(userSuggestionService: UserSuggestionServiceProtocol) { - self.userSuggestionService = userSuggestionService - - let items = userSuggestionService.items.value.map { suggestionItem in - switch suggestionItem { - case .command(let commandSuggestionItem): - return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) - case .user(let userSuggestionItem): - return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, - avatar: userSuggestionItem, - displayName: userSuggestionItem.displayName) - } - } - - super.init(initialViewState: UserSuggestionViewState(items: items)) - - userSuggestionService.items.sink { [weak self] items in - self?.state.items = items.map { item in - switch item { - case .command(let commandSuggestionItem): - return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) - case .user(let userSuggestionItem): - return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, - avatar: userSuggestionItem, - displayName: userSuggestionItem.displayName) - } - } - }.store(in: &cancellables) - } - - // MARK: - Public - - override func process(viewAction: UserSuggestionViewAction) { - switch viewAction { - case .selectedItem(let item): - completion?(.selectedItemWithIdentifier(item.id)) - } - } -} From 56ad4a03d3a548c3afd26182ccecff182bb95f96 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 15:32:30 +0200 Subject: [PATCH 07/52] Display additional command content in suggestion list --- .../CompletionSuggestionModels.swift | 4 +-- .../CompletionSuggestionScreenState.swift | 16 +++++++++--- .../CompletionSuggestionViewModel.swift | 12 +++++++-- .../CompletionSuggestionCoordinator.swift | 26 +++++++++++++------ .../Service/CompletionSuggestionService.swift | 8 ++++-- .../CompletionSuggestionServiceProtocol.swift | 2 ++ .../View/CompletionSuggestionListItem.swift | 26 ++++++++++++++----- 7 files changed, 70 insertions(+), 24 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift index 91fc4ffeb..8476834b9 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift @@ -25,12 +25,12 @@ enum CompletionSuggestionViewModelResult { } enum CompletionSuggestionViewStateItem: Identifiable { - case command(name: String) + case command(name: String, parametersFormat: String, description: String) case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) var id: String { switch self { - case .command(let name): + case .command(let name, _, _): return name case .user(let id, _, _): return id diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 1427c3f3f..81d6e2088 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -64,10 +64,18 @@ extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ - CommandsProviderCommand(name: "/ban"), - CommandsProviderCommand(name: "/invite"), - CommandsProviderCommand(name: "/join"), - CommandsProviderCommand(name: "/me") + CommandsProviderCommand(name: "/ban", + parametersFormat: " [reason]", + description: "Bans user with given id"), + CommandsProviderCommand(name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + CommandsProviderCommand(name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + CommandsProviderCommand(name: "/me", + parametersFormat: "", + description: "Displays action") ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift index 01c881970..0c9c0215c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -42,7 +42,11 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi let items = completionSuggestionService.items.value.map { suggestionItem in switch suggestionItem { case .command(let completionSuggestionCommandItem): - return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + return CompletionSuggestionViewStateItem.command( + name: completionSuggestionCommandItem.name, + parametersFormat: completionSuggestionCommandItem.parametersFormat, + description: completionSuggestionCommandItem.description + ) case .user(let completionSuggestionUserItem): return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, avatar: completionSuggestionUserItem, @@ -56,7 +60,11 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi self?.state.items = items.map { item in switch item { case .command(let completionSuggestionCommandItem): - return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + return CompletionSuggestionViewStateItem.command( + name: completionSuggestionCommandItem.name, + parametersFormat: completionSuggestionCommandItem.parametersFormat, + description: completionSuggestionCommandItem.description + ) case .user(let completionSuggestionUserItem): return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, avatar: completionSuggestionUserItem, diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 8da2356fd..f2dab2dab 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -95,8 +95,8 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) - } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { - self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.completionSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0.name == identifier }).first { + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.name, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } @@ -207,7 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let room: MXRoom private let userID: String - var commands: [String] = [] + var commands: [(name: String, parametersFormat: String, description: String)] = [] init(room: MXRoom, userID: String) { self.room = room @@ -221,13 +221,23 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { self.commands = [ - "/ban", - "/invite", - "/join", - "/me" + (name: "/ban", + parametersFormat: " [reason]", + description: "Bans user with given id"), + (name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + (name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + (name: "/me", + parametersFormat: "", + description: "Displays action") ] // TODO: get real data - commands(self.commands.map { CommandsProviderCommand(name: $0) }) + commands(self.commands.map { CommandsProviderCommand(name: $0.name, + parametersFormat: $0.parametersFormat, + description: $0.description) }) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 0353b63d4..5adf4f3c5 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -25,7 +25,9 @@ struct RoomMembersProviderMember { } struct CommandsProviderCommand { - var name: String + let name: String + let parametersFormat: String + let description: String } class CompletionSuggestionUserID: NSObject { @@ -50,6 +52,8 @@ struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol { let name: String + let parametersFormat: String + let description: String } class CompletionSuggestionService: CompletionSuggestionServiceProtocol { @@ -165,7 +169,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { guard let self else { return } self.suggestionItems = commands.map { command in - CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name, parametersFormat: command.parametersFormat, description: command.description)) } self.items.send(self.suggestionItems.filter { item in diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift index 4586e1294..3930c59d1 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift @@ -26,6 +26,8 @@ protocol CompletionSuggestionUserItemProtocol: Avatarable { protocol CompletionSuggestionCommandItemProtocol { var name: String { get } + var parametersFormat: String { get } + var description: String { get } } enum CompletionSuggestionItem { diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index c30ec5d89..95f81fb75 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -30,12 +30,26 @@ struct CompletionSuggestionListItem: View { var body: some View { HStack { switch content { - case .command(let name): - Text(name) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "nameText") - .lineLimit(1) + case .command(let name, let parametersFormat, let description): + VStack(alignment: .leading) { + HStack { + Text(name) + .font(theme.fonts.body.bold()) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "nameText") + .lineLimit(1) + Text(parametersFormat) + .font(theme.fonts.body.italic()) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "parametersFormatText") + .lineLimit(1) + } + Text(description) + .font(theme.fonts.body) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "descriptionText") + .lineLimit(1) + } case .user(let userId, let avatar, let displayName): if let avatar = avatar { AvatarImage(avatarData: avatar, size: .medium) From 787967a8e47e4d3d5c5d98e4c520770df9eba7cc Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 12:10:03 +0200 Subject: [PATCH 08/52] Rework `MXKSlashCommands` to a more Swift-friendly form and use it in suggestion module --- Riot/Modules/MatrixKit/MatrixKit.h | 2 - .../MatrixKit/Models/Room/MXKRoomDataSource.m | 4 +- .../MatrixKit/Models/Room/MXKSlashCommands.h | 34 ------ .../MatrixKit/Models/Room/MXKSlashCommands.m | 30 ------ .../Models/Room/MXKSlashCommands.swift | 101 ++++++++++++++++++ .../Room/DataSources/RoomDataSource.swift | 2 +- Riot/Modules/Room/MXKRoomViewController.m | 43 ++++---- Riot/Modules/Room/RoomViewController.m | 6 +- .../CompletionSuggestionCoordinator.swift | 77 +++++++++---- .../View/CompletionSuggestionListItem.swift | 1 - 10 files changed, 185 insertions(+), 115 deletions(-) delete mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h delete mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m create mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift diff --git a/Riot/Modules/MatrixKit/MatrixKit.h b/Riot/Modules/MatrixKit/MatrixKit.h index 2bb02223b..ce6ea5f1e 100644 --- a/Riot/Modules/MatrixKit/MatrixKit.h +++ b/Riot/Modules/MatrixKit/MatrixKit.h @@ -145,5 +145,3 @@ #import "MXKCountryPickerViewController.h" #import "MXKLanguagePickerViewController.h" - -#import "MXKSlashCommands.h" diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 0998122ae..a69f504cc 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -31,8 +31,6 @@ #import "MXKAppSettings.h" -#import "MXKSlashCommands.h" - #import "GeneratedInterface-Swift.h" const BOOL USE_THREAD_TIMELINE = YES; @@ -316,7 +314,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { _filterMessagesWithURL = NO; - emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", kMXKSlashCmdEmote]; + emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]; // Set default data and view classes // Cell data diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h deleted file mode 100644 index ef9c71783..000000000 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright 2018 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; - -/** - Slash commands used to perform actions from a room. - */ - -FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeDisplayName; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdEmote; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdJoinRoom; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdPartRoom; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdInviteUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdKickUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdBanUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdUnbanUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdSetUserPowerLevel; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdResetUserPowerLevel; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeRoomTopic; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdDiscardSession; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m deleted file mode 100644 index e9d483d9b..000000000 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - Copyright 2018 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 "MXKSlashCommands.h" - -NSString *const kMXKSlashCmdChangeDisplayName = @"/nick"; -NSString *const kMXKSlashCmdEmote = @"/me"; -NSString *const kMXKSlashCmdJoinRoom = @"/join"; -NSString *const kMXKSlashCmdPartRoom = @"/part"; -NSString *const kMXKSlashCmdInviteUser = @"/invite"; -NSString *const kMXKSlashCmdKickUser = @"/kick"; -NSString *const kMXKSlashCmdBanUser = @"/ban"; -NSString *const kMXKSlashCmdUnbanUser = @"/unban"; -NSString *const kMXKSlashCmdSetUserPowerLevel = @"/op"; -NSString *const kMXKSlashCmdResetUserPowerLevel = @"/deop"; -NSString *const kMXKSlashCmdChangeRoomTopic = @"/topic"; -NSString *const kMXKSlashCmdDiscardSession = @"/discardsession"; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift new file mode 100644 index 000000000..faae85e94 --- /dev/null +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -0,0 +1,101 @@ +// +// 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. +// + +@objc final class MXKSlashCommandsHelper: NSObject { + @objc static func commandNameFor(_ slashCommand: MXKSlashCommand) -> String { + slashCommand.cmd + } + + @objc static func commandUsageFor(_ slashCommand: MXKSlashCommand) -> String { + "Usage: \(slashCommand.cmd) \(slashCommand.parametersFormat)" + } +} + +@objc enum MXKSlashCommand: Int, CaseIterable { + case changeDisplayName + case emote + case joinRoom + case partRoom + case inviteUser + case kickUser + case banUser + case unbanUser + case setUserPowerLevel + case resetUserPowerLevel + case changeRoomTopic + case discardSession + + var cmd: String { + switch self { + case .changeDisplayName: + return "/nick" + case .emote: + return "/me" + case .joinRoom: + return "/join" + case .partRoom: + return "/part" + case .inviteUser: + return "/invite" + case .kickUser: + return "/kick" + case .banUser: + return "/ban" + case .unbanUser: + return "/unban" + case .setUserPowerLevel: + return "/op" + case .resetUserPowerLevel: + return "/deop" + case .changeRoomTopic: + return "/topic" + case .discardSession: + return "/discardsession" + } + } + + // Note: not localized for consistancy, as commands are in english + // also translating these parameters could lead to inconsistency in + // the UI in case of languages with otherlength translation. + var parametersFormat: String { + switch self { + case .changeDisplayName: + return "" + case .emote: + return "" + case .joinRoom: + return "" + case .partRoom: + return "[]" + case .inviteUser: + return "" + case .kickUser: + return " []" + case .banUser: + return " []" + case .unbanUser: + return "" + case .setUserPowerLevel: + return " " + case .resetUserPowerLevel: + return "" + case .changeRoomTopic: + return "" + case .discardSession: + return "" + } + } +} diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.swift b/Riot/Modules/Room/DataSources/RoomDataSource.swift index 281a7a046..89cbabe42 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.swift +++ b/Riot/Modules/Room/DataSources/RoomDataSource.swift @@ -19,7 +19,7 @@ import Foundation extension RoomDataSource { // MARK: - Private Constants private enum Constants { - static let emoteMessageSlashCommandPrefix = String(format: "%@ ", kMXKSlashCmdEmote) + static let emoteMessageSlashCommandPrefix = String(format: "%@ ", MXKSlashCommand.emote.cmd) } // MARK: - NSAttributedString Sending diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 7c014e018..2e55c4771 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -39,7 +39,6 @@ #import "MXKEncryptionKeysImportView.h" #import "NSBundle+MatrixKit.h" -#import "MXKSlashCommands.h" #import "MXKSwiftHeader.h" #import "MXKPreviewViewController.h" @@ -1284,8 +1283,14 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; // TODO: display an alert with the cmd usage in case of error or unrecognized cmd. NSString *cmdUsage; + + NSString* kMXKSlashCmdChangeDisplayName = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeDisplayName]; + NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom]; + NSString* kMXKSlashCmdPartRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandPartRoom]; + NSString* kMXKSlashCmdChangeRoomTopic = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeRoomTopic]; + - if ([cmd isEqualToString:kMXKSlashCmdEmote]) + if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]) { // send message as an emote [self sendTextMessage:string]; @@ -1320,7 +1325,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /nick "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName]; } } else if ([string hasPrefix:kMXKSlashCmdJoinRoom]) @@ -1355,7 +1360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /join "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom]; } } else if ([string hasPrefix:kMXKSlashCmdPartRoom]) @@ -1413,7 +1418,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /part []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom]; } } else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic]) @@ -1445,10 +1450,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /topic "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeRoomTopic]; } } - else if ([string hasPrefix:kMXKSlashCmdDiscardSession]) + else if ([string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandDiscardSession]]) { [roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{ MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session"); @@ -1470,7 +1475,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; userId = nil; } - if ([cmd isEqualToString:kMXKSlashCmdInviteUser]) + if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]]) { if (userId) { @@ -1489,10 +1494,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /invite "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandInviteUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdKickUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandKickUser]]) { if (userId) { @@ -1524,10 +1529,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /kick []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandKickUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdBanUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandBanUser]]) { if (userId) { @@ -1559,10 +1564,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /ban []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandBanUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdUnbanUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandUnbanUser]]) { if (userId) { @@ -1581,10 +1586,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /unban "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandUnbanUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdSetUserPowerLevel]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandSetUserPowerLevel]]) { // Retrieve power level NSString *powerLevel = nil; @@ -1617,10 +1622,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /op "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandSetUserPowerLevel]; } } - else if ([cmd isEqualToString:kMXKSlashCmdResetUserPowerLevel]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandResetUserPowerLevel]]) { if (userId) { @@ -1639,7 +1644,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /deop "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel]; } } else diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 38f71f8d1..274e7d437 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1281,6 +1281,8 @@ static CGSize kThreadListBarButtonItemImageSize; - (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string { // Override the default behavior for `/join` command in order to open automatically the joined room + + NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom]; if ([string hasPrefix:kMXKSlashCmdJoinRoom]) { @@ -1317,7 +1319,7 @@ static CGSize kThreadListBarButtonItemImageSize; else { // Display cmd usage in text input as placeholder - self.inputToolbarView.placeholder = @"Usage: /join "; + self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom]; } return YES; } @@ -5237,7 +5239,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (readyToSend) { BOOL isMessageAHandledCommand = NO; // "/me" command is supported with Pills in RoomDataSource. - if (![attributedTextMessage.string hasPrefix:kMXKSlashCmdEmote]) + if (![attributedTextMessage.string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]) { // Other commands currently work with identifiers (e.g. ban, invite, op, etc). NSString *message; diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index f2dab2dab..8669e812f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -95,8 +95,8 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) - } else if let command = self.commandProvider.commands.filter({ $0.name == identifier }).first { - self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.name, textTrigger: self.completionSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0.cmd == identifier }).first { + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.cmd, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } @@ -207,7 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let room: MXRoom private let userID: String - var commands: [(name: String, parametersFormat: String, description: String)] = [] + var commands = MXKSlashCommand.allCases init(room: MXRoom, userID: String) { self.room = room @@ -216,28 +216,59 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } func updateWithPowerLevels() { - // TODO: filter commands in terms of user power level ? + room.state { [weak self] state in + guard let self, let powerLevels = state?.powerLevels else { return } + + // Note: for now only filter out `/op` and `/deop` (same as Element-Web), + // but we could use power level for ban/invite/etc to filter further. + let adminOnlyCommands: [MXKSlashCommand] = [.setUserPowerLevel, .resetUserPowerLevel] + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) + + if RoomPowerLevel(rawValue: userPowerLevel) != .admin { + self.commands = self.commands.filter { + !adminOnlyCommands.contains($0) + } + } + } } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - self.commands = [ - (name: "/ban", - parametersFormat: " [reason]", - description: "Bans user with given id"), - (name: "/invite", - parametersFormat: "", - description: "Invites user with given id to current room"), - (name: "/join", - parametersFormat: "", - description: "Joins room with given address"), - (name: "/me", - parametersFormat: "", - description: "Displays action") - ] - - // TODO: get real data - commands(self.commands.map { CommandsProviderCommand(name: $0.name, - parametersFormat: $0.parametersFormat, - description: $0.description) }) + commands(self.commands.map { CommandsProviderCommand( + name: $0.cmd, + parametersFormat: $0.parametersFormat, + description: $0.description + )}) + } +} + +private extension MXKSlashCommand { + // TODO: L10N + var description: String { + switch self { + case .changeDisplayName: + return "Changes your display nickname" + case .emote: + return "Displays action" + case .joinRoom: + return "Joins room with given address" + case .partRoom: + return "Leave room" + case .inviteUser: + return "Invites user with given id to current room" + case .kickUser: + return "Removes user with given id from this room" + case .banUser: + return "Bans user with given id" + case .unbanUser: + return "Unbans user with given id" + case .setUserPowerLevel: + return "Define the power level of a user" + case .resetUserPowerLevel: + return "Deops user with given id" + case .changeRoomTopic: + return "Sets the room topic" + case .discardSession: + return "Forces the current outbound group session in an encrypted room to be discarded" + } } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index 95f81fb75..4a1616189 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -48,7 +48,6 @@ struct CompletionSuggestionListItem: View { .font(theme.fonts.body) .foregroundColor(theme.colors.tertiaryContent) .accessibility(identifier: "descriptionText") - .lineLimit(1) } case .user(let userId, let avatar, let displayName): if let avatar = avatar { From 4b0c47c5dd5afe12860eae2965791eb5e565e357 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 14:12:55 +0200 Subject: [PATCH 09/52] Display all commands when a single slash is entered --- .../Service/CompletionSuggestionService.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 5adf4f3c5..b16efc137 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -162,21 +162,29 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { }) } case "/": - // TODO: send all commands if only text is "/" partialName.removeFirst() commandProvider.fetchCommands { [weak self] commands in guard let self else { return } self.suggestionItems = commands.map { command in - CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name, parametersFormat: command.parametersFormat, description: command.description)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem( + name: command.name, + parametersFormat: command.parametersFormat, + description: command.description + )) } - self.items.send(self.suggestionItems.filter { item in - guard case let .command(commandSuggestion) = item else { return false } + if partialName.isEmpty { + // A single `/` will display all available commands. + self.items.send(self.suggestionItems) + } else { + self.items.send(self.suggestionItems.filter { item in + guard case let .command(commandSuggestion) = item else { return false } - return commandSuggestion.name.lowercased().contains(partialName.lowercased()) - }) + return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + }) + } } default: return From 616238f13b59cb22c30d3535d40d10c138b6a5b7 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 15:43:20 +0200 Subject: [PATCH 10/52] Rework `CompletionSuggestionService` text trigger --- .../Service/CompletionSuggestionService.swift | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index b16efc137..86a99370f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -65,7 +65,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { private let commandProvider: CommandsProviderProtocol private var suggestionItems: [CompletionSuggestionItem] = [] - private let currentTextTriggerSubject = CurrentValueSubject(nil) + private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public @@ -73,7 +73,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([]) var currentTextTrigger: String? { - currentTextTriggerSubject.value + currentTextTriggerSubject.value?.asString() } // MARK: - Setup @@ -88,11 +88,11 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { currentTextTriggerSubject .debounce(for: 0.5, scheduler: RunLoop.main) .removeDuplicates() - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) } .store(in: &cancellables) } else { currentTextTriggerSubject - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) } .store(in: &cancellables) } } @@ -101,16 +101,14 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { func processTextMessage(_ textMessage: String?) { guard let textMessage = textMessage, - textMessage.count > 0, - let lastComponent = textMessage.components(separatedBy: .whitespaces).last, - lastComponent.prefix(while: { $0 == "@" || $0 == "/" }).count == 1 // Partial username should start with one and only one "@" character + let textTrigger = textMessage.currentTextTrigger else { items.send([]) currentTextTriggerSubject.send(nil) return } - currentTextTriggerSubject.send(lastComponent) + currentTextTriggerSubject.send(textTrigger) } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { @@ -122,27 +120,23 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { switch suggestionPattern.key { case .at: - currentTextTriggerSubject.send("@" + suggestionPattern.text) + currentTextTriggerSubject.send(TextTrigger(key: .at, text: suggestionPattern.text)) case .hash: // No room suggestion support yet items.send([]) currentTextTriggerSubject.send(nil) case .slash: - currentTextTriggerSubject.send("/" + suggestionPattern.text) + currentTextTriggerSubject.send(TextTrigger(key: .slash, text: suggestionPattern.text)) } } // MARK: - Private - private func fetchAndFilterMembersForTextTrigger(_ textTrigger: String?) { - guard var partialName = textTrigger else { - return - } - - switch partialName.first { - case "@": - partialName.removeFirst() // remove the '@' prefix + private func fetchAndFilterSuggestionsForTextTrigger(_ textTrigger: TextTrigger?) { + guard let textTrigger else { return } + switch textTrigger.key { + case .at: roomMemberProvider.fetchMembers { [weak self] members in guard let self = self else { return @@ -155,15 +149,13 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { self.items.send(self.suggestionItems.filter { item in guard case let .user(completionSuggestionUserItem) = item else { return false } - let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased()) + let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(textTrigger.text.lowercased()) + let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(textTrigger.text.lowercased()) return (containedInUsername || containedInDisplayName) }) } - case "/": - partialName.removeFirst() - + case .slash: commandProvider.fetchCommands { [weak self] commands in guard let self else { return } @@ -175,19 +167,17 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { )) } - if partialName.isEmpty { + if textTrigger.text.isEmpty { // A single `/` will display all available commands. self.items.send(self.suggestionItems) } else { self.items.send(self.suggestionItems.filter { item in guard case let .command(commandSuggestion) = item else { return false } - return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + return commandSuggestion.name.lowercased().contains(textTrigger.text.lowercased()) }) } } - default: - return } } } @@ -199,3 +189,34 @@ extension Array where Element == RoomMembersProviderMember { return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")] } } + +private enum SuggestionKey: Character { + case at = "@" + case slash = "/" +} + +private struct TextTrigger: Equatable { + let key: SuggestionKey + let text: String + + func asString() -> String { + return String(key.rawValue) + text + } +} + +private extension String { + // Returns current completion suggestion for a text message, if any. + var currentTextTrigger: TextTrigger? { + let components = self.components(separatedBy: .whitespaces) + guard var lastComponent = components.last, + lastComponent.count > 0, + let suggestionKey = SuggestionKey(rawValue: lastComponent.removeFirst()), + // If a second character exists and is the same as the key it shouldn't trigger. + lastComponent.first != suggestionKey.rawValue, + // Slash commands should be displayed only if there is a single component + !(suggestionKey == .slash && components.count > 1) + else { return nil } + + return TextTrigger(key: suggestionKey, text: lastComponent) + } +} From a3f7d0433a357b46858498e10ca525e7b2f4ed5d Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:01:17 +0200 Subject: [PATCH 11/52] Re-enable unit tests and fix a few lint warnings --- .../CompletionSuggestionScreenState.swift | 2 +- .../CompletionSuggestionViewModel.swift | 2 +- .../CompletionSuggestionCoordinator.swift | 9 ++------- .../Service/CompletionSuggestionService.swift | 4 ++-- .../CompletionSuggestionServiceTests.swift | 19 ++++++++++++++----- .../View/CompletionSuggestionList.swift | 11 +++-------- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 81d6e2088..b78c25575 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -65,7 +65,7 @@ extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban", - parametersFormat: " [reason]", + parametersFormat: " []", description: "Bans user with given id"), CommandsProviderCommand(name: "/invite", parametersFormat: "", diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift index 0c9c0215c..53d2c6975 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -29,7 +29,7 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi // MARK: Public var sharedContext: CompletionSuggestionViewModelType.Context { - return self.context + context } var completion: ((CompletionSuggestionViewModelResult) -> Void)? diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 8669e812f..102994636 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -103,8 +103,7 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { completionSuggestionService.items.sink { [weak self] _ in guard let self = self else { return } - self.delegate?.completionSuggestionCoordinator(self, - didUpdateViewHeight: self.calculateViewHeight()) + self.delegate?.completionSuggestionCoordinator(self, didUpdateViewHeight: self.calculateViewHeight()) }.store(in: &cancellables) } @@ -233,11 +232,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - commands(self.commands.map { CommandsProviderCommand( - name: $0.cmd, - parametersFormat: $0.parametersFormat, - description: $0.description - )}) + commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description) }) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 86a99370f..09b229ec4 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -200,14 +200,14 @@ private struct TextTrigger: Equatable { let text: String func asString() -> String { - return String(key.rawValue) + text + String(key.rawValue) + text } } private extension String { // Returns current completion suggestion for a text message, if any. var currentTextTrigger: TextTrigger? { - let components = self.components(separatedBy: .whitespaces) + let components = components(separatedBy: .whitespaces) guard var lastComponent = components.last, lastComponent.count > 0, let suggestionKey = SuggestionKey(rawValue: lastComponent.removeFirst()), diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 636ba3355..18283bfb3 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -143,11 +143,20 @@ extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { extension CompletionSuggestionServiceTests: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - let commandList = ["/ban", "/invite", "/join", "/me"] - - commands(commandList.map { command in - CommandsProviderCommand(name: command) - }) + commands([ + CommandsProviderCommand(name: "/ban", + parametersFormat: " []", + description: "Bans user with given id"), + CommandsProviderCommand(name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + CommandsProviderCommand(name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + CommandsProviderCommand(name: "/me", + parametersFormat: "", + description: "Displays action") + ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift index 02aef8a1f..cf8e34e02 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift @@ -30,7 +30,7 @@ struct CompletionSuggestionList: View { to the list items in order to be as close as possible as the `UITableView` display. */ - @available (iOS 16.0, *) + @available(iOS 16.0, *) static let collectionViewPaddingCorrection: CGFloat = -5.0 } @@ -44,19 +44,14 @@ struct CompletionSuggestionList: View { // MARK: Public @ObservedObject var viewModel: CompletionSuggestionViewModel.Context - var showBackgroundShadow: Bool = true + var showBackgroundShadow = true var body: some View { if viewModel.viewState.items.isEmpty { EmptyView() } else { ZStack { - CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( - id: "Prototype", - avatar: AvatarInput(mxContentUri: "", - matrixItemId: "", - displayName: "Prototype"), - displayName: "Prototype")) + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user(id: "Prototype", avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"), displayName: "Prototype")) .background(ViewFrameReader(frame: $prototypeListItemFrame)) .hidden() if showBackgroundShadow { From 0171e64638beddfce7e2993798d7a0a2eb40ebfe Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:43:36 +0200 Subject: [PATCH 12/52] Move room admin condition to be usable in UnitTests and add tests --- .../CompletionSuggestionScreenState.swift | 22 +++- .../CompletionSuggestionCoordinator.swift | 24 ++-- .../Service/CompletionSuggestionService.swift | 11 +- .../CompletionSuggestionServiceTests.swift | 105 +++++++++++++++++- .../CompletionSuggestionListWithInput.swift | 4 +- 5 files changed, 144 insertions(+), 22 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index b78c25575..5bdd72088 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -62,20 +62,34 @@ extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { } extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { + var isRoomAdmin: Bool { false } + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban", parametersFormat: " []", - description: "Bans user with given id"), + description: "Bans user with given id", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/invite", parametersFormat: "", - description: "Invites user with given id to current room"), + description: "Invites user with given id to current room", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/join", parametersFormat: "", - description: "Joins room with given address"), + description: "Joins room with given address", + requiresAdminPowerLevel: false), + CommandsProviderCommand(name: "/op", + parametersFormat: " ", + description: "Define the power level of a user", + requiresAdminPowerLevel: true), + CommandsProviderCommand(name: "/deop", + parametersFormat: "", + description: "Deops user with given id", + requiresAdminPowerLevel: true), CommandsProviderCommand(name: "/me", parametersFormat: "", - description: "Displays action") + description: "Displays action", + requiresAdminPowerLevel: false) ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 102994636..6c868ce4c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -207,6 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let userID: String var commands = MXKSlashCommand.allCases + var isRoomAdmin = false init(room: MXRoom, userID: String) { self.room = room @@ -218,21 +219,13 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr room.state { [weak self] state in guard let self, let powerLevels = state?.powerLevels else { return } - // Note: for now only filter out `/op` and `/deop` (same as Element-Web), - // but we could use power level for ban/invite/etc to filter further. - let adminOnlyCommands: [MXKSlashCommand] = [.setUserPowerLevel, .resetUserPowerLevel] let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) - - if RoomPowerLevel(rawValue: userPowerLevel) != .admin { - self.commands = self.commands.filter { - !adminOnlyCommands.contains($0) - } - } + isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin } } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description) }) + commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description, requiresAdminPowerLevel: $0.requiresAdminPowerLevel) }) } } @@ -266,4 +259,15 @@ private extension MXKSlashCommand { return "Forces the current outbound group session in an encrypted room to be discarded" } } + + // Note: for now only filter out `/op` and `/deop` (same as Element-Web), + // but we could use power level for ban/invite/etc to filter further. + var requiresAdminPowerLevel: Bool { + switch self { + case .setUserPowerLevel, .resetUserPowerLevel: + return true + default: + return false + } + } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 09b229ec4..5ded36c2c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -28,6 +28,7 @@ struct CommandsProviderCommand { let name: String let parametersFormat: String let description: String + let requiresAdminPowerLevel: Bool } class CompletionSuggestionUserID: NSObject { @@ -41,6 +42,7 @@ protocol RoomMembersProviderProtocol { } protocol CommandsProviderProtocol { + var isRoomAdmin: Bool { get } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) } @@ -159,7 +161,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { commandProvider.fetchCommands { [weak self] commands in guard let self else { return } - self.suggestionItems = commands.map { command in + self.suggestionItems = commands.filtered(isRoomAdmin: self.commandProvider.isRoomAdmin).map { command in CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem( name: command.name, parametersFormat: command.parametersFormat, @@ -190,6 +192,13 @@ extension Array where Element == RoomMembersProviderMember { } } +extension Array where Element == CommandsProviderCommand { + func filtered(isRoomAdmin: Bool) -> Self { + guard !isRoomAdmin else { return self } + return filter { !$0.requiresAdminPowerLevel } + } +} + private enum SuggestionKey: Character { case at = "@" case slash = "/" diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 18283bfb3..90542868d 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -22,14 +22,18 @@ import XCTest class CompletionSuggestionServiceTests: XCTestCase { var service: CompletionSuggestionService! var canMentionRoom = false + var isRoomAdmin = false override func setUp() { service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self, shouldDebounce: false) canMentionRoom = false + isRoomAdmin = false } - + + // MARK: - User suggestions + func testAlice() { service.processTextMessage("@Al") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") @@ -128,6 +132,85 @@ class CompletionSuggestionServiceTests: XCTestCase { // Then the completion for a room mention should be shown. XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room) } + + // MARK: - Command suggestions + + func testJoin() { + service.processTextMessage("/jo") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/joi") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/join") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/oin") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + } + + func testInvite() { + service.processTextMessage("/inv") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + + service.processTextMessage("/invite") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + + service.processTextMessage("/vite") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + } + + func testMultipleResults() { + service.processTextMessage("/in") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/invite", "/join"] + ) + } + + func testDoubleSlashDontTrigger() { + service.processTextMessage("//") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testNonLeadingSlashCommandDontTrigger() { + service.processTextMessage("test /joi") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testAdminCommandsAreNotAvailable() { + isRoomAdmin = false + + service.processTextMessage("/op") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testAdminCommandsAreAvailable() { + isRoomAdmin = true + + service.processTextMessage("/op") + XCTAssertEqual(service.items.value.compactMap { $0.asCommand?.name }, ["/op", "/deop"]) + } + + func testDisplayAllCommandsAsStandardUser() { + isRoomAdmin = false + + service.processTextMessage("/") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/ban", "/invite", "/join", "/me"] + ) + } + + func testDisplayAllCommandsAsAdmin() { + isRoomAdmin = true + + service.processTextMessage("/") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/ban", "/invite", "/join", "/op", "/deop", "/me"] + ) + } } extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { @@ -146,16 +229,28 @@ extension CompletionSuggestionServiceTests: CommandsProviderProtocol { commands([ CommandsProviderCommand(name: "/ban", parametersFormat: " []", - description: "Bans user with given id"), + description: "Bans user with given id", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/invite", parametersFormat: "", - description: "Invites user with given id to current room"), + description: "Invites user with given id to current room", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/join", parametersFormat: "", - description: "Joins room with given address"), + description: "Joins room with given address", + requiresAdminPowerLevel: false), + CommandsProviderCommand(name: "/op", + parametersFormat: " ", + description: "Define the power level of a user", + requiresAdminPowerLevel: true), + CommandsProviderCommand(name: "/deop", + parametersFormat: "", + description: "Deops user with given id", + requiresAdminPowerLevel: true), CommandsProviderCommand(name: "/me", parametersFormat: "", - description: "Displays action") + description: "Displays action", + requiresAdminPowerLevel: false) ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift index 0b1dd8e8a..223b4fbc6 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift @@ -34,13 +34,13 @@ struct CompletionSuggestionListWithInput: View { var body: some View { VStack(spacing: 0.0) { CompletionSuggestionList(viewModel: viewModel.listViewModel.context) - TextField("Search for user", text: $inputText) + TextField("Search for user/command", text: $inputText) .background(Color.white) .onChange(of: inputText, perform: viewModel.callback) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding([.leading, .trailing]) .onAppear { - inputText = "@-" // Make the list show all available mock results + inputText = "@-" // Make the list show all available user mock results } } } From f6e7b9710c2715224543a51862219c4c3dc09061 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:50:44 +0200 Subject: [PATCH 13/52] Add changelog --- changelog.d/7493.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7493.feature diff --git a/changelog.d/7493.feature b/changelog.d/7493.feature new file mode 100644 index 000000000..075a7f6a2 --- /dev/null +++ b/changelog.d/7493.feature @@ -0,0 +1 @@ +Add composer suggestions for slash commands From 837bee610f15d2c0313b615af2b52906c74f8998 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 17:09:02 +0200 Subject: [PATCH 14/52] L10N --- Riot/Assets/en.lproj/Vector.strings | 14 ++++++ Riot/Generated/Strings.swift | 48 +++++++++++++++++++ .../CompletionSuggestionCoordinator.swift | 25 +++++----- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1099f168..38cded4a3 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -614,6 +614,20 @@ Tap the + to start adding people."; "room_join_group_call" = "Join"; "room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; +// Room commands descriptions +"room_command_change_display_name_description" = "Changes your display nickname"; +"room_command_emote_description" = "Displays action"; +"room_command_join_room_description" = "Joins room with given address"; +"room_command_part_room_description" = "Leave room"; +"room_command_invite_user_description" = "Invites user with given id to current room"; +"room_command_kick_user_description" = "Removes user with given id from this room"; +"room_command_ban_user_description" = "Bans user with given id"; +"room_command_unban_user_description" = "Unbans user with given id"; +"room_command_set_user_power_level_description" = "Define the power level of a user"; +"room_command_reset_user_power_level_description" = "Deops user with given id"; +"room_command_change_room_topic_description" = "Sets the room topic"; +"room_command_discard_session_description" = "Forces the current outbound group session in an encrypted room to be discarded"; + // MARK: Threads "room_thread_title" = "Thread"; "thread_copy_link_to_thread" = "Copy link to thread"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 0cdd03d2d..e48764f1d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5211,6 +5211,54 @@ public class VectorL10n: NSObject { public static var roomAvatarViewAccessibilityLabel: String { return VectorL10n.tr("Vector", "room_avatar_view_accessibility_label") } + /// Bans user with given id + public static var roomCommandBanUserDescription: String { + return VectorL10n.tr("Vector", "room_command_ban_user_description") + } + /// Changes your display nickname + public static var roomCommandChangeDisplayNameDescription: String { + return VectorL10n.tr("Vector", "room_command_change_display_name_description") + } + /// Sets the room topic + public static var roomCommandChangeRoomTopicDescription: String { + return VectorL10n.tr("Vector", "room_command_change_room_topic_description") + } + /// Forces the current outbound group session in an encrypted room to be discarded + public static var roomCommandDiscardSessionDescription: String { + return VectorL10n.tr("Vector", "room_command_discard_session_description") + } + /// Displays action + public static var roomCommandEmoteDescription: String { + return VectorL10n.tr("Vector", "room_command_emote_description") + } + /// Invites user with given id to current room + public static var roomCommandInviteUserDescription: String { + return VectorL10n.tr("Vector", "room_command_invite_user_description") + } + /// Joins room with given address + public static var roomCommandJoinRoomDescription: String { + return VectorL10n.tr("Vector", "room_command_join_room_description") + } + /// Removes user with given id from this room + public static var roomCommandKickUserDescription: String { + return VectorL10n.tr("Vector", "room_command_kick_user_description") + } + /// Leave room + public static var roomCommandPartRoomDescription: String { + return VectorL10n.tr("Vector", "room_command_part_room_description") + } + /// Deops user with given id + public static var roomCommandResetUserPowerLevelDescription: String { + return VectorL10n.tr("Vector", "room_command_reset_user_power_level_description") + } + /// Define the power level of a user + public static var roomCommandSetUserPowerLevelDescription: String { + return VectorL10n.tr("Vector", "room_command_set_user_power_level_description") + } + /// Unbans user with given id + public static var roomCommandUnbanUserDescription: String { + return VectorL10n.tr("Vector", "room_command_unban_user_description") + } /// You need permission to manage conference call in this room public static var roomConferenceCallNoPower: String { return VectorL10n.tr("Vector", "room_conference_call_no_power") diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 6c868ce4c..c02df825f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -230,33 +230,32 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } private extension MXKSlashCommand { - // TODO: L10N var description: String { switch self { case .changeDisplayName: - return "Changes your display nickname" + return VectorL10n.roomCommandChangeDisplayNameDescription case .emote: - return "Displays action" + return VectorL10n.roomCommandEmoteDescription case .joinRoom: - return "Joins room with given address" + return VectorL10n.roomCommandJoinRoomDescription case .partRoom: - return "Leave room" + return VectorL10n.roomCommandPartRoomDescription case .inviteUser: - return "Invites user with given id to current room" + return VectorL10n.roomCommandInviteUserDescription case .kickUser: - return "Removes user with given id from this room" + return VectorL10n.roomCommandKickUserDescription case .banUser: - return "Bans user with given id" + return VectorL10n.roomCommandBanUserDescription case .unbanUser: - return "Unbans user with given id" + return VectorL10n.roomCommandUnbanUserDescription case .setUserPowerLevel: - return "Define the power level of a user" + return VectorL10n.roomCommandSetUserPowerLevelDescription case .resetUserPowerLevel: - return "Deops user with given id" + return VectorL10n.roomCommandResetUserPowerLevelDescription case .changeRoomTopic: - return "Sets the room topic" + return VectorL10n.roomCommandChangeRoomTopicDescription case .discardSession: - return "Forces the current outbound group session in an encrypted room to be discarded" + return VectorL10n.roomCommandDiscardSessionDescription } } From 2048e2f0850bddc6d50bd1ef86d44904fb83bc26 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 17:12:09 +0200 Subject: [PATCH 15/52] Fix comment typo --- Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift index faae85e94..7498b5769 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -67,7 +67,7 @@ } } - // Note: not localized for consistancy, as commands are in english + // Note: not localized for consistency, as commands are in english // also translating these parameters could lead to inconsistency in // the UI in case of languages with otherlength translation. var parametersFormat: String { From 188916e04fce6a2d0220c624fabc82691b864708 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 09:11:35 +0200 Subject: [PATCH 16/52] Fix missing self in closure --- .../Coordinator/CompletionSuggestionCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index c02df825f..4196da77a 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -220,7 +220,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr guard let self, let powerLevels = state?.powerLevels else { return } let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) - isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin + self.isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin } } From d8acd1f351dfec32b0954e5387d3af4558591c2b Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 09:30:56 +0200 Subject: [PATCH 17/52] Fix `RoomInputToolbarTextView` pills flushing --- .../RoomInputToolbarTextView.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift index 47d981c86..eabd68c9e 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift @@ -23,6 +23,7 @@ class RoomInputToolbarTextView: UITextView { private var heightConstraint: NSLayoutConstraint! + private var pillViews = [UIView]() weak var toolbarDelegate: RoomInputToolbarTextViewDelegate? @@ -51,12 +52,18 @@ class RoomInputToolbarTextView: UITextView { } override var text: String! { + willSet { + flushPills() + } didSet { updateUI() } } override var attributedText: NSAttributedString! { + willSet { + flushPills() + } didSet { updateUI() } @@ -162,3 +169,17 @@ class RoomInputToolbarTextView: UITextView { delegate.onTouchUp(inside: delegate.rightInputToolbarButton) } } + +extension RoomInputToolbarTextView: PillViewFlusher { + func registerPillView(_ pillView: UIView) { + pillViews.append(pillView) + } + + private func flushPills() { + for view in pillViews { + view.alpha = 0.0 + view.removeFromSuperview() + } + pillViews.removeAll() + } +} From 92286ecb889927be0ffff697d869e3853987e186 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 13:47:15 +0200 Subject: [PATCH 18/52] Fix typo --- Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift index 7498b5769..54ab1ab3c 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -69,7 +69,7 @@ // Note: not localized for consistency, as commands are in english // also translating these parameters could lead to inconsistency in - // the UI in case of languages with otherlength translation. + // the UI in case of languages with overlength translation. var parametersFormat: String { switch self { case .changeDisplayName: From 3289733957cd2f4e1fccdfa8effe588f10339687 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 17:13:03 +0200 Subject: [PATCH 19/52] Fix sending command with Pills through RTE --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ .../MXKRoomInputToolbarView.h | 8 ++++++ Riot/Modules/Room/RoomViewController.m | 21 ++++++++++++++++ Riot/Modules/Room/RoomViewController.swift | 2 +- .../WysiwygInputToolbarView.swift | 25 ++++++++++++++++++- 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 38cded4a3..1aadc203f 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -627,6 +627,7 @@ Tap the + to start adding people."; "room_command_reset_user_power_level_description" = "Deops user with given id"; "room_command_change_room_topic_description" = "Sets the room topic"; "room_command_discard_session_description" = "Forces the current outbound group session in an encrypted room to be discarded"; +"room_command_error_unknown_command" = "Invalid or unhandled command"; // MARK: Threads "room_thread_title" = "Thread"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e48764f1d..db052a786 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5231,6 +5231,10 @@ public class VectorL10n: NSObject { public static var roomCommandEmoteDescription: String { return VectorL10n.tr("Vector", "room_command_emote_description") } + /// Invalid or unhandled command + public static var roomCommandErrorUnknownCommand: String { + return VectorL10n.tr("Vector", "room_command_error_unknown_command") + } /// Invites user with given id to current room public static var roomCommandInviteUserDescription: String { return VectorL10n.tr("Vector", "room_command_invite_user_description") diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index e366ae239..abd67ec7e 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -102,6 +102,14 @@ typedef enum : NSUInteger */ - (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText; +/** + Tells the delegate that the user wants to send a command. + + @param toolbarView the room input toolbar view. + @param commandText the command to send. + */ +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText; + /** Tells the delegate that the user wants to display the send media actions. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 274e7d437..7646a135e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5190,6 +5190,27 @@ static CGSize kThreadListBarButtonItemImageSize; }]; } +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText +{ + // Create before sending the message in case of a discussion (direct chat) + MXWeakify(self); + [self createDiscussionIfNeeded:^(BOOL readyToSend) { + MXStrongifyAndReturnIfNil(self); + + if (readyToSend) { + if (![self sendAsIRCStyleCommandIfPossible:commandText]) + { + // Display an error for unknown command + UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil + message:[VectorL10n roomCommandErrorUnknownCommand] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; + } + } + }]; +} + - (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView { NSMutableArray *actionItems = [NSMutableArray new]; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index c94111be4..727ca8f80 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -107,7 +107,7 @@ extension RoomViewController { "event_id": eventModified.eventId ]) }) - } else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) { + } else { roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in switch response { case .success: diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 9bc02c21e..ad488897f 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -338,7 +338,30 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } private func sendWysiwygMessage(content: WysiwygComposerContent) { - delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) + if content.markdown.prefix(while: { $0 == "/" }).count == 1 { + let commandText: String + if content.markdown.hasPrefix(MXKSlashCommand.emote.cmd) { + // `/me` command works with markdown content + commandText = content.markdown + } else if #available(iOS 15.0, *) { + // Other commands should see pills replaced by matrix identifiers + commandText = PillsFormatter.stringByReplacingPills(in: self.wysiwygViewModel.textView.attributedText, mode: .identifier) + } else { + // Without Pills support, just use the raw text for command + commandText = self.wysiwygViewModel.textView.text + } + + // Fix potential command failures due to trailing characters + // or NBSP that are not properly handled by the command interpreter + let sanitizedCommand = commandText + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: String.nbsp, with: " ") + + delegate?.roomInputToolbarView?(self, sendCommand: sanitizedCommand) + } else { + delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) + } + if isMaximised { minimise() } From 53dc32ee57fe3624f540b1ebc59bf673131a5e54 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 25 Apr 2023 17:36:06 +0200 Subject: [PATCH 20/52] Fix: Remove the matrix id from the notice display name changed event --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 2 +- changelog.d/7517.change | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7517.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1aadc203f..679041963 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2984,6 +2984,7 @@ To enable access, tap Settings> Location and select Always"; "notice_avatar_url_changed" = "%@ changed their avatar"; "notice_display_name_set" = "%@ set their display name to %@"; "notice_display_name_changed_from" = "%@ changed their display name from %@ to %@"; +"notice_display_name_changed_to" = "%@ changed their display name to %@"; "notice_display_name_removed" = "%@ removed their display name"; "notice_topic_changed" = "%@ changed the topic to \"%@\"."; "notice_room_name_changed" = "%@ changed the room name to %@."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index db052a786..90aff5d8f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3899,6 +3899,10 @@ public class VectorL10n: NSObject { public static func noticeDisplayNameChangedFromByYou(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "notice_display_name_changed_from_by_you", p1, p2) } + /// %@ changed their display name to %@ + public static func noticeDisplayNameChangedTo(_ p1: String, _ p2: String) -> String { + return VectorL10n.tr("Vector", "notice_display_name_changed_to", p1, p2) + } /// %@ removed their display name public static func noticeDisplayNameRemoved(_ p1: String) -> String { return VectorL10n.tr("Vector", "notice_display_name_removed", p1) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index e2a9e4a01..19fae141d 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -571,7 +571,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* } else { - displayText = [VectorL10n noticeDisplayNameChangedFrom:event.sender :prevDisplayname :displayname]; + displayText = [VectorL10n noticeDisplayNameChangedTo:prevDisplayname :displayname]; } } } diff --git a/changelog.d/7517.change b/changelog.d/7517.change new file mode 100644 index 000000000..f43662947 --- /dev/null +++ b/changelog.d/7517.change @@ -0,0 +1 @@ +Timeline: Remove the matrix ID displayed when someone has changed its display name. From 5926dad024ccc5b301894503b910bdbaa4ba7d4d Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 25 Apr 2023 09:48:11 +0200 Subject: [PATCH 21/52] Fix: allow to render a TimelinePoll even if the poll is loading --- .../MatrixSDK/PollHistoryService.swift | 1 + .../Coordinator/TimelinePollCoordinator.swift | 15 +++++++++++--- .../TimelinePoll/TimelinePollModels.swift | 7 +++++++ .../TimelinePollScreenState.swift | 17 ++++++++++++++++ .../TimelinePoll/TimelinePollViewModel.swift | 6 +++++- .../TimelinePollViewModelProtocol.swift | 1 + .../TimelinePoll/View/TimelinePollView.swift | 20 ++++++++++++++++--- changelog.d/7497.bugfix | 1 + 8 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 changelog.d/7497.bugfix diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 7f6d8c5f6..79784c9d8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -170,6 +170,7 @@ private extension PollHistoryService { do { newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) + newContext.pollAggregator?.reloadPollData() } catch { pollAggregationContexts.removeValue(forKey: eventId) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3214fae65..1cbfc148b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -77,6 +77,8 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } .store(in: &cancellables) + + pollAggregator.reloadPollData() } // MARK: - Public @@ -109,13 +111,20 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) + viewModel.updateWithPollState(.loaded) } - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { + viewModel.updateWithPollState(.loading) + } - func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { } + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + viewModel.updateWithPollState(.loaded) + } - func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { + viewModel.updateWithPollState(.invalidStartEvent) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 0ee87c55f..6439157dd 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -37,6 +37,12 @@ enum TimelinePollEventType { case ended } +enum TimelinePollState { + case loading + case loaded + case invalidStartEvent +} + struct TimelinePollAnswerOption: Identifiable { var id: String var text: String @@ -99,6 +105,7 @@ struct TimelinePollViewState: BindableState { } struct TimelinePollViewStateBindings { + var pollState: TimelinePollState var alertInfo: AlertInfo? } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 8c70b21e3..aea09ade2 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -23,6 +23,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { case openUndisclosed case closedUndisclosed case closedPollEnded + case loading + case invalidStartEvent + case withAlert var screenType: Any.Type { TimelinePollDetails.self @@ -47,6 +50,20 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + switch self { + case .loading: + viewModel.updateWithPollState(.loading) + case .invalidStartEvent: + viewModel.updateWithPollState(.invalidStartEvent) + default: + viewModel.updateWithPollState(.loaded) + } + + if self == .withAlert { + viewModel.showAnsweringFailure() + } + + return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index a86862cf4..da98d26a2 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -31,7 +31,7 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Setup init(timelinePollDetails: TimelinePollDetails) { - super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings())) + super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings(pollState: .loading))) } // MARK: - Public @@ -58,6 +58,10 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro state.poll = pollDetails } + func updateWithPollState(_ pollState: TimelinePollState) { + state.bindings.pollState = pollState + } + func showAnsweringFailure() { state.bindings.alertInfo = AlertInfo(id: .failedSubmittingAnswer, title: VectorL10n.pollTimelineVoteNotRegisteredTitle, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index 492f7f7a3..f4e0e5a20 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -21,6 +21,7 @@ protocol TimelinePollViewModelProtocol { var completion: ((TimelinePollViewModelResult) -> Void)? { get set } func updateWithPollDetails(_ pollDetails: TimelinePollDetails) + func updateWithPollState(_ pollState: TimelinePollState) func showAnsweringFailure() func showClosingFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 2109a0e8a..fb1af9b2b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -28,6 +28,23 @@ struct TimelinePollView: View { @ObservedObject var viewModel: TimelinePollViewModel.Context var body: some View { + Group { + switch viewModel.pollState { + case .loading: + TimelinePollMessageView(message: "loading...") + case .loaded: + pollContent + case .invalidStartEvent: + TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) + } + } + .alert(item: $viewModel.alertInfo) { info in + info.alert + } + } + + @ViewBuilder + private var pollContent: some View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { @@ -61,9 +78,6 @@ struct TimelinePollView: View { } .padding([.horizontal, .top], 2.0) .padding([.bottom]) - .alert(item: $viewModel.alertInfo) { info in - info.alert - } } private var totalVotesString: String { diff --git a/changelog.d/7497.bugfix b/changelog.d/7497.bugfix new file mode 100644 index 000000000..a8558b843 --- /dev/null +++ b/changelog.d/7497.bugfix @@ -0,0 +1 @@ +Poll: The timeline sometimes displayed closed polls in the wrong order. From 8cfb199bc2c9e04bfa0a543fe4abac2a7ccda475 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 12:58:18 +0200 Subject: [PATCH 22/52] Disable accessibility for emojis during verification --- .../Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift b/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift index e609f6b38..df8b3359e 100644 --- a/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift +++ b/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift @@ -24,4 +24,9 @@ class VerifyEmojiCollectionViewCell: UICollectionViewCell, Reusable, Themable { func update(theme: Theme) { name.textColor = theme.textPrimaryColor } + + override func awakeFromNib() { + super.awakeFromNib() + emoji.isAccessibilityElement = false + } } From 7286dc7c48c4d9cc9649fddac353ce5b3729225f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 13:03:06 +0200 Subject: [PATCH 23/52] Add changelog.d file --- changelog.d/pr-7521.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7521.bugfix diff --git a/changelog.d/pr-7521.bugfix b/changelog.d/pr-7521.bugfix new file mode 100644 index 000000000..3cedf12d4 --- /dev/null +++ b/changelog.d/pr-7521.bugfix @@ -0,0 +1 @@ +Disable accessibility for emojis during session verification. \ No newline at end of file From 1d6512042913f89b25e44777c58bef867e576854 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:36:29 +0200 Subject: [PATCH 24/52] Fix accessibility in SetPinCoordinatorBridgePresenter --- .../Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift index c381b76eb..6c4e69d14 100644 --- a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift +++ b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift @@ -91,6 +91,10 @@ final class SetPinCoordinatorBridgePresenter: NSObject { } func presentWithMainAppWindow(_ window: UIWindow) { + // Prevents the VoiceOver reading accessible content when the PIN screen is on top + // Calling `makeKeyAndVisible` in `dismissWithMainAppWindow(_:)` restores the visibility state. + window.isHidden = true + let pinCoordinatorWindow = UIWindow(frame: window.bounds) let setPinCoordinator = SetPinCoordinator(session: self.session, viewMode: self.viewMode, pinCodePreferences: .shared) From 9d73564cfe21eee639bcf5e49454be5d702a2d4f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:46:43 +0200 Subject: [PATCH 25/52] Remove accessibility from placeholder button --- .../EnterPinCodeViewController.storyboard | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard index e3f675ca6..ab5ef6482 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard @@ -1,26 +1,27 @@ - + - + + - + - + @@ -44,20 +45,20 @@ - + @@ -97,7 +98,7 @@ - + @@ -106,7 +107,7 @@ - + @@ -124,12 +125,12 @@ - + - - - - - - - - - + @@ -312,6 +316,7 @@ + @@ -323,7 +328,6 @@ - @@ -350,5 +354,8 @@ + + + From f72f7c238ad605b13484ead5fe53e2f93ef8f88d Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:55:19 +0200 Subject: [PATCH 26/52] Add changelog.d file --- changelog.d/pr-7522.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7522.bugfix diff --git a/changelog.d/pr-7522.bugfix b/changelog.d/pr-7522.bugfix new file mode 100644 index 000000000..0bd4e5b53 --- /dev/null +++ b/changelog.d/pr-7522.bugfix @@ -0,0 +1 @@ +Fix accessibility when entering the PIN to unlock the app. From 92895f0f26d6dbe0e4deb1e63730352b77e3e145 Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Tue, 25 Apr 2023 11:03:51 +0100 Subject: [PATCH 27/52] Update triage for labelled issues Modernise actions from graphql to use new actions. Remove automation for Delight, WTF, FTUE, voice message and message bubble boards. --- .github/workflows/triage-move-labelled.yml | 248 ++------------------- 1 file changed, 21 insertions(+), 227 deletions(-) diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 291360fd2..130326b3d 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -53,23 +53,10 @@ jobs: contains(github.event.issue.labels.*.name, 'O-Frequent')) || contains(github.event.issue.labels.*.name, 'A11y')) steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc0sUA" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/18 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_issues_to_project: name: X-Needs-Product to Design project board @@ -77,138 +64,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'X-Needs-Product') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - Delight_issues_to_board: - name: Spaces issues to Delight project board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: Delight') || - contains(github.event.issue.labels.*.name, 'Z-AppLayout') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc1HvQ" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_voice-message_issues: - name: A-Voice Messages to voice message board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Voice Messages') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc2KCw" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - move_message_bubble_issues: - name: A-Message-Bubbles to Message bubble board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc3m-g" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_FTUE_issues: - name: Z-FTUE to FTUE board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Z-FTUE') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_WTF_issues: - name: Z-WTF to WTF board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Z-WTF') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AArk0" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/28 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ex_plorers: name: Add labelled issues to X-Plorer project @@ -216,23 +75,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/73 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features1: name: Add labelled issues to PS features team 1 @@ -245,23 +91,10 @@ jobs: (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') && contains(github.event.issue.labels.*.name, 'A-User-Settings')) steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKF" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/56 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features2: name: Add labelled issues to PS features team 2 @@ -270,23 +103,10 @@ jobs: contains(github.event.issue.labels.*.name, 'A-DM-Start') || contains(github.event.issue.labels.*.name, 'A-Broadcast') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKd" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/58 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features3: name: Add labelled issues to PS features team 3 @@ -294,23 +114,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/57 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} voip: name: Add labelled issues to VoIP project board @@ -318,20 +125,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: VoIP') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ABMIk" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/41 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} From 1c4545941098572777f46b0351bbaa05f131e76b Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 26 Apr 2023 15:31:07 +0200 Subject: [PATCH 28/52] Fix: TimelinePoll code refactoring --- Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 4 + .../MockPollHistoryDetailScreenState.swift | 2 +- .../MatrixSDK/PollHistoryService.swift | 10 +- .../Coordinator/TimelinePollCoordinator.swift | 29 +++-- .../Unit/TimelinePollViewModelTests.swift | 115 +++++++++++------- .../TimelinePoll/TimelinePollModels.swift | 9 +- .../TimelinePollScreenState.swift | 9 +- .../TimelinePoll/TimelinePollViewModel.swift | 51 ++++---- .../TimelinePollViewModelProtocol.swift | 3 +- .../TimelinePoll/View/TimelinePollView.swift | 26 ++-- 11 files changed, 143 insertions(+), 117 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1aadc203f..41b2b4a54 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2405,6 +2405,8 @@ Tap the + to start adding people."; "poll_timeline_reply_ended_poll" = "Ended poll"; +"poll_timeline_loading" = "Loading..."; + // MARK: - Location sharing "location_sharing_title" = "Location"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index db052a786..cd5a22877 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4923,6 +4923,10 @@ public class VectorL10n: NSObject { public static var pollTimelineEndedText: String { return VectorL10n.tr("Vector", "poll_timeline_ended_text") } + /// Loading... + public static var pollTimelineLoading: String { + return VectorL10n.tr("Vector", "poll_timeline_loading") + } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 09a8fb3c7..9c57bdabb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -48,7 +48,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) + let timelineViewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) let viewModel = PollHistoryDetailViewModel(poll: poll) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context)))) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 79784c9d8..9425cc2d4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -170,7 +170,6 @@ private extension PollHistoryService { do { newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) - newContext.pollAggregator?.reloadPollData() } catch { pollAggregationContexts.removeValue(forKey: eventId) } @@ -210,13 +209,14 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else { + + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published == false else { return } context.published = true - let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) + let newPoll: TimelinePollDetails = .init(poll: poll, represent: .started) if context.isLivePoll { livePollsSubject.send(newPoll) @@ -226,9 +226,9 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published else { + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published else { return } - updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) + updatesSubject.send(.init(poll: poll, represent: .started)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 1cbfc148b..e2202524b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -32,7 +32,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() - private var pollAggregator: PollAggregator + private var pollAggregator: PollAggregator! private(set) var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() @@ -46,10 +46,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) - pollAggregator.delegate = self + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent, delegate: self) - viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) viewModel.completion = { [weak self] result in guard let self = self else { return } @@ -77,8 +76,6 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } .store(in: &cancellables) - - pollAggregator.reloadPollData() } // MARK: - Public @@ -94,11 +91,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } func canEndPoll() -> Bool { - pollAggregator.poll.isClosed == false + pollAggregator.poll?.isClosed == false } func canEditPoll() -> Bool { - pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + pollAggregator.poll?.isClosed == false && pollAggregator.poll?.totalAnswerCount == 0 } func endPoll() { @@ -110,20 +107,22 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) - viewModel.updateWithPollState(.loaded) + if let poll = aggregator.poll { + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) + } } - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { - viewModel.updateWithPollState(.loading) - } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - viewModel.updateWithPollState(.loaded) + guard let poll = aggregator.poll else { + return + } + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - viewModel.updateWithPollState(.invalidStartEvent) + viewModel.updateWithPollDetailsState(.errored) } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index a36a7d092..2fd2b032f 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -41,111 +41,134 @@ class TimelinePollViewModelTests: XCTestCase { hasBeenEdited: false, hasDecryptionError: false) - viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(timelinePoll)) context = viewModel.context } func testInitialState() { - XCTAssertEqual(context.viewState.poll.answerOptions.count, 3) - XCTAssertFalse(context.viewState.poll.closed) - XCTAssertEqual(context.viewState.poll.type, .disclosed) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions.count, 3) + XCTAssertEqual(context.viewState.pollState.poll?.closed, false) + XCTAssertEqual(context.viewState.pollState.poll?.type, .disclosed) } func testSingleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testMultipleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testClosedSelection() { - viewModel.state.poll.closed = true + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.closed = true + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 - + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) + context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) + } +} + +private extension TimelinePollDetailsState { + var poll: TimelinePollDetails? { + switch self { + case .loaded(let poll): + return poll + default: + return nil + } } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 6439157dd..6b2d52c78 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -37,10 +37,10 @@ enum TimelinePollEventType { case ended } -enum TimelinePollState { +enum TimelinePollDetailsState { case loading - case loaded - case invalidStartEvent + case loaded(TimelinePollDetails) + case errored } struct TimelinePollAnswerOption: Identifiable { @@ -100,12 +100,11 @@ struct TimelinePollDetails { extension TimelinePollDetails: Identifiable { } struct TimelinePollViewState: BindableState { - var poll: TimelinePollDetails + var pollState: TimelinePollDetailsState var bindings: TimelinePollViewStateBindings } struct TimelinePollViewStateBindings { - var pollState: TimelinePollState var alertInfo: AlertInfo? } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index aea09ade2..c81d78683 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -48,22 +48,21 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { hasBeenEdited: false, hasDecryptionError: false) - let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + let viewModel: TimelinePollViewModel switch self { case .loading: - viewModel.updateWithPollState(.loading) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) case .invalidStartEvent: - viewModel.updateWithPollState(.invalidStartEvent) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .errored) default: - viewModel.updateWithPollState(.loaded) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) } if self == .withAlert { viewModel.showAnsweringFailure() } - return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index da98d26a2..26ac65a68 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -30,8 +30,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Setup - init(timelinePollDetails: TimelinePollDetails) { - super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings(pollState: .loading))) + init(timelinePollDetailsState: TimelinePollDetailsState) { + super.init(initialViewState: TimelinePollViewState(pollState: timelinePollDetailsState, bindings: TimelinePollViewStateBindings())) } // MARK: - Public @@ -40,11 +40,11 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro switch viewAction { // Update local state. An update will be pushed from the coordinator once sent. case .selectAnswerOptionWithIdentifier(let identifier): - guard !state.poll.closed else { + // only if the poll is ready and not closed + guard case let .loaded(poll) = state.pollState, !poll.closed else { return } - - if state.poll.maxAllowedSelections == 1 { + if poll.maxAllowedSelections == 1 { updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) } else { updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) @@ -54,12 +54,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - TimelinePollViewModelProtocol - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) { - state.poll = pollDetails - } - - func updateWithPollState(_ pollState: TimelinePollState) { - state.bindings.pollState = pollState + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) { + state.pollState = pollDetailsState } func showAnsweringFailure() { @@ -77,33 +73,40 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Private func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - state.poll.answerOptions.updateEach { answerOption in + guard case var .loaded(poll) = state.pollState else { return } + + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.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)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } if answerOption.id == selectedAnswerIdentifier { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } + guard case .loaded(var poll) = state.pollState else { return } + + let selectedAnswerOptions = poll.answerOptions.filter { $0.selected == true } let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 - if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { + if !isDeselecting, selectedAnswerOptions.count >= poll.maxAllowedSelections { return } - state.poll.answerOptions.updateEach { answerOption in + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.updateEach { answerOption in if answerOption.id != selectedAnswerIdentifier { return } @@ -111,22 +114,24 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro 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)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } else { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { - let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in + guard case .loaded(let poll) = state.pollState else { return } + + let selectedIdentifiers = poll.answerOptions.compactMap { answerOption in answerOption.selected ? answerOption.id : nil } - callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index f4e0e5a20..ade681438 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -20,8 +20,7 @@ protocol TimelinePollViewModelProtocol { var context: TimelinePollViewModelType.Context { get } var completion: ((TimelinePollViewModelResult) -> Void)? { get set } - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) - func updateWithPollState(_ pollState: TimelinePollState) + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) func showAnsweringFailure() func showClosingFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index fb1af9b2b..52533288c 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -29,12 +29,12 @@ struct TimelinePollView: View { var body: some View { Group { - switch viewModel.pollState { + switch viewModel.viewState.pollState { case .loading: - TimelinePollMessageView(message: "loading...") - case .loaded: - pollContent - case .invalidStartEvent: + TimelinePollMessageView(message: VectorL10n.pollTimelineLoading) + case .loaded(let poll): + pollContent(poll) + case .errored: TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) } } @@ -44,9 +44,7 @@ struct TimelinePollView: View { } @ViewBuilder - private var pollContent: some View { - let poll = viewModel.viewState.poll - + private func pollContent(_ poll: TimelinePollDetails) -> some View { VStack(alignment: .leading, spacing: 16.0) { if poll.representsPollEndedEvent { Text(VectorL10n.pollTimelineEndedText) @@ -57,7 +55,7 @@ struct TimelinePollView: View { Text(poll.question) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + - Text(editedText) + Text(editedText(poll)) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) @@ -71,7 +69,7 @@ struct TimelinePollView: View { .disabled(poll.closed) .fixedSize(horizontal: false, vertical: true) - Text(totalVotesString) + Text(totalVotesString(poll)) .lineLimit(2) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) @@ -80,9 +78,7 @@ struct TimelinePollView: View { .padding([.bottom]) } - private var totalVotesString: String { - let poll = viewModel.viewState.poll - + private func totalVotesString(_ poll: TimelinePollDetails) -> String { if poll.hasDecryptionError, poll.totalAnswerCount > 0 { return VectorL10n.pollTimelineDecryptionError } @@ -109,8 +105,8 @@ struct TimelinePollView: View { } } - private var editedText: String { - viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" + private func editedText(_ poll: TimelinePollDetails) -> String { + poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" } } From 0bc32f0f97ae477ad15f459e03a08a4ff870373a Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 27 Apr 2023 14:55:40 +0200 Subject: [PATCH 29/52] Update RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift Co-authored-by: Alfonso Grillo --- .../Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 9425cc2d4..c4471844e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -209,7 +209,6 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published == false else { return } From b6811fd99ba966cf577eba1341a2e228486d2eca Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 28 Apr 2023 15:47:51 +0200 Subject: [PATCH 30/52] Fix a flickering issue when the timeline datasource is reloaded. --- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 19 +++++++------------ changelog.d/7523.bugfix | 1 + 2 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 changelog.d/7523.bugfix diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index a69f504cc..033aa9361 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -456,11 +456,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } - (void)reset -{ - [self resetNotifying:YES]; -} - -- (void)resetNotifying:(BOOL)notify { if (roomDidFlushDataNotificationObserver) { @@ -556,12 +551,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } _serverSyncEventCount = 0; - - // Notify the delegate to reload its tableview - if (notify && self.delegate) - { - [self.delegate dataSource:self didCellChange:nil]; - } } - (void)reload @@ -575,10 +564,16 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self setState:MXKDataSourceStatePreparing]; - [self resetNotifying:notify]; + [self reset]; // Reload [self didMXSessionStateChange]; + + // Notify the delegate to refresh the tableview + if (notify && self.delegate) + { + [self.delegate dataSource:self didCellChange:nil]; + } } - (void)destroy diff --git a/changelog.d/7523.bugfix b/changelog.d/7523.bugfix new file mode 100644 index 000000000..bc5cf31a7 --- /dev/null +++ b/changelog.d/7523.bugfix @@ -0,0 +1 @@ +Fix a flickering issue when the timeline datasource is reloaded. From 72013759de3727d700c9e3ddc51d12c60e42672c Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 2 May 2023 11:11:51 +0200 Subject: [PATCH 31/52] Fix application crashing when opening a thread with RTE enabled --- Riot/Modules/Room/RoomViewController.m | 6 ++++++ changelog.d/7530.bugfix | 1 + 2 files changed, 7 insertions(+) create mode 100644 changelog.d/7530.bugfix diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7646a135e..799f1f718 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1194,6 +1194,12 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)updateRoomInputToolbarViewClassIfNeeded { Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass]; + + // If RTE is enabled, delay the toolbar setup until `completionSuggestionCoordinator` is ready. + if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && _completionSuggestionCoordinator == nil) + { + return; + } BOOL shouldDismissContextualMenu = NO; diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix new file mode 100644 index 000000000..5733a8d81 --- /dev/null +++ b/changelog.d/7530.bugfix @@ -0,0 +1 @@ +Fix application crashing when opening a thread with RTE enabled From 53ad72dd2405cbddddabac47cf409f4e1a3e862f Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 2 May 2023 11:44:23 +0200 Subject: [PATCH 32/52] Update room input toolbar when `CompletionSuggestionCoordinator` is initialised --- Riot/Modules/Room/RoomViewController.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 799f1f718..a18de9aa1 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1094,6 +1094,8 @@ static CGSize kThreadListBarButtonItemImageSize; _completionSuggestionCoordinator.delegate = self; [self setupCompletionSuggestionViewIfNeeded]; + + [self updateRoomInputToolbarViewClassIfNeeded]; [self updateTopBanners]; } From 2c1d56ece9d5df66b6f292019f78eb41c3ee859c Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 3 Mar 2023 10:02:57 +0100 Subject: [PATCH 33/52] Secrets recovery: fix an issue preventing the release of SecureBackupSetupCoordinator --- .../Secrets/Recover/SecretsRecoveryCoordinator.swift | 7 ++++--- .../Setup/SecureBackupSetupCoordinator.swift | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift index d36ee995e..817414956 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift @@ -121,11 +121,12 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { private func showSecureBackupSetup(checkKeyBackup: Bool) { let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable) coordinator.delegate = self - coordinator.start() - - self.navigationRouter.push(coordinator.toPresentable(), animated: true, popCompletion: { [weak self] in + // Fix: calling coordinator.start() will update the navigationRouter without a popCompletion + coordinator.start(popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) + // Fix: do not push the presentable from the coordinator to the navigation router as this has already been done by coordinator.start(). + // Also, coordinator.toPresentable() returns a navigation controller, which cannot be pushed into a navigation router. self.add(childCoordinator: coordinator) } } diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift index 53a03e359..0cb633945 100644 --- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift +++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift @@ -73,15 +73,19 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { // MARK: - Public methods func start() { + start(popCompletion: nil) + } + + func start(popCompletion: (() -> Void)?) { let rootViewController = self.createIntro() if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootViewController, animated: true, popCompletion: nil) + self.navigationRouter.push(rootViewController, animated: true, popCompletion: popCompletion) } else { - self.navigationRouter.setRootModule(rootViewController) + self.navigationRouter.setRootModule(rootViewController, popCompletion: popCompletion) } } - + func toPresentable() -> UIViewController { return self.navigationRouter .toPresentable() From 6500a8ac43637a79e902949676a12d327241e866 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 3 Apr 2023 13:50:35 +0200 Subject: [PATCH 34/52] =?UTF-8?q?Fix:=20don=E2=80=99t=20allow=20to=20reset?= =?UTF-8?q?=20secrets=20if=20it=20is=20already=20in=20progress.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift | 2 +- Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift | 1 + .../Secrets/Reset/SecretsResetViewController.swift | 6 ++++++ Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift | 9 ++++++++- Riot/Modules/Secrets/Reset/SecretsResetViewState.swift | 1 + changelog.d/pr-7404.bugfix | 1 + 6 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pr-7404.bugfix diff --git a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift index 6c72ebe5d..bd9740ad5 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift @@ -94,11 +94,11 @@ extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate { extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate { func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { - self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:])) } func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { + self.secretsResetViewModel.process(viewAction: .authenticationCancelled) self.remove(childCoordinator: coordinator) } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift index aa135b5fe..5b960342a 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift @@ -22,6 +22,7 @@ import Foundation enum SecretsResetViewAction { case loadData case reset + case authenticationCancelled case authenticationInfoEntered(_ authInfo: [String: Any]) case cancel } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift index 6008a1fb0..fccbfb6e3 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift @@ -132,6 +132,8 @@ final class SecretsResetViewController: UIViewController { self.renderLoading() case .resetDone: self.renderLoaded() + case .resetCancelled: + self.renderCancelled() case .error(let error): self.render(error: error) } @@ -145,6 +147,10 @@ final class SecretsResetViewController: UIViewController { self.activityPresenter.removeCurrentActivityIndicator(animated: true) } + private func renderCancelled() { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + } + private func render(error: Error) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift index 2e8e7604c..62b0c686f 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift @@ -49,6 +49,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType { break case .reset: self.askAuthentication() + case .authenticationCancelled: + self.authenticationCancelled() case .authenticationInfoEntered(let authParameters): self.resetSecrets(with: authParameters) case .cancel: @@ -68,7 +70,6 @@ final class SecretsResetViewModel: SecretsResetViewModelType { } MXLog.debug("[SecretsResetViewModel] resetSecrets") - self.update(viewState: .resetting) crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in guard let self = self else { return @@ -96,7 +97,13 @@ final class SecretsResetViewModel: SecretsResetViewModelType { } private func askAuthentication() { + self.update(viewState: .resetting) + let setupCrossSigningRequest = self.crossSigningService.setupCrossSigningRequest() self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: setupCrossSigningRequest) } + + private func authenticationCancelled() { + self.update(viewState: .resetCancelled) + } } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift index b7cb0acb8..128f90b19 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift @@ -22,5 +22,6 @@ import Foundation enum SecretsResetViewState { case resetting case resetDone + case resetCancelled case error(Error) } diff --git a/changelog.d/pr-7404.bugfix b/changelog.d/pr-7404.bugfix new file mode 100644 index 000000000..58609a160 --- /dev/null +++ b/changelog.d/pr-7404.bugfix @@ -0,0 +1 @@ +Fix an issue where the Secrets Reset screen would open twice. From c032762bdb398ad3f1a00762ee07b6ca96944ed1 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 2 May 2023 15:26:32 +0200 Subject: [PATCH 35/52] Fix the frame of the marker view highlighting an event --- .../MXKRoomBubbleTableViewCell+Riot.m | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 907dd5ff2..9164a61d7 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -256,40 +256,18 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = if (componentIndex < bubbleComponents.count) { - MXKRoomBubbleComponent *component = bubbleComponents[componentIndex]; - - // Define the marker frame - CGFloat markPosY = component.position.y + self.msgTextViewTopConstraint.constant; - - NSInteger mostRecentComponentIndex = bubbleComponents.count - 1; - if ([bubbleData isKindOfClass:RoomBubbleCellData.class]) + CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex]; + if (CGRectIsEmpty(componentFrame)) { - mostRecentComponentIndex = ((RoomBubbleCellData*)bubbleData).mostRecentComponentIndex; - } - - // Compute the mark height. - // Use the rest of the cell height by default. - CGFloat markHeight = self.contentView.frame.size.height - markPosY; - if (componentIndex != mostRecentComponentIndex) - { - // There is another component (with display) after this component in the cell. - // Stop the marker height to the top of this component. - for (NSInteger index = componentIndex + 1; index < bubbleComponents.count; index ++) - { - MXKRoomBubbleComponent *nextComponent = bubbleComponents[index]; - - if (nextComponent.attributedTextMessage) - { - markHeight = nextComponent.position.y - component.position.y; - break; - } - } + return; } - UIView *markerView = [[UIView alloc] initWithFrame:CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X, - markPosY, - VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH, - markHeight)]; + CGRect markerFrame = CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X, + CGRectGetMinY(componentFrame), + VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH, + CGRectGetHeight(componentFrame)); + + UIView *markerView = [[UIView alloc] initWithFrame:markerFrame]; markerView.backgroundColor = ThemeService.shared.theme.tintColor; [markerView setTranslatesAutoresizingMaskIntoConstraints:NO]; @@ -303,28 +281,28 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = toItem:self.contentView attribute:NSLayoutAttributeLeading multiplier:1.0 - constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X]; + constant:CGRectGetMinX(markerFrame)]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0 - constant:markPosY]; + constant:CGRectGetMinY(markerFrame)]; NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH]; + constant:CGRectGetWidth(markerFrame)]; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:markHeight]; + constant:CGRectGetHeight(markerFrame)]; // Available on iOS 8 and later [NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]]; From 318a806cfd07d4e1cfe4a34e28da22e4f7796194 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 11:43:02 +0200 Subject: [PATCH 36/52] Fix: highlighting an event removes the highlighting of the previous event. --- Riot/Modules/Room/RoomViewController.m | 36 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a18de9aa1..592495dda 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7504,23 +7504,47 @@ static CGSize kThreadListBarButtonItemImageSize; return; } + NSMutableArray *rowsToReload = [[NSMutableArray alloc] init]; + // Get the current hightlighted event because we will need to reload it + NSString *currentHiglightedEventId = self.customizedRoomDataSource.highlightedEventId; + if (currentHiglightedEventId) + { + NSInteger currentHiglightedRow = [self.roomDataSource indexOfCellDataWithEventId:currentHiglightedEventId]; + if (currentHiglightedRow != NSNotFound) + { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentHiglightedRow inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + [rowsToReload addObject:indexPath]; + } + } + } + self.customizedRoomDataSource.highlightedEventId = eventId; + // Add the new highligted event to the list of rows to reload NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; - if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]; + if (indexPathIsVisible) { - [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + [rowsToReload addObject:indexPath]; + } + + // Reload rows + if (rowsToReload.count > 0) + { + [self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone]; - [self.bubblesTableView scrollToRowAtIndexPath:indexPath - atScrollPosition:UITableViewScrollPositionMiddle - animated:YES]; } - else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) + + // Scroll to the newly highlighted row + if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath]) { [self.bubblesTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; } + if (completion) { completion(); From 10e51204af37e7e7a84f91f7e4a496f180b82141 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 11:49:54 +0200 Subject: [PATCH 37/52] Add changelog file --- changelog.d/7526.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7526.bugfix diff --git a/changelog.d/7526.bugfix b/changelog.d/7526.bugfix new file mode 100644 index 000000000..7adb60cc0 --- /dev/null +++ b/changelog.d/7526.bugfix @@ -0,0 +1 @@ +Fix the position of the marker highlighting an event. From 19e94cddc9189a0ff0c42b1bde684ef5df0243a7 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 20 Apr 2023 14:31:26 +0200 Subject: [PATCH 38/52] VoiceBroadcast: Play a sound to notify the user when VB is pause due to an error. --- Riot/Assets/Sounds/vberror.mp3 | Bin 0 -> 9497 bytes .../MatrixKitAssets.bundle/Sounds/vberror.mp3 | Bin 0 -> 9497 bytes .../MatrixKit/MatrixKit-Bridging-Header.h | 1 + .../VoiceBroadcastRecorderService.swift | 35 +++++++++++++++++- changelog.d/7504.change | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Riot/Assets/Sounds/vberror.mp3 create mode 100644 Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/Sounds/vberror.mp3 create mode 100644 changelog.d/7504.change diff --git a/Riot/Assets/Sounds/vberror.mp3 b/Riot/Assets/Sounds/vberror.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..14c710595b54f5a340c2e67d1b4f8938f9e7b560 GIT binary patch literal 9497 zcmd6tXH*kWyYD9?gwR7TfD@3~*jx@+B!XRiel!kWFG-~K;sPhw)Ai~wlE&d$-%5xPnP z0Ngg%`|?%y5HG;O@0v%5yZ2=gHTmNrsw#4NewRh`uJ{FdiYRF)0d}^whR_{s&>iOP zVcx2Ed1ZM;Jf1e&e+Ky1>c3z9Kd&Z1-oem6*`Xg<01$r-Ff(&-@bU@?31Ki&QgU+2 z%F62Mr%o9dn3$NHK5cJ*_Uu_A(c3#P@Y1DASFhf>6&;(^~G=d&= zgrRM>4bTT;Nbv(N)l^u4oVZ4B)ESF2c6-q;^hb&LMJJ?00+6F~OfQ5Urh59M`x~jQWvQ&zZ#^Tq z6X5(-SJ{OszY3{P-l7R5~ASA$v zV43LRjg!|cly%@R)x>n*0?gq4=b`a4k}V#g7^iZmbwAevwL zYyhKf53h&GJD}Z;;pPp8Dv|PwZn4XkfkdOb6_ZxiT9L#n8;kFf$~CZOYQY1;e|6G7 zEj{NhMg(Vh@ox*^D&>s7ys%%0?2bM2Piu4&MJLMeVO)hf@2u*~U(3kg)ahf(KA?pX zI>yp>Dli=@5nK$5DwH*~_;)@Y59~N1=m+ibaqqkqn|M1;(qb>~B$6vTHE?XX@6_LbeaHtR|YT&42 z05i^95bi{VE!Z)v6uMN38~-9AmbPGtMnB{SX?>Jh*LOEzv1ooiPf1qV(A0b!D$8%P_amkH)44I z5-m^hdd{vZIb9_$1^mUDCy6JP^=;>IFxe_hH{%MxV9&SD4&eHW%9Cy&wNW_|2LL)E zXcU6(Fee`K;JICQ0L7=n)I7@q zyA3G#eRmfpIstyafCS{GJ2#08?4R$vM+0IaaXe-k`KjTA5`cwI0$r*|f|PZs zZztI~_Q_=+2Z+_QdElojvcS}FUeG|_LPYOC$L@j;?}_0fJNs`elF?+jiU1tw z%dCLMI{$X-UxOu9!QeMiy7eY`BgXuNOe471IrR2o?=$KQs5#_VmZISgcqFEye-;<| zG9_d)RxrJj8(NrH{`AII^TL$u%XDkl%M%}ld_Zsu1VaEEYlQ&oy?3EaErhcx+~2@a zaHY25B1y)f)R__HgB|Bywv;|=oAV}9`e8}^@KfEGz;VN|SEW@e@(&DFhlBq_Qj=09 z!DUAPrjx}F2-x@b;}`sowpSILrvYK2qO!OLmrXdI2*UhX0Hg#`NI_TZf+*mEl==pH ztg|kYClzf5T11=tyJj-LXA09TX8NUh(fb@um!;DDr$J>$NN50$a>>a2RgLkE_t6bK+ z&n`&RlX&){jO5`>(wAympVNTkNg4M9KFP#$lKEgJ!+@k!y!vMk@*CZkPI2F9K>M!i zSa0G`BSm!B#v{DRX>?eX+Oiq|O{us!y*bCvgsc`&5s|cRXfFKPoo`lgV(;`Gj)$bh z)p^c0s;^$|f&7rX6jRWc@5>J}RHxd}?ov~wb&KC8b+4!9zQ0F((g@mGd;xtb`P(?N zzuDd7Z4$RwdE(%B_B6VM4~muX-lfWLHG*%S2rE-G;`8F`7<6xhh^h1nqS2y1rMr() ztdo&o;2s>s1WxYn(tu6?9QOsSmd!aRP?oC)uzE;mKRoN3GQf@A`x{&WC5!0;3q=@W zX4iT#=j?LN-(E`N?uOHeT5xV0?Z6Ug3!qJ6`w#NpnRI^*7BaOn7PWD1>JA$)Q#l)7&VTCEO zHTQA*HgKfKiTGD~`KvgcS$fn{gQq2^S1Mey@bP*r>SHF<9dY2-KVs{{DT)Uv`h3MV z4%d*v0}$NbrU4m8!4d^+XBE&>e1%R~5D@*Wf-z~fy~aKEhbNGOqEsLf=;_FCEf-^R z-Cj)tebms)k0YX~t>iUg@NdQhG8#sRO4dN%53|<%JJEa~C<%~>s0W}%h`w0qCc`Q? zZhW=aO$}O3C#pj44W6V{cK|=aghC5TShveca2td_QOwF&$bU2K`n1{#<67#+8Cl-4 zZ5%TnPwlGX>}(TtFuqRp`W~%s6*R~2jSnV1AEd8C*J(hHI(@!!;&-$Nh3vdfwvB5O zX-%a8*^_FMS$YT)#B|>I@+^AHd8CNFMwB~-XmdxR6_IE)S%TB*AbfkJckBS_^Kn5% zP{9t2qm~f-RfBkJN2XhDI@$rpG6r`T!IfSTqFz7{$4b4U6e#3)uS1il{M2%2g@Rqsj-yXOPcRUdRmQ`OA zdjc)Huyk~`j+e37>6&{^P5!@4_oCiPY&~ArgtIe_QZVZEnf71e0YE6q$ylc@{S*zz z3l>FSwkC=YSVTqrU0{;daVRg1LVm(*MBKtRxF{j)I`x|m#=K%=MY(qjfVJC=lIcO& z0o1lel6?fvv-@eN8Mb2*cAJCk6NK?xJ}MRW>dT-8uKP>-8S(H>DM2HK-r1d^qTV>o zMd7dN-MsH`fRF<7FSz#ZTgfga-nK{KPc5HBiPkw0Mo(LZc|@;)u4#My-XwrdO3M@v z9JyT4k&qzN#e^HbjHT!N+$&CgSLdv*) z)Or!i*~3$;;RSX3h!=*sq>_r7>&bpw2p-oyL&-4*Uj-zpx6(+l`ln9jm2U9K6n5+* z0nkcbRH`>{`TAZ}j}1Z`F~ff?%3*tDQhLpR|{W<}~1*6M&C zfn~`2MAP-EpHF3VUqo*A1Hc)#J2fbH0+~%XD))5Hel8&=Y22b;QO)#`0|*sq7CC?% zCY9M*&QEGyQx0EW*tf*0w$bZQb+YF!i08=OZw4ZJ}4^VfUCNb5*)1?{a>5`LIBM5=C6Go1s{!rEX;hT;_-b!O52C%%xiiaS~p( zACYC611V00{hzEd_r?P3?U8m+`hL~m>3ShB>72@|975|O8qhsh#%XD*SraZ<5iyNk z;QTp+fuWp!lqe9N>aBvva_+HFjEWfAcr%M$i1-<&({1)jJar)T z=I4b|oyKvePd%){mUz|`KS{f6FZ(Mz1yzd~M;bw83*2l0EnVa;=NPQ!D1EQC5g5)6 zik5F;`*@`jeMeg+W*w}3kDLGHg5k;Fzt~FfsjVsIPZ&3S4o^uB>^!0aC`&;{s$lIV z0om)HaX~f$m#or>7U>O|EV{-=qCT+bwcnVPq5(CeZSd>2(UG`9gcAPX0kL_6{V^I) z>#d?$9Uw391B2`b#Mk6Hs|yVz)x@S3PrGg&zjA(tXzKa!N@lws?scM91u_hr0T&2A z-L_;?deSZo!-j+|tj5_CCUWrc${&v=Bs^D8{&qADQ2}a$e3{L4d!MD|u!UFbL>t7f zK1a7jE4#N33%8+UPYJ4~{1(m5ob80Vq|x9VQ27Q}e`+=3?Qr9j?{#Jc4+!V2em9&t zeo~H~sA3i+?k_y=x*%+$y$n)ETXrI!t(Y^qgdBZB5rKdVECdb}3M-l= z0clZ3mci|cpY%;$Ykjpt3kD5$Bi?aNXUX|4dGqR9Kgzk$u$5Fn>B>7($QQlLX4~fq$PkefA>;9 zZ~UN62Yy$s&)cb@R}oc@d_U26VMa65)XIjkOzaXpiWJiZ=MSJL2EkKrFQ4aD*6ii#)o1`>M3b!jmDsJ4S(%Y5Bip9?u%M z%=A%B2^5psnQF#9-rg_Zzv1-GuC~8u`ge=@Zlxp13`#B1fLsk$#b^q()?sD=v|xWYqm=?A=-zJ-T)$B}IX&6+@WoR;HDpM< zs0`+=1^V&RK78^ra|TlEcx2<3QE1PvybTVXTwDHp=~G%{-ccL-sjHR>?E?!{d>*nb zTZ5nEE`CYIqLE-DWZu47#S^t3H^wKYRvQ(MURCOIeN;2b8uvI%BjkRpS4oEOX-@P& zsjc(urx}nsv)*$sal|EOXQRI55v9VvN9!j$();?O3G;uQBX-3%2D&f)2dK!nCAv^~ zQ?`rm7XleXufYS*T&}E`r~~9`$Xj`aLz~$+)7*HrrDgZsuAkSJ&e7`gsIv*L$KEL8 z9>h)B7vGW1G+7(Dkv#z^v<)r2km39z;~wX3n~|%cUD->r>8(_K7$%YNNLZ9Hio!)@p0WrakdQ(g zhcR@g2uj;*?%?iU>c3~ps??A@w-E`|)P-V$vg$iG3)0MseP2dPzj=CQih1OL=rB_3 zWD9rz&AC^lk^ARuS8^5h@3`Z9vkXBfBVR!-#KWXB$mGlkdjotnvCckE&M>KxNBbjq z050!DOj3Ogc6)a}jIOJFWwW?2&hQ|o+gnh~IyLlOn1?Mk>Ak4$WL6x|JHqav1+`N+`mKE%ndPhd9q3*mAjtB8vL2=zSc>N7nkN!BqG7>oxjS$`wJ?? z)=8zhkM$l+8C;r*6HBhVFjQ%;UYLG|Eg7GAtuCj%GyY|bj~Dc~V(9ItKlcwYEqh8r zb=-NE46E<%-^2KeQs*nBi+*rvOwoWKg}jUo6_TVtr+58XLq)Dbmn+4H(11j~A04-9 z0r#M-cb9Koa^kPeQ+lPjJyjuE#?6)BlbJHGABD;R=8r1%(^B057+ml}p(%2*P&vPH zt+6?DKQ($9=W_Jx>AQoU#g*u}>Tmfw{WI*zfARD$m=g)j3f0{Gt+nQnTMr|~eL^%V zEyqK0{ncEIjbr-K)q8#QQQ>{uRQ)q`UQ1R!K_EDDfTQ#&bGno`v3+$J#%>MlUeF(Bhg|Lwp+pQ)hz%>DhmGOxUyE)s6%Pg?xnxn+pLeKT7pxGOBc=rdHa1zA23{^B|ECnPR^crb*$F6!6C`( zi3z`%P}4nELivjm(oe!tS@W+Owhuf4z8A$RZqk63 z**A=6S?)-P9OVyypm#O25P%e_qGXFh3bA3!@t@7~E;G}1@&UMj-MjHm;U|Y;{!lM2 zmhWN4(GBjCd1>^_xbAm+`jLiFSv<(Fb*P_vJF^4sP=EYM;}fojuSYcG6db`21TH@e z^tvB9d)pa%TrhC5Bh(Cwfo$C|r<(8f_C6;pxJz7SM;NQU3{etNt5LUlZ0!+aiDF*K zBSLxMfa*t$RCO(?ioK&XKQ^2%ZWhhfkI&N2U(+e)x}0eev8FrbMgv-7-w36}kaCEU z^eB|&5)3cVVyKo-ewengPz7FRGGERwvu#$AVTnORI|>R%#G<5ndag5N^RNN4jJ{SE z1=TZ_n2fZS4m}9m3lCT|J-Ia57a?V4_^|AAIj`Il<$Y{sw~-C|arwK#@bjE1DgDu( zp|}K}9%x(2nho9>9*!RBMfnM#744rdQ%+vt^LVRD7SGMqIj;=vgN>2XU}JDoLtoXu zlNY}JGjlsXvi?Zz56pP{lNp`$1@rJUqx&E2% zi4;@Zi?b6#ii%#PFBKKPbJoVcnKQ=J9MmTca$Uj~%cB7ufxD$u%mI_5Eeq=zp>JA1 zg_bPOA4RW>IMu|y2J0($K0C)Qe&}*(8&_yR$CV@~FVRq^V6ZI=DRhbOxEP8d)~12D zk31|r&K%EG20^I#qK)0RJy)fKd3|sO+ah@h9f5;f@d16fiHx9ed&$&rEVIRl^MV1f zT&whT{)cdXf4JrBcSy)fgp{%2CD_HkC8OGGP@YBp*{?qzUnFnYB}0?;DNr-}!ss_r zva6qc=I_eBhhWL+iP2!hFc>-F;QnL+3GzEAjQ@gNzt%#cyWr_c6>TD;OlwCm_+;%@ z>K6$aFVtv2>+BmDdWVHo+&7yBWVpafYkg2{#RX97GjO(FbinePbZ;c3VBSR1_$Yp| z`YilIQ`Soz%cv++ah8Xt$N7tvYkEZf5`oM>bk5ns?&ld877T*TJ84eT^@e9}9Za$J zba_f{myBEqy*K`Y@y1D0gO1H)N?fJB8D!81Js{qW2@gImUa*Q{)G2m$s;TylxAb z>*Ji=!U5sUckCQJ(0(P%!s#7?Jc@|IuK8B!!x&=UxM83}i=jh>kU@xX0jLfICsY=b zp|qXzC=%MhoyZx`qQI|uW5%iAwfUcJ?>9reUUJ1NE|`pQ9+PPQ9PIjfp>MCC@d6n} zTl*Pw5BU4*cW_4ejP2{;r5oqU%MA_6xYt$^{r8l^y{=Xou%F3ucX`naf+LaaVhzU2 zuKHC$*gKoLC-mUEbevCS+u5Xhq(xgN68L4bURtT^bX}GvD%ZP#RJU`hiGdG}>nTc{ z{}Y%e(S+X8!1r4khNc`5Nc<1b)&EKgGK4<>AQ?elPOC#^luB_TloVd-AvnJ$8hb$o zMm~Vf(d+AG^2b7@6Yu^oVp)<)EX+Sj-geeOZ;XZ%GMp+-vSFg}Y|2Cylwi&=Fyc#4t($MCxJp^bdX#p zLD(j!-JL(yvR(ZtN-#z%5?ULof_?wWRSIrwqfE}|6h58(eS_%(=Un4uog{H-os=Ye zU@iF8(>pa(k?YS+2EsrWGv^-g>zO>}~-;E$xFw*OtAJCeg}j8rn*=J1}_jIi+FM5XSq#TXp%{$pXoD z;B&jC*PBVqvT=C4z&#ME*lysz29Hmnr746=73DAJ4#l7K0xww9PoQP7wH$|^fkJaX^hc=*~YUdoO`y=h{*y z+%EZe*4JE=nO_vMH^xB*9-mLs2XufkNW;Ni*x;(`nkq)iO}a|`!8mcX(6%%uJI2^> zjriqE;K!2pu#`c_!u12Cai;Zpn0P|B|00V(60uJN9fE$3kPih?@t34n;$&`LV&m7a zZRH8;nE0pdZ_SUgNI>h$=3d;rTE+1eqqsI-c1rIg%EEGdCgafM(l+dAK#)QSQ0oH^ zqHkh{fI=vBP?kdrV)srw8a3Wp^?F^TS?6(zay7fnWk?z0$FA1L`a(eXX6;+WuIkr2 zJXsQMbQ6k@=X=ERCb49zRG^&hGam6>Ic~0NVR-m;5BvYWL07|~W7{f%&U74<-WR{0 z81*>1n%*)YrUFT>e42R!+Al9xTB-;4yKCD9lt(zP`3hdciOPwy+!be~0j;oagb--0 z&qbMp53~cGVWKFM6v9Xkpl1rud5AGY^s(@5ebfJISjGS1aR>x;3Pw zL7~nzk|o=ik^;FLkVg7nZ2tc=jRy2FZ9@{(2K$Kzj*D@3~*jx@+B!XRiel!kWFG-~K;sPhw)Ai~wlE&d$-%5xPnP z0Ngg%`|?%y5HG;O@0v%5yZ2=gHTmNrsw#4NewRh`uJ{FdiYRF)0d}^whR_{s&>iOP zVcx2Ed1ZM;Jf1e&e+Ky1>c3z9Kd&Z1-oem6*`Xg<01$r-Ff(&-@bU@?31Ki&QgU+2 z%F62Mr%o9dn3$NHK5cJ*_Uu_A(c3#P@Y1DASFhf>6&;(^~G=d&= zgrRM>4bTT;Nbv(N)l^u4oVZ4B)ESF2c6-q;^hb&LMJJ?00+6F~OfQ5Urh59M`x~jQWvQ&zZ#^Tq z6X5(-SJ{OszY3{P-l7R5~ASA$v zV43LRjg!|cly%@R)x>n*0?gq4=b`a4k}V#g7^iZmbwAevwL zYyhKf53h&GJD}Z;;pPp8Dv|PwZn4XkfkdOb6_ZxiT9L#n8;kFf$~CZOYQY1;e|6G7 zEj{NhMg(Vh@ox*^D&>s7ys%%0?2bM2Piu4&MJLMeVO)hf@2u*~U(3kg)ahf(KA?pX zI>yp>Dli=@5nK$5DwH*~_;)@Y59~N1=m+ibaqqkqn|M1;(qb>~B$6vTHE?XX@6_LbeaHtR|YT&42 z05i^95bi{VE!Z)v6uMN38~-9AmbPGtMnB{SX?>Jh*LOEzv1ooiPf1qV(A0b!D$8%P_amkH)44I z5-m^hdd{vZIb9_$1^mUDCy6JP^=;>IFxe_hH{%MxV9&SD4&eHW%9Cy&wNW_|2LL)E zXcU6(Fee`K;JICQ0L7=n)I7@q zyA3G#eRmfpIstyafCS{GJ2#08?4R$vM+0IaaXe-k`KjTA5`cwI0$r*|f|PZs zZztI~_Q_=+2Z+_QdElojvcS}FUeG|_LPYOC$L@j;?}_0fJNs`elF?+jiU1tw z%dCLMI{$X-UxOu9!QeMiy7eY`BgXuNOe471IrR2o?=$KQs5#_VmZISgcqFEye-;<| zG9_d)RxrJj8(NrH{`AII^TL$u%XDkl%M%}ld_Zsu1VaEEYlQ&oy?3EaErhcx+~2@a zaHY25B1y)f)R__HgB|Bywv;|=oAV}9`e8}^@KfEGz;VN|SEW@e@(&DFhlBq_Qj=09 z!DUAPrjx}F2-x@b;}`sowpSILrvYK2qO!OLmrXdI2*UhX0Hg#`NI_TZf+*mEl==pH ztg|kYClzf5T11=tyJj-LXA09TX8NUh(fb@um!;DDr$J>$NN50$a>>a2RgLkE_t6bK+ z&n`&RlX&){jO5`>(wAympVNTkNg4M9KFP#$lKEgJ!+@k!y!vMk@*CZkPI2F9K>M!i zSa0G`BSm!B#v{DRX>?eX+Oiq|O{us!y*bCvgsc`&5s|cRXfFKPoo`lgV(;`Gj)$bh z)p^c0s;^$|f&7rX6jRWc@5>J}RHxd}?ov~wb&KC8b+4!9zQ0F((g@mGd;xtb`P(?N zzuDd7Z4$RwdE(%B_B6VM4~muX-lfWLHG*%S2rE-G;`8F`7<6xhh^h1nqS2y1rMr() ztdo&o;2s>s1WxYn(tu6?9QOsSmd!aRP?oC)uzE;mKRoN3GQf@A`x{&WC5!0;3q=@W zX4iT#=j?LN-(E`N?uOHeT5xV0?Z6Ug3!qJ6`w#NpnRI^*7BaOn7PWD1>JA$)Q#l)7&VTCEO zHTQA*HgKfKiTGD~`KvgcS$fn{gQq2^S1Mey@bP*r>SHF<9dY2-KVs{{DT)Uv`h3MV z4%d*v0}$NbrU4m8!4d^+XBE&>e1%R~5D@*Wf-z~fy~aKEhbNGOqEsLf=;_FCEf-^R z-Cj)tebms)k0YX~t>iUg@NdQhG8#sRO4dN%53|<%JJEa~C<%~>s0W}%h`w0qCc`Q? zZhW=aO$}O3C#pj44W6V{cK|=aghC5TShveca2td_QOwF&$bU2K`n1{#<67#+8Cl-4 zZ5%TnPwlGX>}(TtFuqRp`W~%s6*R~2jSnV1AEd8C*J(hHI(@!!;&-$Nh3vdfwvB5O zX-%a8*^_FMS$YT)#B|>I@+^AHd8CNFMwB~-XmdxR6_IE)S%TB*AbfkJckBS_^Kn5% zP{9t2qm~f-RfBkJN2XhDI@$rpG6r`T!IfSTqFz7{$4b4U6e#3)uS1il{M2%2g@Rqsj-yXOPcRUdRmQ`OA zdjc)Huyk~`j+e37>6&{^P5!@4_oCiPY&~ArgtIe_QZVZEnf71e0YE6q$ylc@{S*zz z3l>FSwkC=YSVTqrU0{;daVRg1LVm(*MBKtRxF{j)I`x|m#=K%=MY(qjfVJC=lIcO& z0o1lel6?fvv-@eN8Mb2*cAJCk6NK?xJ}MRW>dT-8uKP>-8S(H>DM2HK-r1d^qTV>o zMd7dN-MsH`fRF<7FSz#ZTgfga-nK{KPc5HBiPkw0Mo(LZc|@;)u4#My-XwrdO3M@v z9JyT4k&qzN#e^HbjHT!N+$&CgSLdv*) z)Or!i*~3$;;RSX3h!=*sq>_r7>&bpw2p-oyL&-4*Uj-zpx6(+l`ln9jm2U9K6n5+* z0nkcbRH`>{`TAZ}j}1Z`F~ff?%3*tDQhLpR|{W<}~1*6M&C zfn~`2MAP-EpHF3VUqo*A1Hc)#J2fbH0+~%XD))5Hel8&=Y22b;QO)#`0|*sq7CC?% zCY9M*&QEGyQx0EW*tf*0w$bZQb+YF!i08=OZw4ZJ}4^VfUCNb5*)1?{a>5`LIBM5=C6Go1s{!rEX;hT;_-b!O52C%%xiiaS~p( zACYC611V00{hzEd_r?P3?U8m+`hL~m>3ShB>72@|975|O8qhsh#%XD*SraZ<5iyNk z;QTp+fuWp!lqe9N>aBvva_+HFjEWfAcr%M$i1-<&({1)jJar)T z=I4b|oyKvePd%){mUz|`KS{f6FZ(Mz1yzd~M;bw83*2l0EnVa;=NPQ!D1EQC5g5)6 zik5F;`*@`jeMeg+W*w}3kDLGHg5k;Fzt~FfsjVsIPZ&3S4o^uB>^!0aC`&;{s$lIV z0om)HaX~f$m#or>7U>O|EV{-=qCT+bwcnVPq5(CeZSd>2(UG`9gcAPX0kL_6{V^I) z>#d?$9Uw391B2`b#Mk6Hs|yVz)x@S3PrGg&zjA(tXzKa!N@lws?scM91u_hr0T&2A z-L_;?deSZo!-j+|tj5_CCUWrc${&v=Bs^D8{&qADQ2}a$e3{L4d!MD|u!UFbL>t7f zK1a7jE4#N33%8+UPYJ4~{1(m5ob80Vq|x9VQ27Q}e`+=3?Qr9j?{#Jc4+!V2em9&t zeo~H~sA3i+?k_y=x*%+$y$n)ETXrI!t(Y^qgdBZB5rKdVECdb}3M-l= z0clZ3mci|cpY%;$Ykjpt3kD5$Bi?aNXUX|4dGqR9Kgzk$u$5Fn>B>7($QQlLX4~fq$PkefA>;9 zZ~UN62Yy$s&)cb@R}oc@d_U26VMa65)XIjkOzaXpiWJiZ=MSJL2EkKrFQ4aD*6ii#)o1`>M3b!jmDsJ4S(%Y5Bip9?u%M z%=A%B2^5psnQF#9-rg_Zzv1-GuC~8u`ge=@Zlxp13`#B1fLsk$#b^q()?sD=v|xWYqm=?A=-zJ-T)$B}IX&6+@WoR;HDpM< zs0`+=1^V&RK78^ra|TlEcx2<3QE1PvybTVXTwDHp=~G%{-ccL-sjHR>?E?!{d>*nb zTZ5nEE`CYIqLE-DWZu47#S^t3H^wKYRvQ(MURCOIeN;2b8uvI%BjkRpS4oEOX-@P& zsjc(urx}nsv)*$sal|EOXQRI55v9VvN9!j$();?O3G;uQBX-3%2D&f)2dK!nCAv^~ zQ?`rm7XleXufYS*T&}E`r~~9`$Xj`aLz~$+)7*HrrDgZsuAkSJ&e7`gsIv*L$KEL8 z9>h)B7vGW1G+7(Dkv#z^v<)r2km39z;~wX3n~|%cUD->r>8(_K7$%YNNLZ9Hio!)@p0WrakdQ(g zhcR@g2uj;*?%?iU>c3~ps??A@w-E`|)P-V$vg$iG3)0MseP2dPzj=CQih1OL=rB_3 zWD9rz&AC^lk^ARuS8^5h@3`Z9vkXBfBVR!-#KWXB$mGlkdjotnvCckE&M>KxNBbjq z050!DOj3Ogc6)a}jIOJFWwW?2&hQ|o+gnh~IyLlOn1?Mk>Ak4$WL6x|JHqav1+`N+`mKE%ndPhd9q3*mAjtB8vL2=zSc>N7nkN!BqG7>oxjS$`wJ?? z)=8zhkM$l+8C;r*6HBhVFjQ%;UYLG|Eg7GAtuCj%GyY|bj~Dc~V(9ItKlcwYEqh8r zb=-NE46E<%-^2KeQs*nBi+*rvOwoWKg}jUo6_TVtr+58XLq)Dbmn+4H(11j~A04-9 z0r#M-cb9Koa^kPeQ+lPjJyjuE#?6)BlbJHGABD;R=8r1%(^B057+ml}p(%2*P&vPH zt+6?DKQ($9=W_Jx>AQoU#g*u}>Tmfw{WI*zfARD$m=g)j3f0{Gt+nQnTMr|~eL^%V zEyqK0{ncEIjbr-K)q8#QQQ>{uRQ)q`UQ1R!K_EDDfTQ#&bGno`v3+$J#%>MlUeF(Bhg|Lwp+pQ)hz%>DhmGOxUyE)s6%Pg?xnxn+pLeKT7pxGOBc=rdHa1zA23{^B|ECnPR^crb*$F6!6C`( zi3z`%P}4nELivjm(oe!tS@W+Owhuf4z8A$RZqk63 z**A=6S?)-P9OVyypm#O25P%e_qGXFh3bA3!@t@7~E;G}1@&UMj-MjHm;U|Y;{!lM2 zmhWN4(GBjCd1>^_xbAm+`jLiFSv<(Fb*P_vJF^4sP=EYM;}fojuSYcG6db`21TH@e z^tvB9d)pa%TrhC5Bh(Cwfo$C|r<(8f_C6;pxJz7SM;NQU3{etNt5LUlZ0!+aiDF*K zBSLxMfa*t$RCO(?ioK&XKQ^2%ZWhhfkI&N2U(+e)x}0eev8FrbMgv-7-w36}kaCEU z^eB|&5)3cVVyKo-ewengPz7FRGGERwvu#$AVTnORI|>R%#G<5ndag5N^RNN4jJ{SE z1=TZ_n2fZS4m}9m3lCT|J-Ia57a?V4_^|AAIj`Il<$Y{sw~-C|arwK#@bjE1DgDu( zp|}K}9%x(2nho9>9*!RBMfnM#744rdQ%+vt^LVRD7SGMqIj;=vgN>2XU}JDoLtoXu zlNY}JGjlsXvi?Zz56pP{lNp`$1@rJUqx&E2% zi4;@Zi?b6#ii%#PFBKKPbJoVcnKQ=J9MmTca$Uj~%cB7ufxD$u%mI_5Eeq=zp>JA1 zg_bPOA4RW>IMu|y2J0($K0C)Qe&}*(8&_yR$CV@~FVRq^V6ZI=DRhbOxEP8d)~12D zk31|r&K%EG20^I#qK)0RJy)fKd3|sO+ah@h9f5;f@d16fiHx9ed&$&rEVIRl^MV1f zT&whT{)cdXf4JrBcSy)fgp{%2CD_HkC8OGGP@YBp*{?qzUnFnYB}0?;DNr-}!ss_r zva6qc=I_eBhhWL+iP2!hFc>-F;QnL+3GzEAjQ@gNzt%#cyWr_c6>TD;OlwCm_+;%@ z>K6$aFVtv2>+BmDdWVHo+&7yBWVpafYkg2{#RX97GjO(FbinePbZ;c3VBSR1_$Yp| z`YilIQ`Soz%cv++ah8Xt$N7tvYkEZf5`oM>bk5ns?&ld877T*TJ84eT^@e9}9Za$J zba_f{myBEqy*K`Y@y1D0gO1H)N?fJB8D!81Js{qW2@gImUa*Q{)G2m$s;TylxAb z>*Ji=!U5sUckCQJ(0(P%!s#7?Jc@|IuK8B!!x&=UxM83}i=jh>kU@xX0jLfICsY=b zp|qXzC=%MhoyZx`qQI|uW5%iAwfUcJ?>9reUUJ1NE|`pQ9+PPQ9PIjfp>MCC@d6n} zTl*Pw5BU4*cW_4ejP2{;r5oqU%MA_6xYt$^{r8l^y{=Xou%F3ucX`naf+LaaVhzU2 zuKHC$*gKoLC-mUEbevCS+u5Xhq(xgN68L4bURtT^bX}GvD%ZP#RJU`hiGdG}>nTc{ z{}Y%e(S+X8!1r4khNc`5Nc<1b)&EKgGK4<>AQ?elPOC#^luB_TloVd-AvnJ$8hb$o zMm~Vf(d+AG^2b7@6Yu^oVp)<)EX+Sj-geeOZ;XZ%GMp+-vSFg}Y|2Cylwi&=Fyc#4t($MCxJp^bdX#p zLD(j!-JL(yvR(ZtN-#z%5?ULof_?wWRSIrwqfE}|6h58(eS_%(=Un4uog{H-os=Ye zU@iF8(>pa(k?YS+2EsrWGv^-g>zO>}~-;E$xFw*OtAJCeg}j8rn*=J1}_jIi+FM5XSq#TXp%{$pXoD z;B&jC*PBVqvT=C4z&#ME*lysz29Hmnr746=73DAJ4#l7K0xww9PoQP7wH$|^fkJaX^hc=*~YUdoO`y=h{*y z+%EZe*4JE=nO_vMH^xB*9-mLs2XufkNW;Ni*x;(`nkq)iO}a|`!8mcX(6%%uJI2^> zjriqE;K!2pu#`c_!u12Cai;Zpn0P|B|00V(60uJN9fE$3kPih?@t34n;$&`LV&m7a zZRH8;nE0pdZ_SUgNI>h$=3d;rTE+1eqqsI-c1rIg%EEGdCgafM(l+dAK#)QSQ0oH^ zqHkh{fI=vBP?kdrV)srw8a3Wp^?F^TS?6(zay7fnWk?z0$FA1L`a(eXX6;+WuIkr2 zJXsQMbQ6k@=X=ERCb49zRG^&hGam6>Ic~0NVR-m;5BvYWL07|~W7{f%&U74<-WR{0 z81*>1n%*)YrUFT>e42R!+Al9xTB-;4yKCD9lt(zP`3hdciOPwy+!be~0j;oagb--0 z&qbMp53~cGVWKFM6v9Xkpl1rud5AGY^s(@5ebfJISjGS1aR>x;3Pw zL7~nzk|o=ik^;FLkVg7nZ2tc=jRy2FZ9@{(2K$Kzj* URL? { + if let path = Bundle.main.path(forResource: soundName, ofType: "mp3") { + return URL(fileURLWithPath: path) + } else { + return Bundle.mxk_audioURLFromMXKAssetsBundle(withName: soundName) + } + } } diff --git a/changelog.d/7504.change b/changelog.d/7504.change new file mode 100644 index 000000000..2fed9c438 --- /dev/null +++ b/changelog.d/7504.change @@ -0,0 +1 @@ +Add an audio alert when the voice broadcast recording is automatically paused From cc4a2cbca2d36b8cbbe176a6098c8c2df1ca6396 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 14:57:36 +0200 Subject: [PATCH 39/52] Fix partial text messages not being saved for each room with RTE enabled --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../MXKRoomInputToolbarView.h | 14 ++++++ .../MXKRoomInputToolbarView.m | 5 ++ Riot/Modules/Room/MXKRoomViewController.m | 2 +- Riot/Modules/Room/RoomViewController.m | 9 +++- .../WysiwygInputToolbarView.swift | 49 ++++++++++++++++--- .../MockComposerLinkActionScreenState.swift | 2 +- .../ComposerLinkActionViewModel.swift | 2 +- changelog.d/7535.bugfix | 1 + project.yml | 2 +- 10 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 changelog.d/7535.bugfix diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9870eb6da..bf7d208dd 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "758f226a92d6726ab626c1e78ecd183bdba77016", - "version" : "2.0.0" + "revision" : "ff5e8054da60212051cb0dec244500ca0f441bac", + "version" : "2.1.0" } }, { diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index abd67ec7e..b18e93690 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -213,6 +213,15 @@ typedef enum : NSUInteger */ - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView updateActivityIndicator:(BOOL)isAnimating; +/** + Tells the delegate that the partial content of the composer has changed + and should be stored to allow restoring it later if needed. + + @param toolbarView the room input toolbar view + @param partialAttributedTextMessage the partial content to store + */ +- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView shouldStorePartialContent:(NSAttributedString*)partialAttributedTextMessage; + @end /** @@ -390,6 +399,11 @@ typedef enum : NSUInteger */ @property (nonatomic) NSAttributedString *attributedTextMessage; +/** + Sets the partial text message to apply to the current message composer. + */ +- (void)setPartialContent:(NSAttributedString *)attributedTextMessage; + /** Default font for the message composer. */ diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m index d05cd9f53..b5b15d4b8 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -1405,4 +1405,9 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes) return NO; } +- (void)setPartialContent:(NSAttributedString *)attributedTextMessage +{ + self.attributedTextMessage = attributedTextMessage; +} + @end diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 2e55c4771..b1a1bc18b 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -360,7 +360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; { // Retrieve the potential message partially typed during last room display. // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) - inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; + [inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage]; } if (!hasAppearedOnce) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a18de9aa1..cb3214684 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -693,7 +693,7 @@ static CGSize kThreadListBarButtonItemImageSize; { // Retrieve the potential message partially typed during last room display. // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) - self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage; + [self.inputToolbarView setPartialContent:self.roomDataSource.partialAttributedTextMessage]; } [self setMaximisedToolbarIsHiddenIfNeeded: NO]; @@ -5293,6 +5293,11 @@ static CGSize kThreadListBarButtonItemImageSize; }]; } +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage +{ + self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage; +} + #pragma mark - MXKRoomMemberDetailsViewControllerDelegate - (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion @@ -6135,7 +6140,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (self.saveProgressTextInput) { // Restore the potential message partially typed before jump to last unread messages. - self.inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; + [self.inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage]; } }; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index ad488897f..8a5c75618 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -96,11 +96,17 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp // Note: this is only interactive in plain text mode. If RTE is enabled, // APIs from the composer view model should be used. get { - guard !self.textFormattingEnabled else { return nil } + guard !self.textFormattingEnabled else { + MXLog.failure("[WysiwygInputToolbarView] Trying to get attributedTextMessage in RTE mode") + return nil + } return self.wysiwygViewModel.textView.attributedText } set { - guard !self.textFormattingEnabled else { return } + guard !self.textFormattingEnabled else { + MXLog.failure("[WysiwygInputToolbarView] Trying to set attributedTextMessage in RTE mode") + return + } self.wysiwygViewModel.textView.attributedText = newValue } } @@ -174,6 +180,16 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp showKeyboard() } } + + override func setPartialContent(_ attributedTextMessage: NSAttributedString) { + let content: String + if #available(iOS 15.0, *) { + content = PillsFormatter.stringByReplacingPills(in: attributedTextMessage, mode: .markdown) + } else { + content = attributedTextMessage.string + } + self.wysiwygViewModel.setMarkdownContent(content) + } func showKeyboard() { self.viewModel.showKeyboard() @@ -191,7 +207,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } func mention(_ member: MXRoomMember) { - self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), + self.wysiwygViewModel.setMention(url: MXTools.permalinkToUser(withUserId: member.userId), name: member.displayname, mentionType: .user) } @@ -281,12 +297,31 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp }, wysiwygViewModel.$plainTextContent - .dropFirst() .removeDuplicates() - .sink { [weak self] value in - guard let self else { return } - self.textMessage = value.string + .dropFirst() + .sink { [weak self] attributed in + // Note: filter out `plainTextMode` being off, as switching to RTE will trigger this + // publisher with empty content. This avoids saving the partial text message + // or trying to compute suggestion from this empty content. + guard let self, wysiwygViewModel.plainTextMode else { return } + self.textMessage = attributed.string self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) + self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) + }, + + wysiwygViewModel.$attributedContent + .removeDuplicates(by: { + $0.text == $1.text + }) + .dropFirst() + .sink { [weak self] _ in + // Note: filter out `plainTextMode` being on, as switching to plain text mode will trigger this + // publisher with empty content. This avoids saving the partial text message + // or trying to compute suggestion from this empty content. + guard let self, !self.wysiwygViewModel.plainTextMode else { return } + let markdown = self.wysiwygViewModel.content.markdown + let attributed = NSAttributedString(string: markdown, attributes: [.font: self.defaultFont]) + self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) } ] diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift index 6bdc5ebc5..335ff3196 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift @@ -33,7 +33,7 @@ enum MockComposerLinkActionScreenState: MockScreenState, CaseIterable { case .create: viewModel = .init(from: .create) case .edit: - viewModel = .init(from: .edit(link: "https://element.io")) + viewModel = .init(from: .edit(url: "https://element.io")) } return ( [viewModel], diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift index 367417282..d16dd7212 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift @@ -36,7 +36,7 @@ final class ComposerLinkActionViewModel: ComposerLinkActionViewModelType, Compos switch linkAction { case let .edit(link): initialViewState = .init( - linkAction: .edit(link: link), + linkAction: .edit(url: link), bindings: .init( text: "", linkUrl: link diff --git a/changelog.d/7535.bugfix b/changelog.d/7535.bugfix new file mode 100644 index 000000000..f21ab863c --- /dev/null +++ b/changelog.d/7535.bugfix @@ -0,0 +1 @@ +Labs: Rich Text Editor: Fix partial text messages not being saved for each room diff --git a/project.yml b/project.yml index 6a207706d..3922de651 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 2.0.0 + version: 2.1.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 098b68facfb1c29abe14528909e4d03e4b2810d3 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 15:49:58 +0200 Subject: [PATCH 40/52] Fix composer unit tests --- .../Test/Unit/ComposerLinkActionViewModelTests.swift | 6 +++--- .../Room/Composer/Test/Unit/ComposerViewModelTests.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift index 2407eccc4..3fbb8d564 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift @@ -54,7 +54,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { func testEditDefaultState() { let link = "element.io" - setUp(with: .edit(link: link)) + setUp(with: .edit(url: link)) XCTAssertEqual(context.viewState.bindings.text, "") XCTAssertEqual(context.viewState.bindings.linkUrl, link) XCTAssertTrue(context.viewState.isSaveButtonDisabled) @@ -83,7 +83,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testRemoveAction() { - setUp(with: .edit(link: "element.io")) + setUp(with: .edit(url: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value @@ -119,7 +119,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testSaveActionForEdit() { - setUp(with: .edit(link: "element.io")) + setUp(with: .edit(url: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift index e4d5b595d..c68cd7783 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift @@ -98,7 +98,7 @@ final class ComposerViewModelTests: XCTestCase { XCTAssertEqual(result, .linkTapped(LinkAction: .createWithText)) context.send(viewAction: .linkTapped(linkAction: .create)) XCTAssertEqual(result, .linkTapped(LinkAction: .create)) - context.send(viewAction: .linkTapped(linkAction: .edit(link: "https://element.io"))) - XCTAssertEqual(result, .linkTapped(LinkAction: .edit(link: "https://element.io"))) + context.send(viewAction: .linkTapped(linkAction: .edit(url: "https://element.io"))) + XCTAssertEqual(result, .linkTapped(LinkAction: .edit(url: "https://element.io"))) } } From 5b2944c9187628027e1dcfc0367672b7307f80d7 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 17:26:54 +0200 Subject: [PATCH 41/52] Add missing self in closure --- .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 8a5c75618..edd951fd6 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -303,7 +303,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp // Note: filter out `plainTextMode` being off, as switching to RTE will trigger this // publisher with empty content. This avoids saving the partial text message // or trying to compute suggestion from this empty content. - guard let self, wysiwygViewModel.plainTextMode else { return } + guard let self, self.wysiwygViewModel.plainTextMode else { return } self.textMessage = attributed.string self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) From fc59290a34971bb4996e5f5e212c0885d8631a12 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 10:35:55 +0200 Subject: [PATCH 42/52] Add logs to track a problem with the top left avatar disappearing --- Riot/Modules/Common/Avatar/AvatarView.swift | 9 +++++++++ Riot/Modules/Home/AllChats/AllChatsCoordinator.swift | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/Riot/Modules/Common/Avatar/AvatarView.swift b/Riot/Modules/Common/Avatar/AvatarView.swift index a3f46a5aa..a1ead57db 100644 --- a/Riot/Modules/Common/Avatar/AvatarView.swift +++ b/Riot/Modules/Common/Avatar/AvatarView.swift @@ -103,12 +103,17 @@ class AvatarView: UIView, Themable { func updateAvatarImageView(with viewData: AvatarViewDataProtocol) { guard let avatarImageView = self.avatarImageView else { + MXLog.warning("[AvatarView] avatar not updated because avatarImageView is nil.") return } let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill) updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) + if defaultAvatarImage == nil { + MXLog.warning("[AvatarView] defaultAvatarImage is nil") + } + if let avatarUrl = viewData.avatarUrl { avatarImageView.setImageURI(avatarUrl, withType: nil, @@ -118,6 +123,10 @@ class AvatarView: UIView, Themable { previewImage: defaultAvatarImage, mediaManager: viewData.mediaManager) updateAvatarContentMode(contentMode: .scaleAspectFill) + + if avatarImageView.frame.size.width < 8 || avatarImageView.frame.size.height < 8 { + MXLog.warning("[AvatarView] small avatarImageView frame: \(avatarImageView.frame)") + } } else { updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) } diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index 89c8edc2c..8765adb05 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -387,7 +387,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { } private func updateAvatarButtonItem() { + MXLog.info("[AllChatsCoordinator] updating avatar button item.") if let avatar = userAvatarViewData(from: currentMatrixSession) { + if avatarMenuView == nil { + MXLog.warning("[AllChatsCoordinator] updateAvatarButtonItem: avatarMenuView is nil.") + } avatarMenuView?.fill(with: avatar) avatarMenuButton?.setImage(nil, for: .normal) } else { From 05f89f11600af62f3f1c56cf6f8e2df6a601fdd5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 9 May 2023 17:05:27 +0100 Subject: [PATCH 43/52] Use the app's language for accessibility. --- Riot/Modules/Application/LegacyAppDelegate.m | 1 + Riot/Modules/Settings/SettingsViewController.m | 1 + changelog.d/pr-7493.bugfix | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/pr-7493.bugfix diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 8678f5ab8..ab17eeac5 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -390,6 +390,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } [NSBundle mxk_setLanguage:language]; [NSBundle mxk_setFallbackLanguage:@"en"]; + UIApplication.sharedApplication.accessibilityLanguage = language; if (BuildSettings.disableRightToLeftLayout) { diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 055841f3f..be87bea3b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -4158,6 +4158,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> || (language == nil && [NSBundle mxk_language])) { [NSBundle mxk_setLanguage:language]; + UIApplication.sharedApplication.accessibilityLanguage = language; // Store user settings NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; diff --git a/changelog.d/pr-7493.bugfix b/changelog.d/pr-7493.bugfix new file mode 100644 index 000000000..b486878b5 --- /dev/null +++ b/changelog.d/pr-7493.bugfix @@ -0,0 +1 @@ +Make sure to use the chosen language for the VoiceOver voice too. From 68029d6b896abf67de687b049a0273ca65cc208b Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 9 May 2023 09:53:42 +0200 Subject: [PATCH 44/52] Feat: add a flag in the build settings to force the user to define a homeserver. --- Config/BuildSettings.swift | 9 +++++++-- .../AuthenticationCoordinator.swift | 14 ++++++++++++-- .../Legacy/AuthenticationViewController.m | 18 ++++++++++++++++-- .../Common/AuthenticationModels.swift | 4 ++++ ...henticationServerSelectionCoordinator.swift | 8 +++++++- changelog.d/pr-7541.change | 1 + 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 changelog.d/pr-7541.change diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index e8c129619..1b2571708 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -98,10 +98,15 @@ final class BuildSettings: NSObject { // MARK: - Server configuration - // Default servers proposed on the authentication screen + // Force the user to set a homeserver instead of using the default one + static let forceHomeserverSelection = false + + // Default server proposed on the authentication screen static let serverConfigDefaultHomeserverUrlString = "https://matrix.org" - static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" + // Default identity server + static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" + static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify" diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index a245147cd..6d5250497 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -130,9 +130,19 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } let flow: AuthenticationFlow = initialScreen == .login ? .login : .register + + // Use the homeserver defined by a provisioningLink or by the user (if none is set, the default one will be used) + let homeserverAddress = authenticationService.provisioningLink?.homeserverUrl ?? authenticationService.state.homeserver.addressFromUser + + // Check if the user must select a server + if BuildSettings.forceHomeserverSelection, homeserverAddress == nil { + showServerSelectionScreen(for: flow) + return + } + do { - // Start the flow using the default server (or a provisioning link if set). - try await authenticationService.startFlow(flow) + // Start the flow (if homeserverAddress is nil, the default server will be used). + try await authenticationService.startFlow(flow, for: homeserverAddress) } catch { MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") showServerSelectionScreen(for: flow) diff --git a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m index 3f5cfaf67..c0605d813 100644 --- a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m @@ -132,7 +132,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; target:self action:@selector(onButtonPressed:)]; - self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString; + if (BuildSettings.forceHomeserverSelection) + { + self.defaultHomeServerUrl = nil; + } + else + { + self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString; + } self.defaultIdentityServerUrl = RiotSettings.shared.identityServerUrlString; @@ -1207,7 +1214,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; [self saveCustomServerInputs]; // Restore default configuration - [self setHomeServerTextFieldText:self.defaultHomeServerUrl]; + if (BuildSettings.forceHomeserverSelection) + { + [self setHomeServerTextFieldText:nil]; + } + else + { + [self setHomeServerTextFieldText:self.defaultHomeServerUrl]; + } [self setIdentityServerTextFieldText:self.defaultIdentityServerUrl]; [self.customServersTickButton setImage:AssetImages.selectionUntick.image forState:UIControlStateNormal]; diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift index e59aa0189..34d7adb90 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift @@ -86,6 +86,10 @@ class HomeserverAddress: NSObject { /// - Ensure the address contains a scheme, otherwise make it `https`. /// - Remove any trailing slashes. static func sanitized(_ address: String) -> String { + guard !address.isEmpty else { + // prevent prefixing an empty string with "https:" + return address + } var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if !address.contains("://") { diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index c5d521701..13308c262 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -57,7 +57,13 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { self.parameters = parameters let homeserver = parameters.authenticationService.state.homeserver - let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.displayableAddress, + let homeserverAddress: String + if BuildSettings.forceHomeserverSelection, homeserver.addressFromUser == nil { + homeserverAddress = "" + } else { + homeserverAddress = homeserver.displayableAddress + } + let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserverAddress, flow: parameters.authenticationService.state.flow, hasModalPresentation: parameters.hasModalPresentation) let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context) diff --git a/changelog.d/pr-7541.change b/changelog.d/pr-7541.change new file mode 100644 index 000000000..0e0c71fa6 --- /dev/null +++ b/changelog.d/pr-7541.change @@ -0,0 +1 @@ +Add a flag in the build settings to force the user to define a homeserver instead of using the default one. From 7d3efda5efe1678669faa983b5f385a46727b4ca Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 9 May 2023 19:12:30 +0100 Subject: [PATCH 45/52] Fix voiceover order of room creation header and message composer. --- .../RoomCreationIntroCell.swift | 5 +++++ .../RoomCreationIntroCellContentView.swift | 2 ++ .../Views/InputToolbar/RoomInputToolbarView.m | 17 +++++++++++++++++ changelog.d/pr-7543.bugfix | 1 + 4 files changed, 25 insertions(+) create mode 100644 changelog.d/pr-7543.bugfix diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift index 9bccbddf9..33a9c3d2b 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift @@ -164,6 +164,11 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell { roomCellContentView.didTapAddParticipants = { [weak self] in self?.notifyDelegate(with: RoomCreationIntroCell.tapOnAddParticipants) } + + self.accessibilityElements = [roomCellContentView.roomAvatarView as Any, + roomCellContentView.titleLabel as Any, + roomCellContentView.informationLabel as Any, + roomCellContentView.addParticipantsContainerView as Any] } diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift index 809cf4676..e664c66ab 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift @@ -69,8 +69,10 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable { self.addParticipantsButton.layer.masksToBounds = true self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside) + self.addParticipantsButton.accessibilityLabel = VectorL10n.roomIntroCellAddParticipantsAction self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction + self.addParticipantsLabel.isAccessibilityElement = false self.roomAvatarView.showCameraBadgeOnFallbackImage = true } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 2cead382a..0d058fefc 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -70,6 +70,8 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; _sendMode = RoomInputToolbarViewSendModeSend; self.inputContextViewHeightConstraint.constant = 0; + self.inputContextLabel.isAccessibilityElement = NO; + self.inputContextButton.isAccessibilityElement = NO; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateNormal]; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted]; @@ -252,6 +254,10 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; break; } + // Hide the context items from VoiceOver when the context view is "hidden". + self.inputContextLabel.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0; + self.inputContextButton.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0; + [self.rightInputToolbarButton setImage:buttonImage forState:UIControlStateNormal]; if (self.maxHeight && updatedHeight > self.maxHeight) @@ -477,11 +483,22 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor], [self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor], [self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]]; + + // The voice message toolbar is taller than the input toolbar so the record button is read + // out before the other subviews. Fix this by manually adding the elements in the right order. + self.accessibilityElements = @[self.attachMediaButton, + self.actionsBar, + self.inputContextLabel, + self.inputContextButton, + self.textView, + self.rightInputToolbarButton, + self.voiceMessageToolbarView]; } else { [self.voiceMessageToolbarView removeFromSuperview]; _voiceMessageToolbarView = nil; + self.accessibilityElements = nil; } } @end diff --git a/changelog.d/pr-7543.bugfix b/changelog.d/pr-7543.bugfix new file mode 100644 index 000000000..6a56590cd --- /dev/null +++ b/changelog.d/pr-7543.bugfix @@ -0,0 +1 @@ +Fix voiceover order of room creation header and message composer. From 40c57f79681a8a9ff736377a80818ff871f23ec8 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 11 May 2023 09:45:04 +0200 Subject: [PATCH 46/52] Fix: apply the changes requested in the PR review --- Config/BuildSettings.swift | 6 +++--- .../Modules/Authentication/AuthenticationCoordinator.swift | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 1b2571708..f58f969c1 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -98,13 +98,13 @@ final class BuildSettings: NSObject { // MARK: - Server configuration - // Force the user to set a homeserver instead of using the default one + /// Force the user to set a homeserver instead of using the default one static let forceHomeserverSelection = false - // Default server proposed on the authentication screen + /// Default server proposed on the authentication screen static let serverConfigDefaultHomeserverUrlString = "https://matrix.org" - // Default identity server + /// Default identity server static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify" diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 6d5250497..295a591f7 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -131,18 +131,15 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc let flow: AuthenticationFlow = initialScreen == .login ? .login : .register - // Use the homeserver defined by a provisioningLink or by the user (if none is set, the default one will be used) - let homeserverAddress = authenticationService.provisioningLink?.homeserverUrl ?? authenticationService.state.homeserver.addressFromUser - // Check if the user must select a server - if BuildSettings.forceHomeserverSelection, homeserverAddress == nil { + if BuildSettings.forceHomeserverSelection, authenticationService.provisioningLink?.homeserverUrl == nil { showServerSelectionScreen(for: flow) return } do { // Start the flow (if homeserverAddress is nil, the default server will be used). - try await authenticationService.startFlow(flow, for: homeserverAddress) + try await authenticationService.startFlow(flow) } catch { MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") showServerSelectionScreen(for: flow) From c843eafe843f9e02abb9ff7df253b08ad8a52107 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 11 May 2023 18:15:35 +0200 Subject: [PATCH 47/52] Fix: text color of the last event description was incorrect. --- .../Categories/NSAttributedString+Theme.swift | 64 +++++++++++++++++++ Riot/Managers/Theme/ThemeService.swift | 2 +- .../Recents/Views/RecentTableViewCell.m | 3 +- Riot/Utils/EventFormatter.m | 20 +++++- Riot/Utils/ThemeColorResolver.swift | 48 ++++++++++++++ changelog.d/pr-7545.bugfix | 1 + 6 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 Riot/Categories/NSAttributedString+Theme.swift create mode 100644 Riot/Utils/ThemeColorResolver.swift create mode 100644 changelog.d/pr-7545.bugfix diff --git a/Riot/Categories/NSAttributedString+Theme.swift b/Riot/Categories/NSAttributedString+Theme.swift new file mode 100644 index 000000000..9a0e01c93 --- /dev/null +++ b/Riot/Categories/NSAttributedString+Theme.swift @@ -0,0 +1,64 @@ +// +// 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 + +/// Custom NSAttributedString.Key to specify the theme +let themeIdentifierAttributeName = NSAttributedString.Key("ThemeIdentifier") +/// Custom NSAttributedString.Key to specify a theme color by its name +let themeColorNameAttributeName = NSAttributedString.Key("ThemeColorName") + +extension NSAttributedString { + /// Fix foreground color attributes if this attributed string contains the `themeIdentifierAttributeName` and `foregroundColorNameAttributeName` attributes + /// - Returns: a new attributed string with updated colors + @objc func fixForegroundColor() -> NSAttributedString { + let activeTheme = ThemeService.shared().theme + + // Check if a theme is defined for this attributed string + var needUpdate = false + self.vc_enumerateAttribute(themeIdentifierAttributeName) { (themeIdentifier: String, range: NSRange, _) in + needUpdate = themeIdentifier != activeTheme.identifier + } + + guard needUpdate else { + return self + } + + // Build a new attributedString with the proper colors if possible + let mutableAttributedString = NSMutableAttributedString(attributedString: self) + mutableAttributedString.vc_enumerateAttribute(themeColorNameAttributeName) { (colorName: String, range: NSRange, _) in + if let color = ThemeColorResolver.getColorByName(colorName) { + mutableAttributedString.addAttribute(.foregroundColor, value: color, range: range) + } + } + return mutableAttributedString + } +} + +extension NSMutableAttributedString { + /// Adds a theme color name attribute + /// - Parameters: + /// - colorName: color name + /// - range:range for this attribute + @objc func addThemeColorNameAttribute(_ colorName: String, range: NSRange) { + self.addAttribute(themeColorNameAttributeName, value: colorName, range: range) + } + + /// Adds a theme identifier attribute + @objc func addThemeIdentifierAttribute() { + self.addAttribute(themeIdentifierAttributeName, value: ThemeService.shared().theme.identifier, range: .init(location: 0, length: length)) + } +} diff --git a/Riot/Managers/Theme/ThemeService.swift b/Riot/Managers/Theme/ThemeService.swift index 209812111..3ce421d01 100644 --- a/Riot/Managers/Theme/ThemeService.swift +++ b/Riot/Managers/Theme/ThemeService.swift @@ -23,5 +23,5 @@ extension ThemeService { return nil } return ThemeIdentifier(rawValue: themeId) - } + } } diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index a21996333..afd3f5c88 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -81,7 +81,8 @@ // Manage lastEventAttributedTextMessage optional property if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)]) { - self.lastEventDescription.attributedText = roomCellData.lastEventAttributedTextMessage; + // Attempt to correct the attributed string colors to match the current theme + self.lastEventDescription.attributedText = [roomCellData.lastEventAttributedTextMessage fixForegroundColor]; } else { diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 278d902a5..d25655a22 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -573,8 +573,13 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent { // 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)]; + NSRange range = NSMakeRange(0, lastEventDescription.length); + [lastEventDescription addAttribute:NSForegroundColorAttributeName + value:ThemeService.shared.theme.colors.secondaryContent + range:range]; + [lastEventDescription addThemeColorNameAttribute:@"secondaryContent" range:range]; + [lastEventDescription addThemeIdentifierAttribute]; + summary.lastMessage.attributedText = lastEventDescription; } @@ -670,9 +675,11 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent NSAttributedString *attachmentString = nil; UIColor *textColor; + NSString *colorIdentifier; if (isStoppedVoiceBroadcast) { - textColor = ThemeService.shared.theme.textSecondaryColor; + textColor = ThemeService.shared.theme.colors.secondaryContent; + colorIdentifier = @"secondaryContent"; NSString *senderDisplayName; if ([stateEvent.stateKey isEqualToString:session.myUser.userId]) { @@ -688,6 +695,7 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent else { textColor = ThemeService.shared.theme.colors.alert; + colorIdentifier = @"alert"; UIImage *liveImage = AssetImages.voiceBroadcastLive.image; NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; @@ -717,6 +725,12 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent } [lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)]; + if (colorIdentifier) + { + [lastMessage addThemeColorNameAttribute:colorIdentifier range:NSMakeRange(0, lastMessage.length)]; + [lastMessage addThemeIdentifierAttribute]; + } + summary.lastMessage.attributedText = lastMessage; return YES; diff --git a/Riot/Utils/ThemeColorResolver.swift b/Riot/Utils/ThemeColorResolver.swift new file mode 100644 index 000000000..c0010c97d --- /dev/null +++ b/Riot/Utils/ThemeColorResolver.swift @@ -0,0 +1,48 @@ +// +// 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 + +/// Utility struct to get a theme color by its name +struct ThemeColorResolver { + private static var theme: Theme? + private static var colorsTable: [String: UIColor] = [:] + private static let queue = DispatchQueue(label: "io.element.ThemeColorResolver.queue", qos: .userInteractive) + + private static func setTheme(theme: Theme) { + queue.sync { + guard self.theme?.identifier != theme.identifier else { + return + } + self.theme = theme + colorsTable = [:] + let mirror = Mirror(reflecting: theme.colors) + for child in mirror.children { + if let colorName = child.label { + colorsTable[colorName] = child.value as? UIColor + } + } + } + } + + /// Finds a color by its name in the current theme colors + /// - Parameter name: color name + /// - Returns: the corresponding color or nil + static func getColorByName(_ name: String) -> UIColor? { + setTheme(theme: ThemeService.shared().theme) + return colorsTable[name] + } +} diff --git a/changelog.d/pr-7545.bugfix b/changelog.d/pr-7545.bugfix new file mode 100644 index 000000000..a2f30eb67 --- /dev/null +++ b/changelog.d/pr-7545.bugfix @@ -0,0 +1 @@ +Fix: The last event description text color now matches the active theme. From 401d6a59cfe93934a2dbc702f21fa7823527a22f Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 12 May 2023 18:30:20 +0200 Subject: [PATCH 48/52] Disable removing mention/command text trigger with RTE enabled --- Riot/Modules/Room/RoomViewController.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index cc108baa3..17253279b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8147,6 +8147,14 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)removeTriggerTextFromComposer:(NSString *)textTrigger { RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; + Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass]; + + // RTE handles removing the text trigger by itself. + if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && RiotSettings.shared.enableWysiwygTextFormatting) + { + return; + } + if (toolbar && textTrigger.length) { NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage]; [[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger From f6f172c5f2f8a01c10d969f742af681a77c239ae Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 12 May 2023 18:34:32 +0200 Subject: [PATCH 49/52] Fix mention pills display in thread list --- .../Views/Cell/ThreadTableViewCell.swift | 8 ++-- .../Views/Cell/ThreadTableViewCell.xib | 37 ++++++++++--------- changelog.d/7322.bugfix | 1 + 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 changelog.d/7322.bugfix diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index b99eec0c3..be4ed85dc 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -35,7 +35,7 @@ class ThreadTableViewCell: UITableViewCell { @IBOutlet private weak var rootMessageAvatarView: UserAvatarView! @IBOutlet private weak var rootMessageSenderLabel: UILabel! - @IBOutlet private weak var rootMessageContentLabel: UILabel! + @IBOutlet private weak var rootMessageContentTextView: UITextView! @IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var summaryView: ThreadSummaryView! @IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView! @@ -61,7 +61,7 @@ class ThreadTableViewCell: UITableViewCell { if let rootMessageText = model.rootMessageText { updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor) } else { - rootMessageContentLabel.attributedText = nil + rootMessageContentTextView.attributedText = nil } lastMessageTimeLabel.text = model.lastMessageTime if let summaryModel = model.summaryModel { @@ -83,7 +83,7 @@ class ThreadTableViewCell: UITableViewCell { mutable.addAttributes([ .foregroundColor: color ], range: NSRange(location: 0, length: mutable.length)) - rootMessageContentLabel.attributedText = mutable + rootMessageContentTextView.attributedText = mutable } } @@ -97,7 +97,7 @@ extension ThreadTableViewCell: Themable { Self.usernameColorGenerator.update(theme: theme) updateRootMessageSenderColor() rootMessageAvatarView.backgroundColor = .clear - if let attributedText = rootMessageContentLabel.attributedText { + if let attributedText = rootMessageContentTextView.attributedText { updateRootMessageContentAttributes(attributedText, color: rootMessageColor) } lastMessageTimeLabel.textColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib index f9c881396..3014cd711 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -11,14 +11,14 @@ - + - + @@ -27,7 +27,7 @@ - + @@ -51,13 +51,13 @@ - - + + + @@ -68,20 +68,20 @@ - + - - + - + + @@ -89,7 +89,7 @@ - + @@ -97,6 +97,9 @@ + + + diff --git a/changelog.d/7322.bugfix b/changelog.d/7322.bugfix new file mode 100644 index 000000000..b13925fa3 --- /dev/null +++ b/changelog.d/7322.bugfix @@ -0,0 +1 @@ +Fix mention pills display in thread list From 3006c3ae5c20591f6402915e45444eab19f91500 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 15:46:01 +0300 Subject: [PATCH 50/52] changelog.d: Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). --- 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 f4cbeda08..52ce26306 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.26.9' +$matrixSDKVersion = '= 0.26.10' # $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..a52d29522 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). \ No newline at end of file From 49254e6b89c491cd084fa15f1459fbc6bf3db5c7 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 15:46:02 +0300 Subject: [PATCH 51/52] version++ --- CHANGES.md | 31 +++++++++++++++++++++++++++++++ changelog.d/7322.bugfix | 1 - changelog.d/7493.feature | 1 - changelog.d/7497.bugfix | 1 - changelog.d/7504.change | 1 - changelog.d/7517.change | 1 - changelog.d/7523.bugfix | 1 - changelog.d/7526.bugfix | 1 - changelog.d/7530.bugfix | 1 - changelog.d/7535.bugfix | 1 - changelog.d/pr-7404.bugfix | 1 - changelog.d/pr-7493.bugfix | 1 - changelog.d/pr-7508.change | 1 - changelog.d/pr-7512.bugfix | 1 - changelog.d/pr-7521.bugfix | 1 - changelog.d/pr-7522.bugfix | 1 - changelog.d/pr-7541.change | 1 - changelog.d/pr-7543.bugfix | 1 - changelog.d/pr-7545.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 20 files changed, 31 insertions(+), 19 deletions(-) delete mode 100644 changelog.d/7322.bugfix delete mode 100644 changelog.d/7493.feature delete mode 100644 changelog.d/7497.bugfix delete mode 100644 changelog.d/7504.change delete mode 100644 changelog.d/7517.change delete mode 100644 changelog.d/7523.bugfix delete mode 100644 changelog.d/7526.bugfix delete mode 100644 changelog.d/7530.bugfix delete mode 100644 changelog.d/7535.bugfix delete mode 100644 changelog.d/pr-7404.bugfix delete mode 100644 changelog.d/pr-7493.bugfix delete mode 100644 changelog.d/pr-7508.change delete mode 100644 changelog.d/pr-7512.bugfix delete mode 100644 changelog.d/pr-7521.bugfix delete mode 100644 changelog.d/pr-7522.bugfix delete mode 100644 changelog.d/pr-7541.change delete mode 100644 changelog.d/pr-7543.bugfix delete mode 100644 changelog.d/pr-7545.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index 428bd30f7..7e942b526 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,34 @@ +## Changes in 1.10.12 (2023-05-16) + +✨ Features + +- Add composer suggestions for slash commands ([#7493](https://github.com/vector-im/element-ios/issues/7493)) + +🙌 Improvements + +- Crypto: Deprecate MXLegacyCrypto ([#7508](https://github.com/vector-im/element-ios/pull/7508)) +- Add a flag in the build settings to force the user to define a homeserver instead of using the default one. ([#7541](https://github.com/vector-im/element-ios/pull/7541)) +- Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). +- Add an audio alert when the voice broadcast recording is automatically paused ([#7504](https://github.com/vector-im/element-ios/issues/7504)) +- Timeline: Remove the matrix ID displayed when someone has changed its display name. ([#7517](https://github.com/vector-im/element-ios/issues/7517)) + +🐛 Bugfixes + +- Fix an issue where the Secrets Reset screen would open twice. ([#7404](https://github.com/vector-im/element-ios/pull/7404)) +- Make sure to use the chosen language for the VoiceOver voice too. ([#7493](https://github.com/vector-im/element-ios/pull/7493)) +- Fix the position of the send confirmation icon. ([#7512](https://github.com/vector-im/element-ios/pull/7512)) +- Disable accessibility for emojis during session verification. ([#7521](https://github.com/vector-im/element-ios/pull/7521)) +- Fix accessibility when entering the PIN to unlock the app. ([#7522](https://github.com/vector-im/element-ios/pull/7522)) +- Fix voiceover order of room creation header and message composer. ([#7543](https://github.com/vector-im/element-ios/pull/7543)) +- Fix: The last event description text color now matches the active theme. ([#7545](https://github.com/vector-im/element-ios/pull/7545)) +- Fix mention pills display in thread list ([#7322](https://github.com/vector-im/element-ios/issues/7322)) +- Poll: The timeline sometimes displayed closed polls in the wrong order. ([#7497](https://github.com/vector-im/element-ios/issues/7497)) +- Fix a flickering issue when the timeline datasource is reloaded. ([#7523](https://github.com/vector-im/element-ios/issues/7523)) +- Fix the position of the marker highlighting an event. ([#7526](https://github.com/vector-im/element-ios/issues/7526)) +- Fix application crashing when opening a thread with RTE enabled ([#7530](https://github.com/vector-im/element-ios/issues/7530)) +- Labs: Rich Text Editor: Fix partial text messages not being saved for each room ([#7535](https://github.com/vector-im/element-ios/issues/7535)) + + ## Changes in 1.10.11 (2023-04-18) 🙌 Improvements diff --git a/changelog.d/7322.bugfix b/changelog.d/7322.bugfix deleted file mode 100644 index b13925fa3..000000000 --- a/changelog.d/7322.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix mention pills display in thread list diff --git a/changelog.d/7493.feature b/changelog.d/7493.feature deleted file mode 100644 index 075a7f6a2..000000000 --- a/changelog.d/7493.feature +++ /dev/null @@ -1 +0,0 @@ -Add composer suggestions for slash commands diff --git a/changelog.d/7497.bugfix b/changelog.d/7497.bugfix deleted file mode 100644 index a8558b843..000000000 --- a/changelog.d/7497.bugfix +++ /dev/null @@ -1 +0,0 @@ -Poll: The timeline sometimes displayed closed polls in the wrong order. diff --git a/changelog.d/7504.change b/changelog.d/7504.change deleted file mode 100644 index 2fed9c438..000000000 --- a/changelog.d/7504.change +++ /dev/null @@ -1 +0,0 @@ -Add an audio alert when the voice broadcast recording is automatically paused diff --git a/changelog.d/7517.change b/changelog.d/7517.change deleted file mode 100644 index f43662947..000000000 --- a/changelog.d/7517.change +++ /dev/null @@ -1 +0,0 @@ -Timeline: Remove the matrix ID displayed when someone has changed its display name. diff --git a/changelog.d/7523.bugfix b/changelog.d/7523.bugfix deleted file mode 100644 index bc5cf31a7..000000000 --- a/changelog.d/7523.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a flickering issue when the timeline datasource is reloaded. diff --git a/changelog.d/7526.bugfix b/changelog.d/7526.bugfix deleted file mode 100644 index 7adb60cc0..000000000 --- a/changelog.d/7526.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the position of the marker highlighting an event. diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix deleted file mode 100644 index 5733a8d81..000000000 --- a/changelog.d/7530.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix application crashing when opening a thread with RTE enabled diff --git a/changelog.d/7535.bugfix b/changelog.d/7535.bugfix deleted file mode 100644 index f21ab863c..000000000 --- a/changelog.d/7535.bugfix +++ /dev/null @@ -1 +0,0 @@ -Labs: Rich Text Editor: Fix partial text messages not being saved for each room diff --git a/changelog.d/pr-7404.bugfix b/changelog.d/pr-7404.bugfix deleted file mode 100644 index 58609a160..000000000 --- a/changelog.d/pr-7404.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an issue where the Secrets Reset screen would open twice. diff --git a/changelog.d/pr-7493.bugfix b/changelog.d/pr-7493.bugfix deleted file mode 100644 index b486878b5..000000000 --- a/changelog.d/pr-7493.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make sure to use the chosen language for the VoiceOver voice too. diff --git a/changelog.d/pr-7508.change b/changelog.d/pr-7508.change deleted file mode 100644 index dbe206b34..000000000 --- a/changelog.d/pr-7508.change +++ /dev/null @@ -1 +0,0 @@ -Crypto: Deprecate MXLegacyCrypto diff --git a/changelog.d/pr-7512.bugfix b/changelog.d/pr-7512.bugfix deleted file mode 100644 index 1c6d3a98d..000000000 --- a/changelog.d/pr-7512.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the position of the send confirmation icon. diff --git a/changelog.d/pr-7521.bugfix b/changelog.d/pr-7521.bugfix deleted file mode 100644 index 3cedf12d4..000000000 --- a/changelog.d/pr-7521.bugfix +++ /dev/null @@ -1 +0,0 @@ -Disable accessibility for emojis during session verification. \ No newline at end of file diff --git a/changelog.d/pr-7522.bugfix b/changelog.d/pr-7522.bugfix deleted file mode 100644 index 0bd4e5b53..000000000 --- a/changelog.d/pr-7522.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix accessibility when entering the PIN to unlock the app. diff --git a/changelog.d/pr-7541.change b/changelog.d/pr-7541.change deleted file mode 100644 index 0e0c71fa6..000000000 --- a/changelog.d/pr-7541.change +++ /dev/null @@ -1 +0,0 @@ -Add a flag in the build settings to force the user to define a homeserver instead of using the default one. diff --git a/changelog.d/pr-7543.bugfix b/changelog.d/pr-7543.bugfix deleted file mode 100644 index 6a56590cd..000000000 --- a/changelog.d/pr-7543.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix voiceover order of room creation header and message composer. diff --git a/changelog.d/pr-7545.bugfix b/changelog.d/pr-7545.bugfix deleted file mode 100644 index a2f30eb67..000000000 --- a/changelog.d/pr-7545.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix: The last event description text color now matches the active theme. diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index a52d29522..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). \ No newline at end of file From 617c978437f42fe9ed5e54d5967801bf1fde3ec5 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 16:26:10 +0300 Subject: [PATCH 52/52] finish version++ --- Podfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index f47ccb4f3..eae31a173 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,9 +39,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.26.9): - - MatrixSDK/Core (= 0.26.9) - - MatrixSDK/Core (0.26.9): + - MatrixSDK (0.26.10): + - MatrixSDK/Core (= 0.26.10) + - MatrixSDK/Core (0.26.10): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -49,7 +49,7 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.26.9): + - MatrixSDK/JingleCallStack (0.26.10): - JitsiMeetSDKLite (= 7.0.1-lite) - MatrixSDK/Core - MatrixSDKCrypto (0.3.4) @@ -102,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.9) - - MatrixSDK/JingleCallStack (= 0.26.9) + - MatrixSDK (= 0.26.10) + - MatrixSDK/JingleCallStack (= 0.26.10) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -187,7 +187,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 2f6222978156818cf4c6ba590762ade601ba72f9 + MatrixSDK: 68e39c246ff8d80c5788d5fc46e93fcbb24703fa MatrixSDKCrypto: ac805c22c24f79f349cdbfa065855c73a4c81b51 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d @@ -208,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: a55fb48d3bef5f5e24fcaf8c39d1eae1ed8c1603 +PODFILE CHECKSUM: 4c82d7cddeb9c9b7a7adeaa2cd76d416117cd1a6 COCOAPODS: 1.11.3