diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index d0a5dfe789..38c2216c80 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -1035,6 +1035,7 @@ E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; }; E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; + E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; }; E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; }; E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; }; @@ -1488,6 +1489,7 @@ 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = ""; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = ""; }; + 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = ""; }; 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = ""; }; 419957D7B1C983D7B3B93678 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenModels.swift; sourceTree = ""; }; @@ -4018,6 +4020,7 @@ children = ( 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, + 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, 464C6BFAA853DC755B9C1F60 /* PinnedItemsBanner */, ); @@ -6788,6 +6791,7 @@ F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */, C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */, A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */, + E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */, 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, @@ -7785,7 +7789,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.53; + version = 1.0.55; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 77d6b4c460..864c38bde3 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "83abbdc8485340c20f27148153ff62f690ca210b", - "version" : "1.0.53" + "revision" : "8ee63edc76bccd12c17a22eaf4eddae69e5f1303", + "version" : "1.0.55" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 413594832b..0e4fecdf67 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -226,6 +226,7 @@ "common_voice_message" = "Voice message"; "common_waiting" = "Waiting…"; "common_waiting_for_decryption_key" = "Waiting for this message"; +"common.copied_to_clipboard" = "Copied to clipboard"; "common.do_not_show_this_again" = "Do not show this again"; "common.open_source_licenses" = "Open source licenses"; "common.pinned" = "Pinned"; @@ -240,6 +241,7 @@ "confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; "confirm_recovery_key_banner_title" = "Enter your recovery key"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; "dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; "dialog_permission_generic" = "Please grant the permission in the system settings."; "dialog_permission_location_description_ios" = "Grant access in Settings -> Location."; @@ -481,7 +483,7 @@ "screen_edit_profile_updating_details" = "Updating profile…"; "screen_encryption_reset_action_continue_reset" = "Continue reset"; "screen_encryption_reset_bullet_1" = "Your account details, contacts, preferences, and chat list will be kept"; -"screen_encryption_reset_bullet_2" = "You will lose your existing message history"; +"screen_encryption_reset_bullet_2" = "You will lose your existing message history unless it is stored on another device"; "screen_encryption_reset_bullet_3" = "You will need to verify all your existing devices and contacts again"; "screen_encryption_reset_footer" = "Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."; "screen_encryption_reset_title" = "Can't confirm? You’ll need to reset your identity."; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 542d58057a..ec0dc26b69 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -124,6 +124,8 @@ final class AppSettings { let encryptionURL: URL = "https://element.io/help#encryption" /// A URL where users can go read more about the chat backup. let chatBackupDetailsURL: URL = "https://element.io/help#encryption5" + /// A URL where users can go read more about identity pinning violations + let identityPinningViolationDetailsURL: URL = "https://element.io/help#18" /// Any domains that Element web may be hosted on - used for handling links. let elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"] diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 5993152834..0df9e11bc5 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -590,7 +590,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory) - let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, + let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy, + roomProxy: roomProxy, focussedEvent: focussedEvent, timelineController: timelineController, mediaProvider: userSession.mediaProvider, diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 55bb4b85db..36f6866c76 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -520,6 +520,10 @@ internal enum L10n { internal static func crashDetectionDialogContent(_ p1: Any) -> String { return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1)) } + /// %1$@'s identity appears to have changed. %2$@ + internal static func cryptoIdentityChangePinViolation(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "crypto_identity_change_pin_violation", String(describing: p1), String(describing: p2)) + } /// In order to let the application use the camera, please grant the permission in the system settings. internal static var dialogPermissionCamera: String { return L10n.tr("Localizable", "dialog_permission_camera") } /// Please grant the permission in the system settings. @@ -1119,7 +1123,7 @@ internal enum L10n { internal static var screenEncryptionResetActionContinueReset: String { return L10n.tr("Localizable", "screen_encryption_reset_action_continue_reset") } /// Your account details, contacts, preferences, and chat list will be kept internal static var screenEncryptionResetBullet1: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_1") } - /// You will lose your existing message history + /// You will lose your existing message history unless it is stored on another device internal static var screenEncryptionResetBullet2: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_2") } /// You will need to verify all your existing devices and contacts again internal static var screenEncryptionResetBullet3: String { return L10n.tr("Localizable", "screen_encryption_reset_bullet_3") } @@ -2410,6 +2414,8 @@ internal enum L10n { } internal enum Common { + /// Copied to clipboard + internal static var copiedToClipboard: String { return L10n.tr("Localizable", "common.copied_to_clipboard") } /// Do not show this again internal static var doNotShowThisAgain: String { return L10n.tr("Localizable", "common.do_not_show_this_again") } /// Open source licenses diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 356a7b892d..e4f2dadf38 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -4426,6 +4426,76 @@ class ClientProxyMock: ClientProxyProtocol { return curve25519Base64ReturnValue } } + //MARK: - pinUserIdentity + + var pinUserIdentityUnderlyingCallsCount = 0 + var pinUserIdentityCallsCount: Int { + get { + if Thread.isMainThread { + return pinUserIdentityUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinUserIdentityUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUserIdentityUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinUserIdentityUnderlyingCallsCount = newValue + } + } + } + } + var pinUserIdentityCalled: Bool { + return pinUserIdentityCallsCount > 0 + } + var pinUserIdentityReceivedUserID: String? + var pinUserIdentityReceivedInvocations: [String] = [] + + var pinUserIdentityUnderlyingReturnValue: Result! + var pinUserIdentityReturnValue: Result! { + get { + if Thread.isMainThread { + return pinUserIdentityUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = pinUserIdentityUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUserIdentityUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + pinUserIdentityUnderlyingReturnValue = newValue + } + } + } + } + var pinUserIdentityClosure: ((String) async -> Result)? + + func pinUserIdentity(_ userID: String) async -> Result { + pinUserIdentityCallsCount += 1 + pinUserIdentityReceivedUserID = userID + DispatchQueue.main.async { + self.pinUserIdentityReceivedInvocations.append(userID) + } + if let pinUserIdentityClosure = pinUserIdentityClosure { + return await pinUserIdentityClosure(userID) + } else { + return pinUserIdentityReturnValue + } + } //MARK: - resetIdentity var resetIdentityUnderlyingCallsCount = 0 @@ -5791,6 +5861,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { set(value) { underlyingTypingMembersPublisher = value } } var underlyingTypingMembersPublisher: CurrentValuePublisher<[String], Never>! + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { + get { return underlyingIdentityStatusChangesPublisher } + set(value) { underlyingIdentityStatusChangesPublisher = value } + } + var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>! var actionsPublisher: AnyPublisher { get { return underlyingActionsPublisher } set(value) { underlyingActionsPublisher = value } @@ -12495,6 +12570,7 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol { } var underlyingUserID: String! var displayName: String? + var disambiguatedDisplayName: String? var avatarURL: URL? var membership: MembershipState { get { return underlyingMembership } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 4cd45da022..8469d7de3c 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -625,6 +625,52 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - customLoginWithJwt + + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError: Error? + var customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = 0 + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount: Int { + get { + if Thread.isMainThread { + return customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + customLoginWithJwtJwtInitialDeviceNameDeviceIdUnderlyingCallsCount = newValue + } + } + } + } + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdCalled: Bool { + return customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount > 0 + } + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments: (jwt: String, initialDeviceName: String?, deviceId: String?)? + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations: [(jwt: String, initialDeviceName: String?, deviceId: String?)] = [] + open var customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure: ((String, String?, String?) async throws -> Void)? + + open override func customLoginWithJwt(jwt: String, initialDeviceName: String?, deviceId: String?) async throws { + if let error = customLoginWithJwtJwtInitialDeviceNameDeviceIdThrowableError { + throw error + } + customLoginWithJwtJwtInitialDeviceNameDeviceIdCallsCount += 1 + customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedArguments = (jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId) + DispatchQueue.main.async { + self.customLoginWithJwtJwtInitialDeviceNameDeviceIdReceivedInvocations.append((jwt: jwt, initialDeviceName: initialDeviceName, deviceId: deviceId)) + } + try await customLoginWithJwtJwtInitialDeviceNameDeviceIdClosure?(jwt, initialDeviceName, deviceId) + } + //MARK: - deactivateAccount open var deactivateAccountAuthDataEraseDataThrowableError: Error? @@ -4953,6 +4999,77 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } } + //MARK: - roomDecryptionTrustRequirement + + var roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = 0 + open var roomDecryptionTrustRequirementTrustRequirementCallsCount: Int { + get { + if Thread.isMainThread { + return roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + roomDecryptionTrustRequirementTrustRequirementUnderlyingCallsCount = newValue + } + } + } + } + open var roomDecryptionTrustRequirementTrustRequirementCalled: Bool { + return roomDecryptionTrustRequirementTrustRequirementCallsCount > 0 + } + open var roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement: TrustRequirement? + open var roomDecryptionTrustRequirementTrustRequirementReceivedInvocations: [TrustRequirement] = [] + + var roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue: ClientBuilder! + open var roomDecryptionTrustRequirementTrustRequirementReturnValue: ClientBuilder! { + get { + if Thread.isMainThread { + return roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue + } else { + var returnValue: ClientBuilder? = nil + DispatchQueue.main.sync { + returnValue = roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + roomDecryptionTrustRequirementTrustRequirementUnderlyingReturnValue = newValue + } + } + } + } + open var roomDecryptionTrustRequirementTrustRequirementClosure: ((TrustRequirement) -> ClientBuilder)? + + open override func roomDecryptionTrustRequirement(trustRequirement: TrustRequirement) -> ClientBuilder { + roomDecryptionTrustRequirementTrustRequirementCallsCount += 1 + roomDecryptionTrustRequirementTrustRequirementReceivedTrustRequirement = trustRequirement + DispatchQueue.main.async { + self.roomDecryptionTrustRequirementTrustRequirementReceivedInvocations.append(trustRequirement) + } + if let roomDecryptionTrustRequirementTrustRequirementClosure = roomDecryptionTrustRequirementTrustRequirementClosure { + return roomDecryptionTrustRequirementTrustRequirementClosure(trustRequirement) + } else { + return roomDecryptionTrustRequirementTrustRequirementReturnValue + } + } + //MARK: - roomKeyRecipientStrategy var roomKeyRecipientStrategyStrategyUnderlyingCallsCount = 0 @@ -12296,6 +12413,34 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } + //MARK: - pinUserIdentity + + open var pinUserIdentityUserIdThrowableError: Error? + var pinUserIdentityUserIdUnderlyingCallsCount = 0 + open var pinUserIdentityUserIdCallsCount: Int { + get { + if Thread.isMainThread { + return pinUserIdentityUserIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinUserIdentityUserIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinUserIdentityUserIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinUserIdentityUserIdUnderlyingCallsCount = newValue + } + } + } + } + //MARK: - pinnedEventsTimeline open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError: Error? @@ -13068,6 +13213,77 @@ open class RoomSDKMock: MatrixRustSDK.Room { try await setUnreadFlagNewValueClosure?(newValue) } + //MARK: - subscribeToIdentityStatusChanges + + var subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = 0 + open var subscribeToIdentityStatusChangesListenerCallsCount: Int { + get { + if Thread.isMainThread { + return subscribeToIdentityStatusChangesListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = subscribeToIdentityStatusChangesListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + subscribeToIdentityStatusChangesListenerUnderlyingCallsCount = newValue + } + } + } + } + open var subscribeToIdentityStatusChangesListenerCalled: Bool { + return subscribeToIdentityStatusChangesListenerCallsCount > 0 + } + open var subscribeToIdentityStatusChangesListenerReceivedListener: IdentityStatusChangeListener? + open var subscribeToIdentityStatusChangesListenerReceivedInvocations: [IdentityStatusChangeListener] = [] + + var subscribeToIdentityStatusChangesListenerUnderlyingReturnValue: TaskHandle! + open var subscribeToIdentityStatusChangesListenerReturnValue: TaskHandle! { + get { + if Thread.isMainThread { + return subscribeToIdentityStatusChangesListenerUnderlyingReturnValue + } else { + var returnValue: TaskHandle? = nil + DispatchQueue.main.sync { + returnValue = subscribeToIdentityStatusChangesListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + subscribeToIdentityStatusChangesListenerUnderlyingReturnValue = newValue + } + } + } + } + open var subscribeToIdentityStatusChangesListenerClosure: ((IdentityStatusChangeListener) -> TaskHandle)? + + open override func subscribeToIdentityStatusChanges(listener: IdentityStatusChangeListener) -> TaskHandle { + subscribeToIdentityStatusChangesListenerCallsCount += 1 + subscribeToIdentityStatusChangesListenerReceivedListener = listener + DispatchQueue.main.async { + self.subscribeToIdentityStatusChangesListenerReceivedInvocations.append(listener) + } + if let subscribeToIdentityStatusChangesListenerClosure = subscribeToIdentityStatusChangesListenerClosure { + return subscribeToIdentityStatusChangesListenerClosure(listener) + } else { + return subscribeToIdentityStatusChangesListenerReturnValue + } + } + //MARK: - subscribeToRoomInfoUpdates var subscribeToRoomInfoUpdatesListenerUnderlyingCallsCount = 0 @@ -14087,77 +14303,6 @@ open class RoomListSDKMock: MatrixRustSDK.RoomList { fileprivate var pointer: UnsafeMutableRawPointer! - //MARK: - entries - - var entriesListenerUnderlyingCallsCount = 0 - open var entriesListenerCallsCount: Int { - get { - if Thread.isMainThread { - return entriesListenerUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = entriesListenerUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - entriesListenerUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - entriesListenerUnderlyingCallsCount = newValue - } - } - } - } - open var entriesListenerCalled: Bool { - return entriesListenerCallsCount > 0 - } - open var entriesListenerReceivedListener: RoomListEntriesListener? - open var entriesListenerReceivedInvocations: [RoomListEntriesListener] = [] - - var entriesListenerUnderlyingReturnValue: TaskHandle! - open var entriesListenerReturnValue: TaskHandle! { - get { - if Thread.isMainThread { - return entriesListenerUnderlyingReturnValue - } else { - var returnValue: TaskHandle? = nil - DispatchQueue.main.sync { - returnValue = entriesListenerUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - entriesListenerUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - entriesListenerUnderlyingReturnValue = newValue - } - } - } - } - open var entriesListenerClosure: ((RoomListEntriesListener) -> TaskHandle)? - - open override func entries(listener: RoomListEntriesListener) -> TaskHandle { - entriesListenerCallsCount += 1 - entriesListenerReceivedListener = listener - DispatchQueue.main.async { - self.entriesListenerReceivedInvocations.append(listener) - } - if let entriesListenerClosure = entriesListenerClosure { - return entriesListenerClosure(listener) - } else { - return entriesListenerReturnValue - } - } - //MARK: - entriesWithDynamicAdapters var entriesWithDynamicAdaptersPageSizeListenerUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 86e33a7a48..0855235a08 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -80,6 +80,7 @@ extension JoinedRoomProxyMock { membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() + identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher() joinedMembersCount = configuration.members.filter { $0.membership == .join }.count activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 9825257d0a..2074c014b1 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -25,6 +25,11 @@ extension RoomMemberProxyMock { self.init() userID = configuration.userID displayName = configuration.displayName + + if let displayName = configuration.displayName { + disambiguatedDisplayName = "\(displayName) (\(userID))" + } + avatarURL = configuration.avatarURL membership = configuration.membership diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index f1bbc1506d..16b4e2f495 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -12,6 +12,7 @@ import SwiftUI import WysiwygComposer struct RoomScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol let roomProxy: JoinedRoomProxyProtocol var focussedEvent: FocusEvent? let timelineController: RoomTimelineControllerProtocol @@ -61,13 +62,15 @@ final class RoomScreenCoordinator: CoordinatorProtocol { selectedPinnedEventID = focussedEvent.shouldSetPin ? focussedEvent.eventID : nil } - roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, + roomViewModel = RoomScreenViewModel(clientProxy: parameters.clientProxy, + roomProxy: parameters.roomProxy, initialSelectedPinnedEventID: selectedPinnedEventID, mediaProvider: parameters.mediaProvider, ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher, appMediator: parameters.appMediator, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEvent?.eventID, @@ -149,10 +152,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { .store(in: &cancellables) roomViewModel.actions - .sink { [weak self] actions in + .sink { [weak self] action in guard let self else { return } - switch actions { + switch action { case .focusEvent(eventID: let eventID): focusOnEvent(FocusEvent(eventID: eventID, shouldSetPin: false)) case .displayPinnedEventsTimeline: diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 9f4bb68a22..92c9bfdbe2 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -21,6 +21,7 @@ enum RoomScreenViewAction { case viewAllPins case displayRoomDetails case displayCall + case footerViewAction(RoomScreenFooterViewAction) } struct RoomScreenViewState: BindableState { @@ -38,11 +39,21 @@ struct RoomScreenViewState: BindableState { var hasOngoingCall: Bool var shouldShowCallButton = true + var footerDetails: RoomScreenFooterViewDetails? + var bindings: RoomScreenViewStateBindings } struct RoomScreenViewStateBindings { } +enum RoomScreenFooterViewAction { + case resolvePinViolation(userID: String) +} + +enum RoomScreenFooterViewDetails { + case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) +} + enum PinnedEventsBannerState: Equatable { case loading(numbersOfEvents: Int) case loaded(state: PinnedEventsState) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 08beb380b2..36f3bb7239 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -7,18 +7,24 @@ import Combine import Foundation +import MatrixRustSDK import OrderedCollections import SwiftUI typealias RoomScreenViewModelType = StateStoreViewModel class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol private let roomProxy: JoinedRoomProxyProtocol private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analyticsService: AnalyticsService - private let pinnedEventStringBuilder: RoomEventStringBuilder + private let userIndicatorController: UserIndicatorControllerProtocol + private var initialSelectedPinnedEventID: String? + private let pinnedEventStringBuilder: RoomEventStringBuilder + + private var identityPinningViolations = [String: RoomMemberProxyProtocol]() private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -43,17 +49,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - init(roomProxy: JoinedRoomProxyProtocol, + init(clientProxy: ClientProxyProtocol, + roomProxy: JoinedRoomProxyProtocol, initialSelectedPinnedEventID: String?, mediaProvider: MediaProviderProtocol, ongoingCallRoomIDPublisher: CurrentValuePublisher, appMediator: AppMediatorProtocol, appSettings: AppSettings, - analyticsService: AnalyticsService) { + analyticsService: AnalyticsService, + userIndicatorController: UserIndicatorControllerProtocol) { + self.clientProxy = clientProxy self.roomProxy = roomProxy self.appMediator = appMediator self.appSettings = appSettings self.analyticsService = analyticsService + self.userIndicatorController = userIndicatorController + self.initialSelectedPinnedEventID = initialSelectedPinnedEventID pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) @@ -87,6 +98,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayCall) actionsSubject.send(.removeComposerFocus) analyticsService.trackInteraction(name: .MobileRoomCallButton) + case .footerViewAction(let action): + switch action { + case .resolvePinViolation(let userID): + Task { await resolveIdentityPinningViolation(userID) } + } } } @@ -98,6 +114,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.pinnedEventsBannerState.setSelectedPinnedEventID(eventID) } + // MARK: - Private + private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher) { let roomInfoSubscription = roomProxy .actionsPublisher @@ -124,6 +142,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } .store(in: &cancellables) + let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main) + + Task { [weak self] in + for await changes in identityStatusChangesPublisher.values { + guard !Task.isCancelled else { + return + } + + await self?.processIdentityStatusChanges(changes) + } + } + .store(in: &cancellables) + appMediator.networkMonitor.reachabilityPublisher .filter { $0 == .reachable } .receive(on: DispatchQueue.main) @@ -141,6 +172,43 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .store(in: &cancellables) } + private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async { + for change in changes { + switch change.changedTo { + case .pinned: + identityPinningViolations[change.userId] = nil + case .pinViolation: + guard case let .success(member) = await roomProxy.getMember(userID: change.userId) else { + MXLog.error("Failed retrieving room member for identity status change: \(change)") + continue + } + + identityPinningViolations[change.userId] = member + default: + break + } + } + + if let member = identityPinningViolations.values.first { + state.footerDetails = .pinViolation(member: member, + learnMoreURL: appSettings.identityPinningViolationDetailsURL) + } else { + state.footerDetails = nil + } + } + + private func resolveIdentityPinningViolation(_ userID: String) async { + defer { + hideLoadingIndicator() + } + + showLoadingIndicator() + + if case .failure = await clientProxy.pinUserIdentity(userID) { + userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) + } + } + private func buildPinnedEventContents(timelineItems: [TimelineItemProxy]) { var pinnedEventContents = OrderedDictionary() @@ -190,16 +258,30 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } } + + // MARK: Loading indicators + + private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading" + + private func showLoadingIndicator() { + userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading)) + } + + private func hideLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } } extension RoomScreenViewModel { static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel { - RoomScreenViewModel(roomProxy: roomProxyMock, + RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index dee0516fb5..72a77e4480 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -29,21 +29,28 @@ struct RoomScreen: View { timeline .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .safeAreaInset(edge: .bottom, spacing: 0) { - composerToolbar - .padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12) - .background { - if composerToolbarContext.composerFormattingEnabled { - RoundedRectangle(cornerRadius: 20) - .stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5) - .ignoresSafeArea() - } + VStack(spacing: 0) { + RoomScreenFooterView(details: roomContext.viewState.footerDetails, + mediaProvider: roomContext.mediaProvider) { action in + roomContext.send(viewAction: .footerViewAction(action)) } - .padding(.top, 8) - .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) - .environmentObject(timelineContext) - .environment(\.timelineContext, timelineContext) - // Make sure the reply header honours the hideTimelineMedia setting too. - .environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia) + + composerToolbar + .padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12) + .background { + if composerToolbarContext.composerFormattingEnabled { + RoundedRectangle(cornerRadius: 20) + .stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5) + .ignoresSafeArea() + } + } + .padding(.top, 8) + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + .environmentObject(timelineContext) + .environment(\.timelineContext, timelineContext) + // Make sure the reply header honours the hideTimelineMedia setting too. + .environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia) + } } .overlay(alignment: .top) { Group { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift new file mode 100644 index 0000000000..ec9e7df87f --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreenFooterView.swift @@ -0,0 +1,78 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import SwiftUI + +struct RoomScreenFooterView: View { + let details: RoomScreenFooterViewDetails? + let mediaProvider: MediaProviderProtocol? + let callback: (RoomScreenFooterViewAction) -> Void + + var body: some View { + if let details { + ZStack(alignment: .top) { + VStack(spacing: 0) { + Color.compound.borderInfoSubtle + .frame(height: 1) + LinearGradient(colors: [.compound.bgInfoSubtle, .compound.bgCanvasDefault], + startPoint: .top, + endPoint: .bottom) + } + + switch details { + case .pinViolation(let member, let learnMoreURL): + pinViolation(member: member, learnMoreURL: learnMoreURL) + } + } + .padding(.top, 8) + .fixedSize(horizontal: false, vertical: true) + } + } + + private func pinViolation(member: RoomMemberProxyProtocol, + learnMoreURL: URL) -> some View { + VStack(spacing: 16) { + HStack(spacing: 16) { + LoadableAvatarImage(url: member.avatarURL, + name: member.disambiguatedDisplayName, + contentID: member.userID, + avatarSize: .user(on: .timeline), + mediaProvider: mediaProvider) + + Text(pinViolationDescriptionWithLearnMoreLink(displayName: member.disambiguatedDisplayName ?? member.userID, + url: learnMoreURL)) + .font(.compound.bodyMD) + .foregroundColor(.compound.textPrimary) + } + + Button(L10n.actionOk) { + callback(.resolvePinViolation(userID: member.userID)) + } + .buttonStyle(.compound(.primary, size: .medium)) + } + .padding(.top, 16) + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + + private func pinViolationDescriptionWithLearnMoreLink(displayName: String, url: URL) -> AttributedString { + let linkPlaceholder = "{link}" + var description = AttributedString(L10n.cryptoIdentityChangePinViolation(displayName, linkPlaceholder)) + var linkString = AttributedString(L10n.actionLearnMore) + linkString.link = url + linkString.bold() + description.replace(linkPlaceholder, with: linkString) + return description + } +} + +struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + RoomScreenFooterView(details: .pinViolation(member: RoomMemberProxyMock.mockBob, learnMoreURL: "https://element.io/"), + mediaProvider: MediaProviderMock(configuration: .init())) { _ in } + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 9a05854447..3ab6cfaac8 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -895,6 +895,22 @@ class ClientProxy: ClientProxyProtocol { await client.encryption().curve25519Key() } + func pinUserIdentity(_ userID: String) async -> Result { + MXLog.info("Pinning current identity for user: \(userID)") + + do { + guard let userIdentity = try await client.encryption().getUserIdentity(userId: userID) else { + MXLog.error("Failed retrieving identity for user: \(userID)") + return .failure(.failedRetrievingUserIdentity) + } + + return try await .success(userIdentity.pin()) + } catch { + MXLog.error("Failed pinning current identity for user: \(error)") + return .failure(.sdkError(error)) + } + } + func resetIdentity() async -> Result { do { return try await .success(client.encryption().resetIdentity()) diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 29a5a856cb..a942ec638e 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -35,6 +35,7 @@ enum ClientProxyError: Error { case invalidServerName case failedUploadingMedia(Error, MatrixErrorCode) case roomPreviewIsPrivate + case failedRetrievingUserIdentity } enum SlidingSyncConstants { @@ -196,5 +197,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func ed25519Base64() async -> String? func curve25519Base64() async -> String? + func pinUserIdentity(_ userID: String) async -> Result func resetIdentity() async -> Result } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 9555d62e8b..049f4c49bf 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -295,10 +295,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe roomProxy .actionsPublisher - .map { action -> (Bool, [String]) in + .compactMap { action -> (Bool, [String])? in switch action { case .roomInfoUpdate: return (roomProxy.hasOngoingCall, roomProxy.activeRoomCallParticipants) + default: + return nil } } .removeDuplicates { $0 == $1 } diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index b1fcd8bf7c..cdcd4fc19c 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -58,6 +58,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { private var roomInfoObservationToken: TaskHandle? // periphery:ignore - required for instance retention in the rust codebase private var typingNotificationObservationToken: TaskHandle? + // periphery:ignore - required for instance retention in the rust codebase + private var identityStatusChangesObservationToken: TaskHandle? private var subscribedForUpdates = false @@ -70,6 +72,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { var typingMembersPublisher: CurrentValuePublisher<[String], Never> { typingMembersSubject.asCurrentValuePublisher() } + + private let identityStatusChangesSubject = CurrentValueSubject<[IdentityStatusChange], Never>([]) + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { + identityStatusChangesSubject.asCurrentValuePublisher() + } private let actionsSubject = PassthroughSubject() var actionsPublisher: AnyPublisher { @@ -193,6 +200,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { subscribeToRoomInfoUpdates() + subscribeToIdentityStatusChanges() + subscribeToTypingNotifications() } @@ -708,6 +717,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { typingMembersSubject.send(typingMembers) }) } + + private func subscribeToIdentityStatusChanges() { + identityStatusChangesObservationToken = room.subscribeToIdentityStatusChanges(listener: RoomIdentityStatusChangeListener { [weak self] changes in + guard let self else { return } + + MXLog.info("Received identity status changes: \(changes)") + + identityStatusChangesSubject.send(changes) + }) + } } private final class RoomInfoUpdateListener: RoomInfoListener { @@ -733,3 +752,15 @@ private final class RoomTypingNotificationUpdateListener: TypingNotificationsLis onUpdateClosure(typingUserIds) } } + +private final class RoomIdentityStatusChangeListener: IdentityStatusChangeListener { + private let onUpdateClosure: ([IdentityStatusChange]) -> Void + + init(_ onUpdateClosure: @escaping ([IdentityStatusChange]) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func call(identityStatusChange: [IdentityStatusChange]) { + onUpdateClosure(identityStatusChange) + } +} diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift index 856ec67ae2..e583a83c14 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift @@ -16,12 +16,24 @@ final class RoomMemberProxy: RoomMemberProxyProtocol { } var userID: String { member.userId } + var displayName: String? { member.displayName } + + var disambiguatedDisplayName: String? { + guard let displayName else { + return nil + } + + return member.isNameAmbiguous ? "\(displayName) (\(userID))" : displayName + } + var avatarURL: URL? { member.avatarUrl.flatMap(URL.init(string:)) } var membership: MembershipState { member.membership } + var isIgnored: Bool { member.isIgnored } var powerLevel: Int { Int(member.powerLevel) } + var role: RoomMemberRole { member.suggestedRoleForPowerLevel } } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index 6c1fe0afe2..817f6b57d4 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -11,13 +11,18 @@ import MatrixRustSDK // sourcery: AutoMockable protocol RoomMemberProxyProtocol: AnyObject { var userID: String { get } + var displayName: String? { get } + var disambiguatedDisplayName: String? { get } + var avatarURL: URL? { get } var membership: MembershipState { get } + var isIgnored: Bool { get } var powerLevel: Int { get } + var role: RoomMemberRole { get } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index d2d99406cb..552e0484d6 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -56,7 +56,7 @@ protocol InvitedRoomProxyProtocol: RoomProxyProtocol { func acceptInvitation() async -> Result } -enum JoinedRoomProxyAction { +enum JoinedRoomProxyAction: Equatable { case roomInfoUpdate } @@ -73,6 +73,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get } + var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get } + var actionsPublisher: AnyPublisher { get } var timeline: TimelineProxyProtocol { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift index 847e0ce9d5..67b50ea408 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift @@ -19,7 +19,7 @@ enum EncryptionAuthenticity: Hashable { case unknownDevice(color: Color) case unsignedDevice(color: Color) case unverifiedIdentity(color: Color) - case previouslyVerified(color: Color) + case verificationViolation(color: Color) case sentInClear(color: Color) var message: String { @@ -32,7 +32,7 @@ enum EncryptionAuthenticity: Hashable { L10n.eventShieldReasonUnsignedDevice case .unverifiedIdentity: L10n.eventShieldReasonUnverifiedIdentity - case .previouslyVerified: + case .verificationViolation: L10n.eventShieldReasonPreviouslyVerified case .sentInClear: L10n.eventShieldReasonSentInClear @@ -45,7 +45,7 @@ enum EncryptionAuthenticity: Hashable { .unknownDevice(let color), .unsignedDevice(let color), .unverifiedIdentity(let color), - .previouslyVerified(let color), + .verificationViolation(let color), .sentInClear(let color): color } @@ -54,7 +54,7 @@ enum EncryptionAuthenticity: Hashable { var icon: KeyPath { switch self { case .notGuaranteed: \.info - case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .previouslyVerified: \.helpSolid + case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .verificationViolation: \.helpSolid case .sentInClear: \.lockOff } } @@ -82,8 +82,8 @@ extension EncryptionAuthenticity { self = .unsignedDevice(color: color) case .unverifiedIdentity: self = .unverifiedIdentity(color: color) - case .previouslyVerified: - self = .previouslyVerified(color: color) + case .verificationViolation: + self = .verificationViolation(color: color) case .sentInClear: self = .sentInClear(color: color) } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 493e333020..1c125258bc 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -233,7 +233,8 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomPlainNoAvatar: let navigationStackCoordinator = NavigationStackCoordinator() - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: MockRoomTimelineController(), mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -251,7 +252,8 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -269,7 +271,8 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.default - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -287,7 +290,8 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -308,7 +312,8 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -329,7 +334,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -350,7 +356,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -372,7 +379,8 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -393,7 +401,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController(listenForSignals: true) timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -413,7 +422,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -447,7 +457,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -468,7 +479,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), @@ -489,7 +501,8 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls timelineController.incomingItems = [] - let parameters = RoomScreenCoordinatorParameters(roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), + let parameters = RoomScreenCoordinatorParameters(clientProxy: ClientProxyMock(), + roomProxy: JoinedRoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 5819975f1c..615b072af3 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -689,6 +689,12 @@ extension PreviewTests { } } + func test_roomScreenFooterView() { + for preview in RoomScreenFooterView_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_roomScreen() { for preview in RoomScreen_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-en-GB.1.png new file mode 100644 index 0000000000..43ba189302 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff5ac1cb8df1556b0d4ba1f119e65a182cb3ece6003f58db8a6129ef701dd94 +size 156835 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-pseudo.1.png new file mode 100644 index 0000000000..8e484ff041 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPad-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9693534082a1948f05d6f50c7e0cb8db768fdf6fe01189af4df64638edaae37 +size 166386 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-en-GB.1.png new file mode 100644 index 0000000000..2019d816bd --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b02b43b978488a8e7b81d2767862a9a5289df638c502570557ef5f5e7a8dd4cc +size 77863 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-pseudo.1.png new file mode 100644 index 0000000000..658879046e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreenFooterView-iPhone-16-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7ab7912c6041b90042ed19c52ae21ee6ea4e88f5592b692607e7f8eb01adc40 +size 99897 diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 99019ac587..66e069fab1 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -218,9 +218,32 @@ class LoggingTests: XCTestCase { let rustEmoteMessage = EmoteMessageContent(body: emoteString, formatted: FormattedBody(format: .html, body: "\(emoteString)")) - let rustImageMessage = ImageMessageContent(body: "ImageString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustVideoMessage = VideoMessageContent(body: "VideoString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil) - let rustFileMessage = FileMessageContent(body: "FileString", formatted: nil, filename: "FileName", source: MediaSource(noPointer: .init()), info: nil) + let rustImageMessage = ImageMessageContent(body: "ImageString", + formatted: nil, + rawFilename: "ImageString", + filename: "ImageString", + caption: "ImageString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) + + let rustVideoMessage = VideoMessageContent(body: "VideoString", + formatted: nil, + rawFilename: "VideoString", + filename: "VideoString", + caption: "VideoString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) + + let rustFileMessage = FileMessageContent(body: "FileString", + formatted: nil, + rawFilename: "FileString", + filename: "FileString", + caption: "FileString", + formattedCaption: nil, + source: MediaSource(noPointer: .init()), + info: nil) // When logging that value MXLog.info(rustTextMessage) diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index da9196d2cb..43b8cf10c9 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -33,13 +33,15 @@ class RoomScreenViewModelTests: XCTestCase { } // setup the room proxy actions publisher roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel // check if in the default state is not showing but is indeed loading @@ -111,13 +113,15 @@ class RoomScreenViewModelTests: XCTestCase { .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: "2")), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: "3"))] roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: "test1", mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel // check if the banner is now in a loaded state and is showing the counter @@ -158,13 +162,15 @@ class RoomScreenViewModelTests: XCTestCase { // setup the room proxy actions publisher roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false) roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel var deferred = deferFulfillment(viewModel.context.$viewState) { viewState in @@ -195,13 +201,15 @@ class RoomScreenViewModelTests: XCTestCase { // Given a room screen with no ongoing call. let ongoingCallRoomIDSubject = CurrentValueSubject(nil) let roomProxyMock = JoinedRoomProxyMock(.init(id: "MyRoomID")) - let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, + let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), + roomProxy: roomProxyMock, initialSelectedPinnedEventID: nil, mediaProvider: MediaProviderMock(configuration: .init()), ongoingCallRoomIDPublisher: ongoingCallRoomIDSubject.asCurrentValuePublisher(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + userIndicatorController: ServiceLocator.shared.userIndicatorController) self.viewModel = viewModel XCTAssertTrue(viewModel.state.shouldShowCallButton) diff --git a/project.yml b/project.yml index 48265b0c99..d0c6ff5b5a 100644 --- a/project.yml +++ b/project.yml @@ -60,7 +60,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.53 + exactVersion: 1.0.55 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios