diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index cf409bcec..ec714e255 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -17,7 +17,7 @@ jobs: add_to_triage: runs-on: ubuntu-latest if: > - github.repository == 'vector-im/element-x-ios' + github.repository == 'vector-im/element-ios' steps: - uses: octokit/graphql-action@v2.x with: diff --git a/CHANGES.md b/CHANGES.md index d646ee3ca..033842289 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +## Changes in 1.10.9 (2023-04-04) + +🙌 Improvements + +- Encryption: Simplify event encryption decoration ([#7440](https://github.com/vector-im/element-ios/pull/7440)) +- Add user suggestions for @room and highlight incoming messages containing @room when the room is encrypted. ([#7453](https://github.com/vector-im/element-ios/pull/7453)) +- Crypto: Expand rust crypto rollout to 50% users ([#7466](https://github.com/vector-im/element-ios/pull/7466)) +- Upgrade MatrixSDK version ([v0.26.6](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.6)). +- Replace Terms and Conditions with Acceptable Use Policy. ([#7456](https://github.com/vector-im/element-ios/issues/7456)) +- Crypto: Display correct SDK version ([#7457](https://github.com/vector-im/element-ios/issues/7457)) + +🐛 Bugfixes + +- QR verification: Start scanning as soon as camera ready ([#7469](https://github.com/vector-im/element-ios/pull/7469)) +- Timeline: No event decoration if no decryption result ([#7471](https://github.com/vector-im/element-ios/pull/7471)) +- Long pills are now truncated. ([#7413](https://github.com/vector-im/element-ios/issues/7413)) +- Update the read marker position even if it is not displayed ([#7420](https://github.com/vector-im/element-ios/issues/7420)) + + +## Changes in 1.10.8 (2023-03-28) + +🙌 Improvements + +- Verification: Display upgrade verification prompt ([#7454](https://github.com/vector-im/element-ios/pull/7454)) +- Upgrade MatrixSDK version ([v0.26.5](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.5)). + + ## Changes in 1.10.7 (2023-03-22) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 4b4e48563..dea3ab8b7 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.8 -CURRENT_PROJECT_VERSION = 1.10.8 +MARKETING_VERSION = 1.10.10 +CURRENT_PROJECT_VERSION = 1.10.10 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 2f85f3c13..e8c129619 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -110,7 +110,7 @@ final class BuildSettings: NSObject { // Note: Set empty strings to hide the related entry in application settings static let applicationCopyrightUrlString = "https://element.io/copyright" static let applicationPrivacyPolicyUrlString = "https://element.io/privacy" - static let applicationTermsConditionsUrlString = "https://element.io/terms-of-service" + static let applicationAcceptableUsePolicyUrlString = "https://element.io/acceptable-use-policy-terms" static let applicationHelpUrlString = "https://element.io/help" diff --git a/Podfile b/Podfile index d7a2bb7ac..ec141c86d 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.4' +$matrixSDKVersion = '= 0.26.6' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/Podfile.lock b/Podfile.lock index 01ef218c4..fcb75095b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,7 +27,9 @@ PODS: - GBDeviceInfo/Core (7.1.0) - GZIP (1.3.0) - Introspect (0.1.4) - - JitsiMeetSDK (5.0.2) + - JitsiMeetSDKLite (7.0.1-lite): + - JitsiWebRTC (~> 106.0) + - JitsiWebRTC (106.0.0) - KeychainAccess (4.2.2) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) @@ -37,20 +39,20 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.26.4): - - MatrixSDK/Core (= 0.26.4) - - MatrixSDK/Core (0.26.4): + - MatrixSDK (0.26.6): + - MatrixSDK/Core (= 0.26.6) + - MatrixSDK/Core (0.26.6): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDKCrypto (= 0.3.0) + - MatrixSDKCrypto (= 0.3.2) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.26.4): - - JitsiMeetSDK (= 5.0.2) + - MatrixSDK/JingleCallStack (0.26.6): + - JitsiMeetSDKLite (= 7.0.1-lite) - MatrixSDK/Core - - MatrixSDKCrypto (0.3.0) + - MatrixSDKCrypto (0.3.2) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) @@ -100,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.4) - - MatrixSDK/JingleCallStack (= 0.26.4) + - MatrixSDK (= 0.26.6) + - MatrixSDK/JingleCallStack (= 0.26.6) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -133,7 +135,8 @@ SPEC REPOS: - GBDeviceInfo - GZIP - Introspect - - JitsiMeetSDK + - JitsiMeetSDKLite + - JitsiWebRTC - KeychainAccess - KituraContracts - KTCenterFlowLayout @@ -175,7 +178,8 @@ SPEC CHECKSUMS: GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 Introspect: b62c4dd2063072327c21d618ef2bedc3c87bc366 - JitsiMeetSDK: edcac8e2b92ee0c7f3e75bd0aefefbe9faccfc93 + JitsiMeetSDKLite: d59573336ce887ec52327a9927aa8443f560d0b9 + JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 @@ -183,8 +187,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: f336fa2ba23d5db48873fa78d3e9565f768a2cda - MatrixSDKCrypto: 05ebe373ccebf40f8a0cff37d8f8b24fd01b9883 + MatrixSDK: 8179c184d819782282f47dab16ce6c2b68ef8a74 + MatrixSDKCrypto: 7073c382c484cb8ba7dba0a83e112ead96d3bbfd OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -204,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 741947b0cd7b44554c29e7ca268403622ba770b4 +PODFILE CHECKSUM: 54848168ab5303c9126626395886cd85f27a44b3 COCOAPODS: 1.11.3 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8a9f712c3..d88b99b9d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -815,6 +815,7 @@ Tap the + to start adding people."; "settings_olm_version" = "Olm Version %@"; "settings_copyright" = "Copyright"; "settings_term_conditions" = "Terms & Conditions"; +"settings_acceptable_use" = "Acceptable Use Policy"; "settings_privacy_policy" = "Privacy Policy"; "settings_third_party_notices" = "Third-party Notices"; "settings_analytics_and_crash_data" = "Send crash and analytics data"; @@ -1560,6 +1561,11 @@ Tap the + to start adding people."; "key_verification_self_verify_current_session_alert_message" = "Other users may not trust it."; "key_verification_self_verify_current_session_alert_validate_action" = "Verify"; +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "App updated"; +"key_verification_self_verify_security_upgrade_alert_message" = "Secure messaging has been improved with the latest update. Please re-verify your device."; + // Unverified sessions "key_verification_alert_title" = "You have unverified sessions"; "key_verification_alert_body" = "Review to ensure your account is safe."; diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index 2c4649869..fef876f92 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -68,6 +68,13 @@ extension MXBugReportRestClient { dateFormatter.timeZone = TimeZone(identifier: "UTC") userInfo["utc_time"] = dateFormatter.string(from: currentDate) + // SDKs + userInfo["matrix_sdk_version"] = MatrixSDKVersion + userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId + if let crypto = mainAccount?.mxSession?.crypto { + userInfo["crypto_module_version"] = crypto.version + } + if let customFields = customFields { // combine userInfo with custom fields overriding with custom where there is a conflict userInfo.merge(customFields) { (_, new) in new } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift index 96c743951..1bcff8a43 100644 --- a/Riot/Experiments/CryptoSDKFeature.swift +++ b/Riot/Experiments/CryptoSDKFeature.swift @@ -31,22 +31,28 @@ import MatrixSDKCrypto @objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { @objc static let shared = CryptoSDKFeature() - var version: String { - // Will be moved into the olm machine as API - Bundle(for: OlmMachine.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - } - 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 = 0.2 + localTargetPercentage: Double = 0.5 ) { self.remoteFeature = remoteFeature self.localFeature = PhasedRolloutFeature( @@ -98,6 +104,13 @@ import MatrixSDKCrypto } private func isFeatureEnabled(userId: String) -> Bool { - remoteFeature.isFeatureEnabled(Self.FeatureName) || localFeature.isEnabled(userId: userId) + // 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 b31ee8596..c02a605c6 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3027,6 +3027,14 @@ public class VectorL10n: NSObject { public static var keyVerificationSelfVerifyCurrentSessionAlertValidateAction: String { return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_validate_action") } + /// Secure messaging has been improved with the latest update. Please re-verify your device. + public static var keyVerificationSelfVerifySecurityUpgradeAlertMessage: String { + return VectorL10n.tr("Vector", "key_verification_self_verify_security_upgrade_alert_message") + } + /// App updated + public static var keyVerificationSelfVerifySecurityUpgradeAlertTitle: String { + return VectorL10n.tr("Vector", "key_verification_self_verify_security_upgrade_alert_title") + } /// Review public static var keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction: String { return VectorL10n.tr("Vector", "key_verification_self_verify_unverified_sessions_alert_validate_action") @@ -7211,6 +7219,10 @@ public class VectorL10n: NSObject { public static var settingsAbout: String { return VectorL10n.tr("Vector", "settings_about") } + /// Acceptable Use Policy + public static var settingsAcceptableUse: String { + return VectorL10n.tr("Vector", "settings_acceptable_use") + } /// Invalid credentials public static var settingsAdd3pidInvalidPasswordMessage: String { return VectorL10n.tr("Vector", "settings_add_3pid_invalid_password_message") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index efb9a8e1c..06746801c 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -211,6 +211,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "hideVerifyThisSessionAlert", defaultValue: false, storage: defaults) var hideVerifyThisSessionAlert + @UserDefault(key: "showVerificationUpgradeAlert", defaultValue: false, storage: defaults) + var showVerificationUpgradeAlert + @UserDefault(key: "matrixApps", defaultValue: false, storage: defaults) var matrixApps diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 1c72df342..ea96873ba 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -985,8 +985,20 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { private func presentVerifyCurrentSessionAlert(with session: MXSession) { MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertWithSession") - let alert = UIAlertController(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle, - message: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage, + let title: String + let message: String + + if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature, + feature.isEnabled && feature.needsVerificationUpgrade { + title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle + message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage + } else { + title = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle + message = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage + } + + let alert = UIAlertController(title: title, + message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertValidateAction, diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 520209860..d048d75a8 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -187,7 +187,7 @@ // Always show a warning badge if there was a decryption error. if (event.decryptionError) { - return EventEncryptionDecorationDecryptionError; + return EventEncryptionDecorationRed; } // Unencrypted message events should show a warning unless they're pending local echoes @@ -199,29 +199,27 @@ return EventEncryptionDecorationNone; } - return EventEncryptionDecorationNotEncrypted; + return EventEncryptionDecorationRed; } // The encryption is in a good state. - // Only show a warning badge if there are trust issues. - if (event.sender) + // Only show a warning badge if there are decryption trust issues. + if (event.decryptionDecoration) { - MXUserTrustLevel *userTrustLevel = [session.crypto trustLevelForUser:event.sender]; - MXDeviceInfo *deviceInfo = [session.crypto eventDeviceInfo:event]; - - if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified) + switch (event.decryptionDecoration.color) { - return EventEncryptionDecorationUntrustedDevice; + case MXEventDecryptionDecorationColorNone: + return EventEncryptionDecorationNone; + case MXEventDecryptionDecorationColorGrey: + return EventEncryptionDecorationGrey; + case MXEventDecryptionDecorationColorRed: + return EventEncryptionDecorationRed; } } - - if (event.isUntrusted) + else { - return EventEncryptionDecorationUnsafeKey; + return EventEncryptionDecorationNone; } - - // Everything was fine - return EventEncryptionDecorationNone; } @end diff --git a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m index 878742076..0c4adcd15 100644 --- a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m +++ b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m @@ -192,7 +192,13 @@ static NSAttributedString *verticalWhitespace = nil; NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"]; NSString *algorithm = _mxEvent.wireContent[@"algorithm"]; NSString *sessionId = _mxEvent.wireContent[@"session_id"]; - NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted]; + NSString *safetyMessage = _mxEvent.decryptionDecoration.message; + if (!safetyMessage) + { + // Use default copy if none is provided by the decryption decoration + BOOL isUntrusted = _mxEvent.decryptionDecoration && _mxEvent.decryptionDecoration.color != MXEventDecryptionDecorationColorNone; + safetyMessage = isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted]; + } NSString *decryptionError; if (_mxEvent.decryptionError) @@ -218,7 +224,8 @@ static NSAttributedString *verticalWhitespace = nil; } [eventInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionInfoEventUserId] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, + initWithString:[VectorL10n roomEventEncryptionInfoEventUserId] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [eventInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:senderId @@ -284,7 +291,7 @@ static NSAttributedString *verticalWhitespace = nil; attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [eventInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:untrusted + initWithString:safetyMessage attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont systemFontOfSize:14]}]]; [eventInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; @@ -368,7 +375,8 @@ static NSAttributedString *verticalWhitespace = nil; [deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionInfoDeviceId] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; + initWithString:[VectorL10n roomEventEncryptionInfoDeviceId] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:deviceId attributes:@{NSForegroundColorAttributeName: _defaultTextColor, @@ -376,12 +384,14 @@ static NSAttributedString *verticalWhitespace = nil; [deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionInfoDeviceVerification] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; + initWithString:[VectorL10n roomEventEncryptionInfoDeviceVerification] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:verification]; [deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionInfoDeviceFingerprint] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; + initWithString:[VectorL10n roomEventEncryptionInfoDeviceFingerprint] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]]; [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] initWithString:fingerprint attributes:@{NSForegroundColorAttributeName: _defaultTextColor, @@ -392,7 +402,8 @@ static NSAttributedString *verticalWhitespace = nil; { // Unknown device [deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionInfoDeviceUnknown] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont italicSystemFontOfSize:14]}]]; + initWithString:[VectorL10n roomEventEncryptionInfoDeviceUnknown] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont italicSystemFontOfSize:14]}]]; } [textViewAttributedString appendAttributedString:deviceInformationString]; @@ -462,7 +473,8 @@ static NSAttributedString *verticalWhitespace = nil; { // Prompt user NSMutableAttributedString *textViewAttributedString = [[NSMutableAttributedString alloc] - initWithString:[VectorL10n roomEventEncryptionVerifyTitle] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, + initWithString:[VectorL10n roomEventEncryptionVerifyTitle] + attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:17]}]; NSString *message = [VectorL10n roomEventEncryptionVerifyMessage:_mxDeviceInfo.displayName :_mxDeviceInfo.deviceId :_mxDeviceInfo.fingerprint]; diff --git a/Riot/Modules/Pills/PillAttachmentView.swift b/Riot/Modules/Pills/PillAttachmentView.swift index 538b88a48..575808bd7 100644 --- a/Riot/Modules/Pills/PillAttachmentView.swift +++ b/Riot/Modules/Pills/PillAttachmentView.swift @@ -70,7 +70,6 @@ class PillAttachmentView: UIView { label.font = pillData.font label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor label.translatesAutoresizingMaskIntoConstraints = false - label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) stack.addArrangedSubview(label) computedWidth += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: sizes.pillBackgroundHeight)).width @@ -146,10 +145,12 @@ class PillAttachmentView: UIView { computedWidth += 2 * sizes.horizontalMargin } + computedWidth = min(pillData.maxWidth, computedWidth) + let pillBackgroundView = UIView(frame: CGRect(x: 0, - y: sizes.verticalMargin, - width: computedWidth, - height: sizes.pillBackgroundHeight)) + y: sizes.verticalMargin, + width: computedWidth, + height: sizes.pillBackgroundHeight)) pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -sizes.horizontalMargin)) diff --git a/Riot/Modules/Pills/PillAttachmentViewProvider.swift b/Riot/Modules/Pills/PillAttachmentViewProvider.swift index 07806eddc..e47331a36 100644 --- a/Riot/Modules/Pills/PillAttachmentViewProvider.swift +++ b/Riot/Modules/Pills/PillAttachmentViewProvider.swift @@ -47,11 +47,15 @@ import UIKit return } - guard let pillData = textAttachment.data else { + guard var pillData = textAttachment.data else { MXLog.debug("[PillAttachmentViewProvider]: attachment misses pill data") return } - + + if let messageTextView { + pillData.maxWidth = messageTextView.bounds.width - 8 + } + let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)), diff --git a/Riot/Modules/Pills/PillTextAttachment.swift b/Riot/Modules/Pills/PillTextAttachment.swift index 5e46fe234..021e46ca4 100644 --- a/Riot/Modules/Pills/PillTextAttachment.swift +++ b/Riot/Modules/Pills/PillTextAttachment.swift @@ -125,6 +125,8 @@ class PillTextAttachment: NSTextAttachment { width += 2 * sizes.horizontalMargin } + width = min(width, data.maxWidth) + return CGSize(width: width, height: sizes.pillHeight) } diff --git a/Riot/Modules/Pills/PillTextAttachmentData.swift b/Riot/Modules/Pills/PillTextAttachmentData.swift index 99877444d..a119c941b 100644 --- a/Riot/Modules/Pills/PillTextAttachmentData.swift +++ b/Riot/Modules/Pills/PillTextAttachmentData.swift @@ -72,6 +72,8 @@ struct PillTextAttachmentData: Codable { var alpha: CGFloat /// Font for the display name var font: UIFont + /// Max width + var maxWidth: CGFloat /// Helper for preferred text to display. var displayText: String { @@ -93,12 +95,14 @@ struct PillTextAttachmentData: Codable { items: [PillTextAttachmentItem], isHighlighted: Bool, alpha: CGFloat, - font: UIFont) { + font: UIFont, + maxWidth: CGFloat = .greatestFiniteMagnitude) { self.pillType = pillType self.items = items self.isHighlighted = isHighlighted self.alpha = alpha self.font = font + self.maxWidth = maxWidth } // MARK: - Codable @@ -126,6 +130,7 @@ struct PillTextAttachmentData: Codable { } else { throw PillTextAttachmentDataError.noFontData } + maxWidth = .greatestFiniteMagnitude } func encode(to encoder: Encoder) throws { diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift index ecf9ca5a0..21f1b8497 100644 --- a/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift @@ -196,7 +196,7 @@ final class QRCodeReaderView: UIView { extension QRCodeReaderView: ZXCaptureDelegate { func captureCameraIsReady(_ capture: ZXCapture!) { - isScanning = true + startScanning() } func captureResult(_ capture: ZXCapture!, result: ZXResult!) { diff --git a/Riot/Modules/Rendezvous/RendezvousService.swift b/Riot/Modules/Rendezvous/RendezvousService.swift index ac9a890e1..de8f07979 100644 --- a/Riot/Modules/Rendezvous/RendezvousService.swift +++ b/Riot/Modules/Rendezvous/RendezvousService.swift @@ -33,6 +33,13 @@ enum RendezvousChannelAlgorithm: String { case ECDH_V2 = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256" } +/// Algorithm name as per MSC3906 +enum RendezvousFlow: String { + /// The v1 value never actually appears in JSON + case SETUP_ADDITIONAL_DEVICE_V1 = "org.matrix.msc3906.v1" + case SETUP_ADDITIONAL_DEVICE_V2 = "org.matrix.msc3906.setup.additional_device.v2" +} + /// Allows communication through a secure channel. Based on MSC3886 and MSC3903 @MainActor class RendezvousService { diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 8d0d2808a..7c014e018 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -44,6 +44,9 @@ #import "MXKPreviewViewController.h" +// Constant used to determine whether an event is visible at the bottom of the tableview, based on its visible height +static const CGFloat kCellVisibilityMinimumHeight = 8.0; + @interface MXKRoomViewController () { /** @@ -2419,7 +2422,7 @@ contentBottomOffsetY = _bubblesTableView.contentSize.height; } // Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden. - contentBottomOffsetY += 8; + contentBottomOffsetY += kCellVisibilityMinimumHeight; // Reset the current event id currentEventIdAtTableBottom = nil; @@ -2489,24 +2492,26 @@ if (acknowledge && self.isEventsAcknowledgementEnabled) { // Indicate to the homeserver that the user has read this event. - - // Check whether the read marker must be updated. - BOOL updateReadMarker = _updateRoomReadMarker; - if (updateReadMarker && roomDataSource.room.accountData.readMarkerEventId) - { - MXEvent *currentReadMarkerEvent = [roomDataSource.mxSession.store eventWithEventId:roomDataSource.room.accountData.readMarkerEventId inRoom:roomDataSource.roomId]; - if (!currentReadMarkerEvent) - { - currentReadMarkerEvent = [roomDataSource eventWithEventId:roomDataSource.room.accountData.readMarkerEventId]; - } - - // Update the read marker only if the current event is available, and the new event is posterior to it. - updateReadMarker = (currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= component.event.originServerTs)); - } - if (self.navigationController.viewControllers.lastObject == self) { - [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateReadMarker]; + // Check if the selected event is eligible to be the new read marker position too + if (!bubbleData.collapsed && [self eligibleForReadMarkerUpdate:component.event]) + { + BOOL updateRoomReadMarker = _updateRoomReadMarker && [self isEventPosteriorToCurrentReadMarker:component.event]; + // Acknowledge this event and update the read marker if needed + [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateRoomReadMarker]; + } + else + { + // Acknowledge only this event. The read marker is handled separately + [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:NO]; + + if (_updateRoomReadMarker) + { + // Try to find the best event for the new read marker position + [self updateReadMarkerEvent]; + } + } } } break; @@ -2517,6 +2522,118 @@ } } +- (BOOL)eligibleForReadMarkerUpdate:(MXEvent *)event { + // Prevent the readmarker to be placed on a relatesTo or a redaction event + if (event.relatesTo || event.redacts) + { + return NO; + } + + return YES; +} + +- (BOOL)isEventPosteriorToCurrentReadMarker:(MXEvent *)event { + if (roomDataSource.room.accountData.readMarkerEventId) + { + MXEvent *currentReadMarkerEvent = [roomDataSource.mxSession.store eventWithEventId:roomDataSource.room.accountData.readMarkerEventId inRoom:roomDataSource.roomId]; + if (!currentReadMarkerEvent) + { + currentReadMarkerEvent = [roomDataSource eventWithEventId:roomDataSource.room.accountData.readMarkerEventId]; + } + + // Update the read marker only if the current event is available, and the new event is posterior to it. + return currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= event.originServerTs); + } + return YES; +} + +/// Try to update the read marker by looking for an eligible event displayed at the bottom of the tableview +- (void)updateReadMarkerEvent +{ + // Compute the content offset corresponding to the line displayed at the table bottom (just above the toolbar). + CGFloat contentBottomOffsetY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom); + if (contentBottomOffsetY > _bubblesTableView.contentSize.height) + { + contentBottomOffsetY = _bubblesTableView.contentSize.height; + } + // Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden. + contentBottomOffsetY += kCellVisibilityMinimumHeight; + + // Consider the visible cells (starting by those displayed at the bottom) + NSArray *visibleCells = [_bubblesTableView visibleCells]; + NSInteger index = visibleCells.count; + UITableViewCell *cell; + while (index--) + { + cell = visibleCells[index]; + + // Check whether the cell is actually visible + if (!cell || cell.frame.origin.y > contentBottomOffsetY) + { + continue; + } + + if (![cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + { + continue; + } + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; + MXKRoomBubbleCellData *bubbleData = roomBubbleTableViewCell.bubbleData; + if (!bubbleData) + { + continue; + } + + // Prevent to place the read marker on a collapsed cell + if (bubbleData.collapsed) + { + continue; + } + + // Check which bubble component is displayed at the bottom. + // For that update each component position. + [bubbleData prepareBubbleComponentsPosition]; + + NSArray *bubbleComponents = bubbleData.bubbleComponents; + NSInteger componentIndex = bubbleComponents.count; + + CGFloat bottomPositionY = cell.frame.size.height; + + MXKRoomBubbleComponent *component; + + while (componentIndex --) + { + component = bubbleComponents[componentIndex]; + + // Prevent the read marker to be placed on an unsupported event (e.g. redactions, reactions, ...) + if (![self eligibleForReadMarkerUpdate:component.event]) + { + continue; + } + + // Check whether the bottom part of the component is visible. + CGFloat pos = cell.frame.origin.y + bottomPositionY; + if (pos <= contentBottomOffsetY) + { + // We found the component + // Check whether the read marker must be updated. + if ([self isEventPosteriorToCurrentReadMarker:component.event]) + { + // Move the read marker to this event + [roomDataSource.room moveReadMarkerToEventId:component.event.eventId]; + } + + return; + } + + // Prepare the bottom position for the next component + bottomPositionY = roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y; + } + + // else we consider the previous cell. + } +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 6fa73faa5..398e12e8f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1089,7 +1089,8 @@ static CGSize kThreadListBarButtonItemImageSize; _voiceMessageController.roomId = dataSource.roomId; _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager - room:dataSource.room]; + room:dataSource.room + userID:self.roomDataSource.mxSession.myUserId]; _userSuggestionCoordinator.delegate = self; [self setupUserSuggestionViewIfNeeded]; @@ -5337,7 +5338,7 @@ static CGSize kThreadListBarButtonItemImageSize; [self dismissKeyboard]; NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId; NSString *threadId = self.roomDataSource.threadId; - [self reloadRoomWihtEventId:eventId threadId:threadId]; + [self reloadRoomWihtEventId:eventId threadId:threadId forceUpdateRoomMarker:YES]; } else if (sender == self.resetReadMarkerButton) { @@ -6626,8 +6627,12 @@ static CGSize kThreadListBarButtonItemImageSize; // Check whether the read marker exists and has not been rendered yet. if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId) { - UITableViewCell *cell = [self.bubblesTableView visibleCells].firstObject; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + return [evaluatedObject isKindOfClass:MXKRoomBubbleTableViewCell.class]; + }]; + NSArray *visibleCells = [[self.bubblesTableView visibleCells] filteredArrayUsingPredicate:predicate]; + UITableViewCell *cell = visibleCells.firstObject; + if (cell) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; // Check whether the read marker is inside the first displayed cell. @@ -6654,6 +6659,9 @@ static CGSize kThreadListBarButtonItemImageSize; else { self.jumpToLastUnreadBannerContainer.hidden = YES; + + // Force the read marker position in order to not depend on the read marker animation (https://github.com/vector-im/element-ios/issues/7420) + self.updateRoomReadMarker = YES; } } } @@ -7933,15 +7941,17 @@ static CGSize kThreadListBarButtonItemImageSize; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } } + - (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator viewEventInTimeline:(MXEvent *)event { [self.navigationController popToViewController:self animated:true]; - [self reloadRoomWihtEventId:event.eventId threadId:event.threadId]; + [self reloadRoomWihtEventId:event.eventId threadId:event.threadId forceUpdateRoomMarker:NO]; } -(void)reloadRoomWihtEventId:(NSString *)eventId threadId:(NSString *)threadId + forceUpdateRoomMarker:(BOOL)forceUpdateRoomMarker { // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. MXWeakify(self); @@ -7960,6 +7970,9 @@ static CGSize kThreadListBarButtonItemImageSize; // Give the data source ownership to the room view controller. self.hasRoomDataSourceOwnership = YES; + + // Force the read marker update if needed (e.g if we jumped on the last unread message using the banner). + self.updateRoomReadMarker |= forceUpdateRoomMarker; }]; } @@ -8062,6 +8075,19 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didRequestMentionForMember:(MXRoomMember *)member textTrigger:(NSString *)textTrigger +{ + [self removeTriggerTextFromComposer:textTrigger]; + [self mention:member]; +} + +- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator + textTrigger:(NSString *)textTrigger +{ + [self removeTriggerTextFromComposer:textTrigger]; + [self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; +} + +- (void)removeTriggerTextFromComposer:(NSString *)textTrigger { RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; if (toolbar && textTrigger.length) { @@ -8072,8 +8098,6 @@ static CGSize kThreadListBarButtonItemImageSize; range:NSMakeRange(0, attributedTextMessage.length)]; [toolbar setAttributedTextMessage:attributedTextMessage]; } - - [self mention:member]; } - (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height diff --git a/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h b/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h index f6d4264bd..fa0138d7e 100644 --- a/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h +++ b/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h @@ -17,14 +17,14 @@ #ifndef EventEncryptionDecoration_h #define EventEncryptionDecoration_h +/** + Decoration used alongside encrypted events + */ typedef NS_ENUM(NSUInteger, EventEncryptionDecoration) { EventEncryptionDecorationNone, - EventEncryptionDecorationUnsafeKey, - EventEncryptionDecorationDecryptionError, - EventEncryptionDecorationNotEncrypted, - EventEncryptionDecorationUntrustedDevice + EventEncryptionDecorationGrey, + EventEncryptionDecorationRed }; - #endif /* EventEncryptionDecoration_h */ diff --git a/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m b/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m index 07d23f209..05c5bd2ae 100644 --- a/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m @@ -27,11 +27,9 @@ NSString *const kRoomEncryptedDataBubbleCellTapOnEncryptionIcon = @"kRoomEncrypt switch (bubbleComponent.encryptionDecoration) { case EventEncryptionDecorationNone: return nil; - case EventEncryptionDecorationUnsafeKey: + case EventEncryptionDecorationGrey: return AssetImages.encryptionUntrusted.image; - case EventEncryptionDecorationDecryptionError: - case EventEncryptionDecorationNotEncrypted: - case EventEncryptionDecorationUntrustedDevice: + case EventEncryptionDecorationRed: return AssetImages.encryptionWarning.image; default: return nil; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 023679125..244e28be0 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -162,7 +162,7 @@ typedef NS_ENUM(NSUInteger, ADVANCED) typedef NS_ENUM(NSUInteger, ABOUT) { ABOUT_COPYRIGHT_INDEX = 0, - ABOUT_TERM_CONDITIONS_INDEX, + ABOUT_ACCEPTABLE_USE_INDEX, ABOUT_PRIVACY_INDEX, ABOUT_THIRD_PARTY_INDEX, }; @@ -567,9 +567,9 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionAbout addRowWithTag:ABOUT_COPYRIGHT_INDEX]; } - if (BuildSettings.applicationTermsConditionsUrlString.length) + if (BuildSettings.applicationAcceptableUsePolicyUrlString.length) { - [sectionAbout addRowWithTag:ABOUT_TERM_CONDITIONS_INDEX]; + [sectionAbout addRowWithTag:ABOUT_ACCEPTABLE_USE_INDEX]; } if (BuildSettings.applicationPrivacyPolicyUrlString.length) { @@ -2465,11 +2465,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } else if (section == SECTION_TAG_ABOUT) { - if (row == ABOUT_TERM_CONDITIONS_INDEX) + if (row == ABOUT_ACCEPTABLE_USE_INDEX) { MXKTableViewCell *termAndConditionCell = [self getDefaultTableViewCell:tableView]; - termAndConditionCell.textLabel.text = [VectorL10n settingsTermConditions]; + termAndConditionCell.textLabel.text = [VectorL10n settingsAcceptableUse]; [termAndConditionCell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; @@ -2885,11 +2885,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self pushViewController:webViewViewController]; } - else if (row == ABOUT_TERM_CONDITIONS_INDEX) + else if (row == ABOUT_ACCEPTABLE_USE_INDEX) { - WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationTermsConditionsUrlString]; + WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationAcceptableUsePolicyUrlString]; - webViewViewController.title = [VectorL10n settingsTermConditions]; + webViewViewController.title = [VectorL10n settingsAcceptableUse]; [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self pushViewController:webViewViewController]; diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift index b6bae6757..1ec1f0238 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift @@ -18,6 +18,7 @@ import Foundation struct QRLoginCode: Codable { let rendezvous: RendezvousDetails + let flow: String? let intent: String } @@ -42,7 +43,8 @@ struct QRLoginRendezvousPayload: Codable { var intent: Intent? var outcome: Outcome? - + var reason: FailureReason? + // swiftformat:disable:next redundantBackticks var protocols: [`Protocol`]? @@ -64,6 +66,7 @@ struct QRLoginRendezvousPayload: Codable { case type case intent case outcome + case reason case homeserver case user case protocols @@ -77,9 +80,18 @@ struct QRLoginRendezvousPayload: Codable { } enum `Type`: String, Codable { - case loginStart = "m.login.start" case loginProgress = "m.login.progress" + /** + This is only used in MSC3906 v1 and will be removed + */ case loginFinish = "m.login.finish" + case loginFailure = "m.login.failure" + case loginProtocol = "m.login.protocol" + case loginProtocols = "m.login.protocols" + case loginApproved = "m.login.approved" + case loginDeclined = "m.login.declined" + case loginSuccess = "m.login.success" + case loginVerified = "m.login.verified" } enum Intent: String, Codable { @@ -87,6 +99,9 @@ struct QRLoginRendezvousPayload: Codable { case loginReciprocate = "login.reciprocate" } + /** + This is only used in MSC306 v1 and will be removed + */ enum Outcome: String, Codable { case success case declined @@ -97,4 +112,11 @@ struct QRLoginRendezvousPayload: Codable { enum `Protocol`: String, Codable { case loginToken = "org.matrix.msc3906.login_token" } + + enum FailureReason: String, Codable { + case cancelled + case unsupported + case e2eeSecurityError = "e2ee_security_error" + case incompatibleIntent = "incompatible_intent" + } } diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 05506b4bd..9c9100087 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -180,6 +180,12 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { return } + guard let flow = code.flow != nil ? RendezvousFlow(rawValue: code.flow!) : .SETUP_ADDITIONAL_DEVICE_V1 else { + MXLog.error("[QRLoginService] Unsupported flow") + state = .failed(error: .deviceNotSupported) + return + } + // so, this is of an expected algorithm so any bad data can be considered an invalid QR code guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue, let uri = code.rendezvous.transport?.uri, @@ -223,7 +229,10 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { } MXLog.debug("[QRLoginService] Request login with `login_token`") - guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)), + let protocolPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1 + ? QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken) + : QRLoginRendezvousPayload(type: .loginProtocol, protocol: .loginToken) + guard let requestData = try? JSONEncoder().encode(protocolPayload), case .success = await rendezvousService.send(data: requestData) else { MXLog.error("[QRLoginService] Failed sending continue with `login_token` request") await teardownRendezvous(state: .failed(error: .rendezvousFailed)) @@ -282,10 +291,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { } MXLog.debug("[QRLoginService] Session created, sending device details") - guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress, - outcome: .success, - deviceId: session.myDeviceId, - deviceKey: session.crypto.deviceEd25519Key)), + let successPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1 + ? QRLoginRendezvousPayload(type: .loginProgress, outcome: .success, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key) + : QRLoginRendezvousPayload(type: .loginSuccess, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key) + + guard let requestData = try? JSONEncoder().encode(successPayload), case .success = await rendezvousService.send(data: requestData) else { MXLog.error("[QRLoginService] Failed sending session details") await teardownRendezvous(state: .failed(error: .rendezvousFailed)) @@ -307,7 +317,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { MXLog.debug("[QRLoginService] Wait for cross-signing details") guard case let .success(data) = await rendezvousService.receive(), let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data), - responsePayload.outcome == .verified, + flow == .SETUP_ADDITIONAL_DEVICE_V1 && responsePayload.outcome == .verified || responsePayload.type == .loginVerified, let verifiyingDeviceId = responsePayload.verifyingDeviceId, let verifyingDeviceKey = responsePayload.verifyingDeviceKey else { MXLog.error("[QRLoginService] Received invalid cross-signing details") diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/Mock/MockQRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/Mock/MockQRLoginService.swift index 3f511214e..2285c7960 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/Mock/MockQRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/Mock/MockQRLoginService.swift @@ -20,13 +20,16 @@ import SwiftUI class MockQRLoginService: QRLoginServiceProtocol { private let mockCanDisplayQR: Bool + private let mockFlow: String? init(withState state: QRLoginServiceState = .initial, mode: QRLoginServiceMode = .notAuthenticated, - canDisplayQR: Bool = true) { + canDisplayQR: Bool = true, + flow: String? = nil) { self.state = state self.mode = mode mockCanDisplayQR = canDisplayQR + mockFlow = flow } // MARK: - QRLoginServiceProtocol @@ -57,6 +60,7 @@ class MockQRLoginService: QRLoginServiceProtocol { uri: "https://matrix.org"), key: "some.public.key") return QRLoginCode(rendezvous: details, + flow: mockFlow, intent: "login.start") } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index de56c0736..a2156cd89 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -22,12 +22,14 @@ import WysiwygComposer protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } struct UserSuggestionCoordinatorParameters { let mediaManager: MXMediaManager let room: MXRoom + let userID: String } /// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c. @@ -66,7 +68,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { init(parameters: UserSuggestionCoordinatorParameters) { self.parameters = parameters - roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room) + roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider) let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) @@ -83,6 +85,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { switch result { case .selectedItemWithIdentifier(let identifier): + if identifier == UserSuggestionID.room { + self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger) + return + } + guard let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first else { return } @@ -148,29 +155,44 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { private let room: MXRoom + private let userID: String var roomMembers: [MXRoomMember] = [] + var canMentionRoom = false - init(room: MXRoom) { + init(room: MXRoom, userID: String) { self.room = room + self.userID = userID + updateWithPowerLevels() + } + + /// Gets the power levels for the room to update suggestions accordingly. + func updateWithPowerLevels() { + room.state { [weak self] state in + guard let self, let powerLevels = state?.powerLevels else { return } + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) + let mentionRoomPowerLevel = powerLevels.minimumPowerLevel(forNotifications: kMXRoomPowerLevelNotificationsRoomKey, + defaultPower: kMXRoomPowerLevelNotificationsRoomDefault) + self.canMentionRoom = userPowerLevel >= mentionRoomPowerLevel + } } func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { - room.members({ [weak self] roomMembers in + room.members { [weak self] roomMembers in guard let self = self, let joinedMembers = roomMembers?.joinedMembers else { return } self.roomMembers = joinedMembers members(self.roomMembersToProviderMembers(joinedMembers)) - }, lazyLoadedMembers: { [weak self] lazyRoomMembers in + } lazyLoadedMembers: { [weak self] lazyRoomMembers in guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else { return } self.roomMembers = joinedMembers members(self.roomMembersToProviderMembers(joinedMembers)) - }, failure: { error in + } failure: { error in MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) - }) + } } private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index 9dbebdbf3..0d1f6795e 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -19,6 +19,7 @@ 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, didUpdateViewHeight height: CGFloat) } @@ -31,8 +32,8 @@ final class UserSuggestionCoordinatorBridge: NSObject { weak var delegate: UserSuggestionCoordinatorBridgeDelegate? - init(mediaManager: MXMediaManager, room: MXRoom) { - let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room) + init(mediaManager: MXMediaManager, room: MXRoom, userID: String) { + let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID) let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) _userSuggestionCoordinator = userSuggestionCoordinator @@ -62,6 +63,10 @@ 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, 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 0f161ee38..a790e2845 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -24,7 +24,13 @@ struct RoomMembersProviderMember { var avatarUrl: String } +class UserSuggestionID: NSObject { + /// A special case added for suggesting `@room` mentions. + @objc static let room = "@room" +} + protocol RoomMembersProviderProtocol { + var canMentionRoom: Bool { get } func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) } @@ -111,7 +117,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { return } - self.suggestionItems = members.map { member in + self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl) } @@ -124,3 +130,11 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } } } + +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: "")] + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index b32580c8d..7ae0bfa39 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -20,87 +20,111 @@ import XCTest @testable import RiotSwiftUI class UserSuggestionServiceTests: XCTestCase { - var service: UserSuggestionService? + var service: UserSuggestionService! + var canMentionRoom = false override func setUp() { service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) + canMentionRoom = false } func testAlice() { - service?.processTextMessage("@Al") - assert(service?.items.value.first?.displayName == "Alice") + service.processTextMessage("@Al") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") - service?.processTextMessage("@al") - assert(service?.items.value.first?.displayName == "Alice") + service.processTextMessage("@al") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") - service?.processTextMessage("@ice") - assert(service?.items.value.first?.displayName == "Alice") + service.processTextMessage("@ice") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") - service?.processTextMessage("@Alice") - assert(service?.items.value.first?.displayName == "Alice") + service.processTextMessage("@Alice") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") - service?.processTextMessage("@alice:matrix.org") - assert(service?.items.value.first?.displayName == "Alice") + service.processTextMessage("@alice:matrix.org") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") } func testBob() { - service?.processTextMessage("@ob") - assert(service?.items.value.first?.displayName == "Bob") + service.processTextMessage("@ob") + XCTAssertEqual(service.items.value.first?.displayName, "Bob") - service?.processTextMessage("@ob:") - assert(service?.items.value.first?.displayName == "Bob") + service.processTextMessage("@ob:") + XCTAssertEqual(service.items.value.first?.displayName, "Bob") - service?.processTextMessage("@b:matrix") - assert(service?.items.value.first?.displayName == "Bob") + service.processTextMessage("@b:matrix") + XCTAssertEqual(service.items.value.first?.displayName, "Bob") } func testBoth() { - service?.processTextMessage("@:matrix") - assert(service?.items.value.first?.displayName == "Alice") - assert(service?.items.value.last?.displayName == "Bob") + service.processTextMessage("@:matrix") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.displayName, "Bob") - service?.processTextMessage("@.org") - assert(service?.items.value.first?.displayName == "Alice") - assert(service?.items.value.last?.displayName == "Bob") + service.processTextMessage("@.org") + XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.displayName, "Bob") } func testEmptyResult() { - service?.processTextMessage("Lorem ipsum idolor") - assert(service?.items.value.count == 0) + service.processTextMessage("Lorem ipsum idolor") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage("@") - assert(service?.items.value.count == 0) + service.processTextMessage("@") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage("@@") - assert(service?.items.value.count == 0) + service.processTextMessage("@@") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage("alice@matrix.org") - assert(service?.items.value.count == 0) + service.processTextMessage("alice@matrix.org") + XCTAssertTrue(service.items.value.isEmpty) } func testStuff() { - service?.processTextMessage("@@") - assert(service?.items.value.count == 0) + service.processTextMessage("@@") + XCTAssertTrue(service.items.value.isEmpty) } func testWhitespaces() { - service?.processTextMessage("") - assert(service?.items.value.count == 0) + service.processTextMessage("") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage(" ") - assert(service?.items.value.count == 0) + service.processTextMessage(" ") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage("\n") - assert(service?.items.value.count == 0) + service.processTextMessage("\n") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage(" \n ") - assert(service?.items.value.count == 0) + service.processTextMessage(" \n ") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage("@A ") - assert(service?.items.value.count == 0) + service.processTextMessage("@A ") + XCTAssertTrue(service.items.value.isEmpty) - service?.processTextMessage(" @A ") - assert(service?.items.value.count == 0) + service.processTextMessage(" @A ") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testRoomWithoutPower() { + // Given a user without the power to mention a room. + canMentionRoom = false + + // Given a user without the power to mention a room. + service.processTextMessage("@ro") + + // Then the completion for a room mention should not be shown. + XCTAssertTrue(service.items.value.isEmpty) + } + + func testRoomWithPower() { + // Given a user without the power to mention a room. + canMentionRoom = true + + // Given a user without 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) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index a0ed20268..0a9395fa5 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -43,6 +43,8 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { } extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { + var canMentionRoom: Bool { false } + func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { if Self.members == nil { Self.members = generateUsersWithCount(10) diff --git a/changelog.d/7476.build b/changelog.d/7476.build new file mode 100644 index 000000000..09ac016f1 --- /dev/null +++ b/changelog.d/7476.build @@ -0,0 +1 @@ +Pinned used Xcode version to 14.2 as newer version fail ASC validation \ No newline at end of file