diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 72a891454..f23754c4a 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -278,7 +278,6 @@ 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; - 69ABFBAF05D7EF11E7C88CEA /* EncryptionSyncListenerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; @@ -723,7 +722,6 @@ F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */; }; - F91B4629E4AF51A4FE8E7608 /* EncryptionSyncListenerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */; }; F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; }; F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; @@ -1071,7 +1069,6 @@ 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; - 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSyncListenerProxy.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = ""; }; 69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = ""; }; @@ -2649,7 +2646,6 @@ 832FC81F760220239E285294 /* Proxy */ = { isa = PBXGroup; children = ( - 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */, 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */, ); path = Proxy; @@ -4004,7 +4000,6 @@ 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */, 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, - F91B4629E4AF51A4FE8E7608 /* EncryptionSyncListenerProxy.swift in Sources */, A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, A3E390675E9730C176B59E1B /* ImageProviderProtocol.swift in Sources */, @@ -4262,7 +4257,6 @@ 9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */, 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */, B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, - 69ABFBAF05D7EF11E7C88CEA /* EncryptionSyncListenerProxy.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, @@ -5289,7 +5283,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "1.0.96-alpha"; + version = "1.0.98-alpha"; }; }; 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a45c37c8a..81c45ef53 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -111,8 +111,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "8bc8015237083035cb5f4a00d1eedb9ebbbb83c6", - "version" : "1.0.96-alpha" + "revision" : "985708733af7d2db1684f90f0a954854ca3a83ad", + "version" : "1.0.98-alpha" } }, { diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme index 81b1b3191..af399e45d 100644 --- a/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ElementX.xcscheme @@ -4,7 +4,8 @@ version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> + + + + + + + + - - - - + + + + diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 9d454bee4..8b51ea37d 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -542,7 +542,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, // MARK: - Application State private func stopSync() { - userSession?.clientProxy.stopSync() + userSession?.clientProxy.pauseSync() backgroundAppRefreshTask?.setTaskCompleted(success: true) backgroundAppRefreshTask = nil @@ -602,7 +602,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, @objc private func applicationWillTerminate() { - userSession?.clientProxy.stopSync() + userSession?.clientProxy.pauseSync() } @objc @@ -616,7 +616,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in guard let self else { return } - userSession?.clientProxy.stopSync() + userSession?.clientProxy.pauseSync() backgroundTask?.stop() backgroundTask = nil diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 591e6cf71..7118f51aa 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -207,8 +207,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomMemberDetails, .dismissRoomMemberDetails, .room): break - case (.room, .presentMessageForwarding(let eventID), .messageForwarding): - presentMessageForwarding(for: eventID) + case (.room, .presentMessageForwarding(let itemID), .messageForwarding): + presentMessageForwarding(for: itemID) case (.messageForwarding, .dismissMessageForwarding, .room): break case (.room, .presentMapNavigator(let mode), .mapNavigator): @@ -401,14 +401,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentReportContent(for itemID: String, from senderID: String) { - guard let roomProxy else { + private func presentReportContent(for itemID: TimelineItemIdentifier, from senderID: String) { + guard let roomProxy, let eventID = itemID.eventID else { fatalError() } let navigationCoordinator = NavigationStackCoordinator() let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator) - let parameters = ReportContentScreenCoordinatorParameters(itemID: itemID, + let parameters = ReportContentScreenCoordinatorParameters(eventID: eventID, senderID: senderID, roomProxy: roomProxy, userIndicatorController: userIndicatorController) @@ -479,17 +479,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentEmojiPicker(for itemId: String) { + private func presentEmojiPicker(for itemID: TimelineItemIdentifier) { let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider, - itemId: itemId) + itemID: itemID) let coordinator = EmojiPickerScreenCoordinator(parameters: params) coordinator.callback = { [weak self] action in switch action { - case let .emojiSelected(emoji: emoji, itemId: itemId): - MXLog.debug("Selected \(emoji) for \(itemId)") + case let .emojiSelected(emoji: emoji, itemID: itemID): + MXLog.debug("Selected \(emoji) for \(itemID)") self?.navigationStackCoordinator.setSheetCoordinator(nil) Task { - await self?.timelineController?.toggleReaction(emoji, to: itemId) + await self?.timelineController?.toggleReaction(emoji, to: itemID) } case .dismiss: self?.navigationStackCoordinator.setSheetCoordinator(nil) @@ -547,8 +547,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentMessageForwarding(for eventID: String) { - guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider else { + private func presentMessageForwarding(for itemID: TimelineItemIdentifier) { + guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider, let eventID = itemID.eventID else { fatalError() } @@ -624,14 +624,14 @@ private extension RoomFlowCoordinator { enum State: StateType { case initial case room(roomID: String) - case reportContent(roomID: String, itemID: String, senderID: String) + case reportContent(roomID: String, itemID: TimelineItemIdentifier, senderID: String) case roomDetails(roomID: String, isRoot: Bool) case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource) case mediaUploadPreview(roomID: String, fileURL: URL) - case emojiPicker(roomID: String, itemID: String) + case emojiPicker(roomID: String, itemID: TimelineItemIdentifier) case mapNavigator(roomID: String) case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) - case messageForwarding(roomID: String, itemID: String) + case messageForwarding(roomID: String, itemID: TimelineItemIdentifier) } struct EventUserInfo { @@ -643,7 +643,7 @@ private extension RoomFlowCoordinator { case presentRoom(roomID: String) case dismissRoom - case presentReportContent(itemID: String, senderID: String) + case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) case dismissReportContent case presentRoomDetails(roomID: String) @@ -655,7 +655,7 @@ private extension RoomFlowCoordinator { case presentMediaUploadPreview(fileURL: URL) case dismissMediaUploadPreview - case presentEmojiPicker(itemID: String) + case presentEmojiPicker(itemID: TimelineItemIdentifier) case dismissEmojiPicker case presentMapNavigator(interactionMode: StaticLocationInteractionMode) @@ -664,7 +664,7 @@ private extension RoomFlowCoordinator { case presentRoomMemberDetails(member: HashableRoomMemberWrapper) case dismissRoomMemberDetails - case presentMessageForwarding(itemID: String) + case presentMessageForwarding(itemID: TimelineItemIdentifier) case dismissMessageForwarding } } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 8c4d97280..b34a789ac 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -31,6 +31,23 @@ class SDKClientMock: SDKClientProtocol { return accountDataEventTypeReturnValue } } + //MARK: - `app` + + public var appCallsCount = 0 + public var appCalled: Bool { + return appCallsCount > 0 + } + public var appReturnValue: AppBuilder! + public var appClosure: (() -> AppBuilder)? + + public func `app`() -> AppBuilder { + appCallsCount += 1 + if let appClosure = appClosure { + return appClosure() + } else { + return appReturnValue + } + } //MARK: - `avatarUrl` public var avatarUrlThrowableError: Error? @@ -240,29 +257,21 @@ class SDKClientMock: SDKClientProtocol { return getMediaThumbnailMediaSourceWidthHeightReturnValue } } - //MARK: - `getNotificationItem` + //MARK: - `getNotificationSettings` - public var getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError: Error? - public var getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount = 0 - public var getNotificationItemRoomIdEventIdFilterByPushRulesCalled: Bool { - return getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount > 0 + public var getNotificationSettingsCallsCount = 0 + public var getNotificationSettingsCalled: Bool { + return getNotificationSettingsCallsCount > 0 } - public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedArguments: (`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)? - public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedInvocations: [(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)] = [] - public var getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue: NotificationItem? - public var getNotificationItemRoomIdEventIdFilterByPushRulesClosure: ((String, String, Bool) throws -> NotificationItem?)? + public var getNotificationSettingsReturnValue: NotificationSettings! + public var getNotificationSettingsClosure: (() -> NotificationSettings)? - public func `getNotificationItem`(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool) throws -> NotificationItem? { - if let error = getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError { - throw error - } - getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount += 1 - getNotificationItemRoomIdEventIdFilterByPushRulesReceivedArguments = (roomId: roomId, eventId: eventId, filterByPushRules: filterByPushRules) - getNotificationItemRoomIdEventIdFilterByPushRulesReceivedInvocations.append((roomId: roomId, eventId: eventId, filterByPushRules: filterByPushRules)) - if let getNotificationItemRoomIdEventIdFilterByPushRulesClosure = getNotificationItemRoomIdEventIdFilterByPushRulesClosure { - return try getNotificationItemRoomIdEventIdFilterByPushRulesClosure(`roomId`, `eventId`, `filterByPushRules`) + public func `getNotificationSettings`() -> NotificationSettings { + getNotificationSettingsCallsCount += 1 + if let getNotificationSettingsClosure = getNotificationSettingsClosure { + return getNotificationSettingsClosure() } else { - return getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue + return getNotificationSettingsReturnValue } } //MARK: - `getProfile` @@ -384,54 +393,21 @@ class SDKClientMock: SDKClientProtocol { logoutCallsCount += 1 try logoutClosure?() } - //MARK: - `mainEncryptionSync` + //MARK: - `notificationClient` - public var mainEncryptionSyncIdListenerThrowableError: Error? - public var mainEncryptionSyncIdListenerCallsCount = 0 - public var mainEncryptionSyncIdListenerCalled: Bool { - return mainEncryptionSyncIdListenerCallsCount > 0 + public var notificationClientCallsCount = 0 + public var notificationClientCalled: Bool { + return notificationClientCallsCount > 0 } - public var mainEncryptionSyncIdListenerReceivedArguments: (`id`: String, `listener`: EncryptionSyncListener)? - public var mainEncryptionSyncIdListenerReceivedInvocations: [(`id`: String, `listener`: EncryptionSyncListener)] = [] - public var mainEncryptionSyncIdListenerReturnValue: EncryptionSync! - public var mainEncryptionSyncIdListenerClosure: ((String, EncryptionSyncListener) throws -> EncryptionSync)? + public var notificationClientReturnValue: NotificationClientBuilder! + public var notificationClientClosure: (() -> NotificationClientBuilder)? - public func `mainEncryptionSync`(`id`: String, `listener`: EncryptionSyncListener) throws -> EncryptionSync { - if let error = mainEncryptionSyncIdListenerThrowableError { - throw error - } - mainEncryptionSyncIdListenerCallsCount += 1 - mainEncryptionSyncIdListenerReceivedArguments = (id: id, listener: listener) - mainEncryptionSyncIdListenerReceivedInvocations.append((id: id, listener: listener)) - if let mainEncryptionSyncIdListenerClosure = mainEncryptionSyncIdListenerClosure { - return try mainEncryptionSyncIdListenerClosure(`id`, `listener`) + public func `notificationClient`() -> NotificationClientBuilder { + notificationClientCallsCount += 1 + if let notificationClientClosure = notificationClientClosure { + return notificationClientClosure() } else { - return mainEncryptionSyncIdListenerReturnValue - } - } - //MARK: - `notificationEncryptionSync` - - public var notificationEncryptionSyncIdListenerNumItersThrowableError: Error? - public var notificationEncryptionSyncIdListenerNumItersCallsCount = 0 - public var notificationEncryptionSyncIdListenerNumItersCalled: Bool { - return notificationEncryptionSyncIdListenerNumItersCallsCount > 0 - } - public var notificationEncryptionSyncIdListenerNumItersReceivedArguments: (`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8)? - public var notificationEncryptionSyncIdListenerNumItersReceivedInvocations: [(`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8)] = [] - public var notificationEncryptionSyncIdListenerNumItersReturnValue: EncryptionSync! - public var notificationEncryptionSyncIdListenerNumItersClosure: ((String, EncryptionSyncListener, UInt8) throws -> EncryptionSync)? - - public func `notificationEncryptionSync`(`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8) throws -> EncryptionSync { - if let error = notificationEncryptionSyncIdListenerNumItersThrowableError { - throw error - } - notificationEncryptionSyncIdListenerNumItersCallsCount += 1 - notificationEncryptionSyncIdListenerNumItersReceivedArguments = (id: id, listener: listener, numIters: numIters) - notificationEncryptionSyncIdListenerNumItersReceivedInvocations.append((id: id, listener: listener, numIters: numIters)) - if let notificationEncryptionSyncIdListenerNumItersClosure = notificationEncryptionSyncIdListenerNumItersClosure { - return try notificationEncryptionSyncIdListenerNumItersClosure(`id`, `listener`, `numIters`) - } else { - return notificationEncryptionSyncIdListenerNumItersReturnValue + return notificationClientReturnValue } } //MARK: - `restoreSession` @@ -454,48 +430,6 @@ class SDKClientMock: SDKClientProtocol { restoreSessionSessionReceivedInvocations.append(`session`) try restoreSessionSessionClosure?(`session`) } - //MARK: - `roomListService` - - public var roomListServiceThrowableError: Error? - public var roomListServiceCallsCount = 0 - public var roomListServiceCalled: Bool { - return roomListServiceCallsCount > 0 - } - public var roomListServiceReturnValue: RoomListService! - public var roomListServiceClosure: (() throws -> RoomListService)? - - public func `roomListService`() throws -> RoomListService { - if let error = roomListServiceThrowableError { - throw error - } - roomListServiceCallsCount += 1 - if let roomListServiceClosure = roomListServiceClosure { - return try roomListServiceClosure() - } else { - return roomListServiceReturnValue - } - } - //MARK: - `roomListServiceWithEncryption` - - public var roomListServiceWithEncryptionThrowableError: Error? - public var roomListServiceWithEncryptionCallsCount = 0 - public var roomListServiceWithEncryptionCalled: Bool { - return roomListServiceWithEncryptionCallsCount > 0 - } - public var roomListServiceWithEncryptionReturnValue: RoomListService! - public var roomListServiceWithEncryptionClosure: (() throws -> RoomListService)? - - public func `roomListServiceWithEncryption`() throws -> RoomListService { - if let error = roomListServiceWithEncryptionThrowableError { - throw error - } - roomListServiceWithEncryptionCallsCount += 1 - if let roomListServiceWithEncryptionClosure = roomListServiceWithEncryptionClosure { - return try roomListServiceWithEncryptionClosure() - } else { - return roomListServiceWithEncryptionReturnValue - } - } //MARK: - `rooms` public var roomsCallsCount = 0 @@ -615,22 +549,6 @@ class SDKClientMock: SDKClientProtocol { setDisplayNameNameReceivedInvocations.append(`name`) try setDisplayNameNameClosure?(`name`) } - //MARK: - `setNotificationDelegate` - - public var setNotificationDelegateNotificationDelegateCallsCount = 0 - public var setNotificationDelegateNotificationDelegateCalled: Bool { - return setNotificationDelegateNotificationDelegateCallsCount > 0 - } - public var setNotificationDelegateNotificationDelegateReceivedNotificationDelegate: NotificationDelegate? - public var setNotificationDelegateNotificationDelegateReceivedInvocations: [NotificationDelegate?] = [] - public var setNotificationDelegateNotificationDelegateClosure: ((NotificationDelegate?) -> Void)? - - public func `setNotificationDelegate`(`notificationDelegate`: NotificationDelegate?) { - setNotificationDelegateNotificationDelegateCallsCount += 1 - setNotificationDelegateNotificationDelegateReceivedNotificationDelegate = notificationDelegate - setNotificationDelegateNotificationDelegateReceivedInvocations.append(`notificationDelegate`) - setNotificationDelegateNotificationDelegateClosure?(`notificationDelegate`) - } //MARK: - `setPusher` public var setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangThrowableError: Error? diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift index 4c1043ce6..c63a666dd 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift @@ -18,11 +18,11 @@ import SwiftUI struct EmojiPickerScreenCoordinatorParameters { let emojiProvider: EmojiProviderProtocol - let itemId: String + let itemID: TimelineItemIdentifier } enum EmojiPickerScreenCoordinatorAction { - case emojiSelected(emoji: String, itemId: String) + case emojiSelected(emoji: String, itemID: TimelineItemIdentifier) case dismiss } @@ -44,7 +44,7 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol { switch action { case let .emojiSelected(emoji: emoji): - self.callback?(.emojiSelected(emoji: emoji, itemId: self.parameters.itemId)) + self.callback?(.emojiSelected(emoji: emoji, itemID: self.parameters.itemID)) case .dismiss: self.callback?(.dismiss) } diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift index 4b4700825..9efc705d8 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift @@ -18,7 +18,7 @@ import Combine import SwiftUI struct ReportContentScreenCoordinatorParameters { - let itemID: String + let eventID: String let senderID: String let roomProxy: RoomProxyProtocol weak var userIndicatorController: UserIndicatorControllerProtocol? @@ -39,7 +39,7 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol { init(parameters: ReportContentScreenCoordinatorParameters) { self.parameters = parameters - viewModel = ReportContentScreenViewModel(itemID: parameters.itemID, senderID: parameters.senderID, roomProxy: parameters.roomProxy) + viewModel = ReportContentScreenViewModel(eventID: parameters.eventID, senderID: parameters.senderID, roomProxy: parameters.roomProxy) } // MARK: - Public diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift index 91825bc9b..7c09857a8 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift @@ -20,7 +20,7 @@ import SwiftUI typealias ReportContentScreenViewModelType = StateStoreViewModel class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportContentScreenViewModelProtocol { - private let itemID: String + private let eventID: String private let senderID: String private let roomProxy: RoomProxyProtocol private let actionsSubject: PassthroughSubject = .init() @@ -29,8 +29,8 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont actionsSubject.eraseToAnyPublisher() } - init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) { - self.itemID = itemID + init(eventID: String, senderID: String, roomProxy: RoomProxyProtocol) { + self.eventID = eventID self.senderID = senderID self.roomProxy = roomProxy @@ -53,7 +53,7 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont private func submitReport() async { actionsSubject.send(.submitStarted) - if case let .failure(error) = await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) { + if case let .failure(error) = await roomProxy.reportContent(eventID, reason: state.bindings.reasonText) { MXLog.error("Submit Report Content failed: \(error)") actionsSubject.send(.submitFailed(error: error)) return diff --git a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift index 51e734fa4..b5b77bd67 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift @@ -84,7 +84,7 @@ struct ReportContentScreen: View { // MARK: - Previews struct ReportContentScreen_Previews: PreviewProvider { - static let viewModel = ReportContentScreenViewModel(itemID: "", + static let viewModel = ReportContentScreenViewModel(eventID: "", senderID: "", roomProxy: RoomProxyMock(with: .init(displayName: nil))) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index e1c9269d3..a3196825d 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -25,15 +25,15 @@ struct RoomScreenCoordinatorParameters { } enum RoomScreenCoordinatorAction { - case presentReportContent(itemID: String, senderID: String) + case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) case presentMediaUploadPicker(MediaPickerScreenSource) case presentMediaUploadPreviewScreen(URL) case presentRoomDetails case presentLocationPicker case presentLocationViewer(body: String, geoURI: GeoURI, description: String?) - case presentEmojiPicker(itemID: String) + case presentEmojiPicker(itemID: TimelineItemIdentifier) case presentRoomMemberDetails(member: RoomMemberProxyProtocol) - case presentMessageForwarding(itemID: String) + case presentMessageForwarding(itemID: TimelineItemIdentifier) } final class RoomScreenCoordinator: CoordinatorProtocol { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 82ccaa166..9f49fa4e5 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -22,22 +22,22 @@ import OrderedCollections enum RoomScreenViewModelAction { case displayRoomDetails - case displayEmojiPicker(itemID: String) - case displayReportContent(itemID: String, senderID: String) + case displayEmojiPicker(itemID: TimelineItemIdentifier) + case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) case displayCameraPicker case displayMediaPicker case displayDocumentPicker case displayLocationPicker case displayMediaUploadPreviewScreen(url: URL) case displayRoomMemberDetails(member: RoomMemberProxyProtocol) - case displayMessageForwarding(itemID: String) + case displayMessageForwarding(itemID: TimelineItemIdentifier) case displayLocation(body: String, geoURI: GeoURI, description: String?) } enum RoomScreenComposerMode: Equatable { case `default` - case reply(itemID: String, replyDetails: TimelineItemReplyDetails) - case edit(originalItemId: String) + case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails) + case edit(originalItemId: TimelineItemIdentifier) var isEdit: Bool { switch self { @@ -52,21 +52,21 @@ enum RoomScreenComposerMode: Equatable { enum RoomScreenViewAction { case displayRoomDetails case paginateBackwards - case itemAppeared(id: String) - case itemDisappeared(id: String) - case itemTapped(id: String) + case itemAppeared(itemID: TimelineItemIdentifier) + case itemDisappeared(itemID: TimelineItemIdentifier) + case itemTapped(itemID: TimelineItemIdentifier) case linkClicked(url: URL) case sendMessage - case toggleReaction(key: String, eventID: String) + case toggleReaction(key: String, itemID: TimelineItemIdentifier) case cancelReply case cancelEdit /// Mark the entire room as read - this is heavy handed as a starting point for now. case markRoomAsRead - case timelineItemMenu(itemID: String) - case timelineItemMenuAction(itemID: String, action: TimelineItemMenuAction) + case timelineItemMenu(itemID: TimelineItemIdentifier) + case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) - case displayEmojiPicker(itemID: String) + case displayEmojiPicker(itemID: TimelineItemIdentifier) case displayCameraPicker case displayMediaPicker @@ -76,10 +76,10 @@ enum RoomScreenViewAction { case handlePasteOrDrop(provider: NSItemProvider) case tappedOnUser(userID: String) - case reactionSummary(itemID: String, key: String) + case reactionSummary(itemID: TimelineItemIdentifier, key: String) - case retrySend(transactionID: String?) - case cancelSend(transactionID: String?) + case retrySend(itemID: TimelineItemIdentifier) + case cancelSend(itemID: TimelineItemIdentifier) } struct RoomScreenViewState: BindableState { @@ -97,7 +97,7 @@ struct RoomScreenViewState: BindableState { var bindings: RoomScreenViewStateBindings - var timelineItemMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)? + var timelineItemMenuActionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)? var composerMode: RoomScreenComposerMode = .default @@ -105,7 +105,7 @@ struct RoomScreenViewState: BindableState { bindings.composerText.count == 0 } - var itemIDs: [String] { + var timelineIDs: [String] { itemsDictionary.keys.elements } @@ -129,7 +129,7 @@ struct RoomScreenViewStateBindings { /// The state of wether reactions listed on the timeline are expanded/collapsed. /// Key is itemID, value is the collapsed state. - var reactionsCollapsed: [String: Bool] + var reactionsCollapsed: [TimelineItemIdentifier: Bool] /// A media item that will be previewed with QuickLook. var mediaPreviewItem: MediaPreviewItem? @@ -146,14 +146,14 @@ struct RoomScreenViewStateBindings { var reactionSummaryInfo: ReactionSummaryInfo? } -struct TimelineItemActionMenuInfo: Identifiable, Equatable { +struct TimelineItemActionMenuInfo: Equatable, Identifiable { static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool { lhs.id == rhs.id } let item: EventBasedTimelineItemProtocol - var id: String { + var id: TimelineItemIdentifier { item.id } } @@ -161,7 +161,7 @@ struct TimelineItemActionMenuInfo: Identifiable, Equatable { struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol { let title = L10n.screenRoomRetrySendMenuTitle - let transactionID: String? + let itemID: TimelineItemIdentifier } struct ReactionSummaryInfo: Identifiable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 0f1a45ac1..820eb6173 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -120,14 +120,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .tappedOnUser(userID: let userID): Task { await handleTappedUser(userID: userID) } case .displayEmojiPicker(let itemID): - guard let item = state.itemsDictionary[itemID], item.isReactable else { return } + guard let item = state.itemsDictionary[itemID.timelineID], item.isReactable else { return } callback?(.displayEmojiPicker(itemID: itemID)) - case .reactionSummary(let itemId, let key): - showReactionSummary(for: itemId, selectedKey: key) - case .retrySend(let transactionID): - Task { await handleRetrySend(transactionID: transactionID) } - case .cancelSend(let transactionID): - Task { await handleCancelSend(transactionID: transactionID) } + case .reactionSummary(let itemID, let key): + showReactionSummary(for: itemID, selectedKey: key) + case .retrySend(let itemID): + Task { await handleRetrySend(itemID: itemID) } + case .cancelSend(let itemID): + Task { await handleCancelSend(itemID: itemID) } } } @@ -221,9 +221,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol _ = await timelineController.markRoomAsRead() } - private func itemTapped(with itemId: String) async { + private func itemTapped(with itemID: TimelineItemIdentifier) async { state.showLoading = true - let action = await timelineController.processItemTap(itemId) + let action = await timelineController.processItemTap(itemID) switch action { case .displayMediaFile(let file, let title): @@ -252,19 +252,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol if itemGroup.count == 1 { if let firstItem = itemGroup.first { timelineItemsDictionary.updateValue(updateViewModel(item: firstItem, groupStyle: .single), - forKey: firstItem.id) + forKey: firstItem.id.timelineID) } } else { for (index, item) in itemGroup.enumerated() { if index == 0 { timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .first), - forKey: item.id) + forKey: item.id.timelineID) } else if index == itemGroup.count - 1 { timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .last), - forKey: item.id) + forKey: item.id.timelineID) } else { timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .middle), - forKey: item.id) + forKey: item.id.timelineID) } } } @@ -274,7 +274,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } private func updateViewModel(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewModel { - if let timelineItemViewModel = state.itemsDictionary[item.id] { + if let timelineItemViewModel = state.itemsDictionary[item.id.timelineID] { timelineItemViewModel.groupStyle = groupStyle timelineItemViewModel.type = .init(item: item) return timelineItemViewModel @@ -360,7 +360,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: TimelineItemActionMenu - private func showTimelineItemActionMenu(for itemID: String) { + private func showTimelineItemActionMenu(for itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { // Don't show a menu for non-event based items. @@ -370,8 +370,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.bindings.actionMenuInfo = .init(item: eventTimelineItem) } - private func timelineItemMenuActionsForItemId(_ itemId: String) -> TimelineItemMenuActions? { - guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }), + private func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? { + guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }), let item = timelineItem as? EventBasedTimelineItemProtocol else { // Don't show a context menu for non-event based items. return nil @@ -395,7 +395,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol ] if item.isMessage { - actions.append(.forward(itemID: itemId)) + actions.append(.forward(itemID: itemID)) } if item.isEditable { @@ -426,7 +426,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } // swiftlint:disable:next cyclomatic_complexity function_body_length - private func processTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: String) { + private func processTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { return @@ -449,7 +449,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol setComposerMode(.edit(originalItemId: messageTimelineItem.id)) case .copyPermalink: do { - let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventTimelineItem.id, roomIdentifier: timelineController.roomID, + guard let eventID = eventTimelineItem.id.eventID else { + displayError(.alert(L10n.errorFailedCreatingThePermalink)) + break + } + + let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventID, roomIdentifier: timelineController.roomID, baseURL: appSettings.permalinkBaseURL) UIPasteboard.general.url = permalink } catch { @@ -457,9 +462,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } case .redact: Task { - if eventTimelineItem.hasFailedToSend, - let transactionID = eventTimelineItem.properties.transactionID { - await timelineController.cancelSend(transactionID) + if eventTimelineItem.hasFailedToSend { + await timelineController.cancelSend(itemID) } else { await timelineController.redact(itemID) } @@ -567,16 +571,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - private func handleRetrySend(transactionID: String?) async { - guard let transactionID else { + private func handleRetrySend(itemID: TimelineItemIdentifier) async { + guard let transactionID = itemID.transactionID else { + MXLog.error("Failed Retry Send: missing transaction ID") return } await roomProxy.retrySend(transactionID: transactionID) } - private func handleCancelSend(transactionID: String?) async { - guard let transactionID else { + private func handleCancelSend(itemID: TimelineItemIdentifier) async { + guard let transactionID = itemID.transactionID else { + MXLog.error("Failed Cancel Send: missing transaction ID") return } @@ -643,7 +649,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Reaction summary - private func showReactionSummary(for itemID: String, selectedKey: String) { + private func showReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) { guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { return @@ -664,7 +670,7 @@ extension RoomScreenViewModel.Context { /// A function to make it easier to bind to reactions expand/collapsed state /// - Parameter itemID: The id of the timeline item the reacted to /// - Returns: Wether the reactions should show in the collapsed state, true by default. - func reactionsCollapsedBinding(for itemID: String) -> Binding { + func reactionsCollapsedBinding(for itemID: TimelineItemIdentifier) -> Binding { Binding(get: { self.reactionsCollapsed[itemID] ?? true }, set: { diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift index 2572bee89..6b3b8d3c1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift @@ -173,7 +173,7 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle { struct MessageComposer_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel.mock - + static var previews: some View { VStack { MessageComposer(text: .constant(""), @@ -184,7 +184,7 @@ struct MessageComposer_Previews: PreviewProvider { pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) - + MessageComposer(text: .constant("This is a short message."), focused: .constant(false), sendingDisabled: false, @@ -193,7 +193,7 @@ struct MessageComposer_Previews: PreviewProvider { pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) - + MessageComposer(text: .constant("This is a very long message that will wrap to 2 lines on an iPhone 14."), focused: .constant(false), sendingDisabled: false, @@ -202,7 +202,7 @@ struct MessageComposer_Previews: PreviewProvider { pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) - + MessageComposer(text: .constant("This is an even longer message that will wrap to 3 lines on an iPhone 14, just to see the difference it makes."), focused: .constant(false), sendingDisabled: false, @@ -211,20 +211,20 @@ struct MessageComposer_Previews: PreviewProvider { pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) - + MessageComposer(text: .constant("Some message"), focused: .constant(false), sendingDisabled: false, - mode: .edit(originalItemId: UUID().uuidString), + mode: .edit(originalItemId: .init(timelineID: UUID().uuidString)), sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) - + MessageComposer(text: .constant(""), focused: .constant(false), sendingDisabled: false, - mode: .reply(itemID: UUID().uuidString, + mode: .reply(itemID: .init(timelineID: UUID().uuidString), replyDetails: .loaded(sender: .init(id: "Kirk"), contentType: .text(.init(body: "Text: Where the wild things are")))), sendAction: { }, @@ -233,7 +233,7 @@ struct MessageComposer_Previews: PreviewProvider { editCancellationAction: { }) } .padding(.horizontal) - + ScrollView { VStack { let replyTypes: [TimelineItemReplyDetails] = [ @@ -251,12 +251,12 @@ struct MessageComposer_Previews: PreviewProvider { thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))), .loading(eventID: "") ] - + ForEach(replyTypes, id: \.self) { replyDetails in MessageComposer(text: .constant(""), focused: .constant(false), sendingDisabled: false, - mode: .reply(itemID: UUID().uuidString, + mode: .reply(itemID: .init(timelineID: UUID().uuidString), replyDetails: replyDetails), sendAction: { }, pasteAction: { _ in }, diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index b98dea8a0..2e65dbb66 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -71,12 +71,12 @@ struct RoomScreen: View { context.send(viewAction: .handlePasteOrDrop(provider: provider)) return true } - .confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { item in + .confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { info in Button(L10n.screenRoomRetrySendMenuSendAgainAction) { - context.send(viewAction: .retrySend(transactionID: item.transactionID)) + context.send(viewAction: .retrySend(itemID: info.itemID)) } Button(L10n.screenRoomRetrySendMenuRemoveAction, role: .destructive) { - context.send(viewAction: .cancelSend(transactionID: item.transactionID)) + context.send(viewAction: .cancelSend(itemID: info.itemID)) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index d3116993b..fb756d8cc 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -125,7 +125,7 @@ struct TimelineItemBubbledStylerView: View { context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id)) } .onTapGesture { - context.send(viewAction: .itemTapped(id: timelineItem.id)) + context.send(viewAction: .itemTapped(itemID: timelineItem.id)) } // We need a tap gesture before this long one so that it doesn't // steal away the gestures from the scroll view @@ -178,7 +178,7 @@ struct TimelineItemBubbledStylerView: View { if timelineItem.hasFailedToSend { backgroundedLocalizedSendInfo .onTapGesture { - context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID) + context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id) } } else { backgroundedLocalizedSendInfo @@ -335,7 +335,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { static var replies: some View { VStack { - RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "", + RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), timestamp: "10:42", isOutgoing: true, isEditable: false, @@ -344,7 +344,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), contentType: .text(.init(body: "Short")))), groupStyle: .single)) - RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "", + RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), timestamp: "10:42", isOutgoing: true, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index bb3267522..2e2922273 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -67,7 +67,7 @@ struct TimelineItemPlainStylerView: View { context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id)) } .onTapGesture { - context.send(viewAction: .itemTapped(id: timelineItem.id)) + context.send(viewAction: .itemTapped(itemID: timelineItem.id)) } // We need a tap gesture before this long one so that it doesn't // steal away the gestures from the scroll view diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift index 3563d6994..e49c5bcc1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift @@ -38,7 +38,7 @@ struct TimelineStyler: View { struct TimelineItemStyler_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel.mock - static let base = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + static let base = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) static let sentNonLast: TextRoomTimelineItem = { var result = base @@ -53,8 +53,8 @@ struct TimelineItemStyler_Previews: PreviewProvider { }() static let sendingLast: TextRoomTimelineItem = { - let id = viewModel.state.itemIDs.last ?? UUID().uuidString - var result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let id = viewModel.state.timelineIDs.last ?? UUID().uuidString + var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) result.properties.deliveryStatus = .sending return result }() @@ -66,22 +66,22 @@ struct TimelineItemStyler_Previews: PreviewProvider { }() static let sentLast: TextRoomTimelineItem = { - let id = viewModel.state.itemIDs.last ?? UUID().uuidString - let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let id = viewModel.state.timelineIDs.last ?? UUID().uuidString + let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) return result }() - static let ltrString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!")) + static let ltrString = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!")) - static let rtlString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!")) + static let rtlString = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!")) - static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!")) + static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!")) - static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!")) + static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!")) - static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!")) + static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!")) - static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!")) + static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!")) static var testView: some View { VStack { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift index 38ba4e9ff..cdfc615ed 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift @@ -23,7 +23,7 @@ struct TimelineItemStatusView: View { @EnvironmentObject private var context: RoomScreenViewModel.Context private var isLastOutgoingMessage: Bool { - context.viewState.itemIDs.last == timelineItem.id && + context.viewState.timelineIDs.last == timelineItem.id.timelineID && timelineItem.isOutgoing } @@ -53,7 +53,7 @@ struct TimelineItemStatusView: View { .foregroundColor(.compound.iconCriticalPrimary) .frame(width: 16, height: 16) .onTapGesture { - context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID) + context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift index 1f171df3e..dd73274a9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift @@ -25,7 +25,7 @@ struct TimelineReactionsView: View { @EnvironmentObject private var context: RoomScreenViewModel.Context @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection - let itemID: String + let itemID: TimelineItemIdentifier let reactions: [AggregatedReaction] @Binding var collapsed: Bool @@ -33,7 +33,7 @@ struct TimelineReactionsView: View { CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) { ForEach(reactions, id: \.self) { reaction in TimelineReactionButton(itemID: itemID, reaction: reaction) { key in - context.send(viewAction: .toggleReaction(key: key, eventID: itemID)) + context.send(viewAction: .toggleReaction(key: key, itemID: itemID)) } showReactionSummary: { key in context.send(viewAction: .reactionSummary(itemID: itemID, key: key)) } @@ -93,7 +93,7 @@ struct TimelineCollapseButtonLabel: View { } struct TimelineReactionButton: View { - let itemID: String + let itemID: TimelineItemIdentifier let reaction: AggregatedReaction let toggleReaction: (String) -> Void let showReactionSummary: (String) -> Void @@ -131,11 +131,11 @@ struct TimelineReactionViewPreviewsContainer: View { var body: some View { VStack { - TimelineReactionsView(itemID: "1", reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true)) + TimelineReactionsView(itemID: .init(timelineID: "1"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true)) Divider() - TimelineReactionsView(itemID: "2", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1) + TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1) Divider() - TimelineReactionsView(itemID: "3", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2) + TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2) .environment(\.layoutDirection, .rightToLeft) } .background(Color.red) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index f2cd3e94c..aa01c4d32 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -78,7 +78,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider { ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift index 160ad22a6..a7170a3ac 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift @@ -44,7 +44,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider { } static var body: some View { - AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: UUID().uuidString, + AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift index 3ca6f66f7..65b1be12a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift @@ -71,8 +71,8 @@ private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupS struct CollapsibleRoomTimelineView_Previews: PreviewProvider { static let item = CollapsibleTimelineItem(items: [ - SeparatorRoomTimelineItem(id: "First separator", text: "This is a separator"), - SeparatorRoomTimelineItem(id: "Second separator", text: "This is another separator") + SeparatorRoomTimelineItem(id: .init(timelineID: "First separator"), text: "This is a separator"), + SeparatorRoomTimelineItem(id: .init(timelineID: "Second separator"), text: "This is another separator") ]) static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift index 2b73a8abc..4c85cea8d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift @@ -55,7 +55,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem { - EmoteRoomTimelineItem(id: UUID().uuidString, + EmoteRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedHistoryRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedHistoryRoomTimelineView.swift index a395ce401..07e73b01f 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedHistoryRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedHistoryRoomTimelineView.swift @@ -55,7 +55,7 @@ private struct EncryptedHistoryLabelStyle: LabelStyle { struct EncryptedHistoryRoomTimelineView_Previews: PreviewProvider { static var previews: some View { - let item = EncryptedHistoryRoomTimelineItem(id: UUID().uuidString) + let item = EncryptedHistoryRoomTimelineItem(id: .init(timelineID: UUID().uuidString)) EncryptedHistoryRoomTimelineView(timelineItem: item) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift index 4b19ea8d4..053fc495d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift @@ -67,7 +67,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { - EncryptedRoomTimelineItem(id: UUID().uuidString, + EncryptedRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: text, encryptionType: .unknown, timestamp: timestamp, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift index 7be8972e3..17497b91c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift @@ -45,21 +45,21 @@ struct FileRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil))) - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil))) - FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, + FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift index 02af645f9..d8d581651 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift @@ -66,21 +66,21 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) - ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, + ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift index 580b1ca1c..29823e166 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift @@ -91,14 +91,14 @@ struct LocationRoomTimelineView_Previews: PreviewProvider { @ViewBuilder static var body: some View { - LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, + LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "Fallback geo uri description"))) - LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, + LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift index c8b798e78..cab486f64 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift @@ -66,7 +66,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { - NoticeRoomTimelineItem(id: UUID().uuidString, + NoticeRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift index d3022a4a6..532262cd4 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift @@ -41,8 +41,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider { static let item = ReadMarkerRoomTimelineItem() static var previews: some View { VStack(alignment: .leading, spacing: 0) { - RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single)) - RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "", + RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single)) + RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""), timestamp: "", isOutgoing: true, isEditable: false, @@ -51,8 +51,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider { ReadMarkerRoomTimelineView(timelineItem: item) - RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single)) - RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "", + RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single)) + RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""), timestamp: "", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift index ca961a9b2..28386997e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift @@ -42,7 +42,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { - RedactedRoomTimelineItem(id: UUID().uuidString, + RedactedRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: text, timestamp: timestamp, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift index ed70ef776..e17a81895 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift @@ -31,7 +31,7 @@ struct SeparatorRoomTimelineView: View { struct SeparatorRoomTimelineView_Previews: PreviewProvider { static var previews: some View { - let item = SeparatorRoomTimelineItem(id: "Separator", text: "This is a separator") + let item = SeparatorRoomTimelineItem(id: .init(timelineID: "Separator"), text: "This is a separator") SeparatorRoomTimelineView(timelineItem: item) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift index 552bf8d08..7ff159ded 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider { StateRoomTimelineView(timelineItem: item) } - static let item = StateRoomTimelineItem(id: UUID().uuidString, + static let item = StateRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: "Alice joined", timestamp: "Now", isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift index ad6be094a..a381696f7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift @@ -58,7 +58,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: "Some image", timestamp: "Now", isOutgoing: false, @@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { sender: .init(id: "Bob"), imageURL: URL.picturesDirectory)) - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: "Some other image", timestamp: "Now", isOutgoing: false, @@ -74,7 +74,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { sender: .init(id: "Bob"), imageURL: URL.picturesDirectory)) - StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, + StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: "Blurhashed image", timestamp: "Now", isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift index 94934a90b..6da9a1fc0 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift @@ -71,7 +71,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: isOutgoing, isEditable: isOutgoing, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift index 6bb7b1475..7fa30628d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift @@ -63,7 +63,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider { } private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem { - UnsupportedRoomTimelineItem(id: UUID().uuidString, + UnsupportedRoomTimelineItem(id: .init(timelineID: UUID().uuidString), body: text, eventType: "Some Event Type", error: "Something went wrong", diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift index 367d667cd..3e1bae03d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift @@ -77,21 +77,21 @@ struct VideoRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil))) - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, sender: .init(id: "Bob"), content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil))) - VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, + VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: false, isEditable: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 3871702ef..ed5f06c87 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -46,7 +46,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { case copyPermalink case redact case reply - case forward(itemID: String) + case forward(itemID: TimelineItemIdentifier) case viewSource case retryDecryption(sessionID: String) case report @@ -188,7 +188,7 @@ public struct TimelineItemMenu: View { private func reactionButton(for emoji: String) -> some View { Button { presentationMode.wrappedValue.dismiss() - context.send(viewAction: .toggleReaction(key: emoji, eventID: item.id)) + context.send(viewAction: .toggleReaction(key: emoji, itemID: item.id)) } label: { Text(emoji) .padding(8.0) diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift index b838cd020..9988c7206 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift @@ -72,7 +72,7 @@ class TimelineTableViewController: UIViewController { } } - var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)? + var contextMenuActionProvider: (@MainActor (_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions?)? @Binding private var scrollToBottomButtonVisible: Bool @@ -214,10 +214,10 @@ class TimelineTableViewController: UIViewController { .frame(maxWidth: .infinity, alignment: .leading) .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu .onAppear { - coordinator.send(viewAction: .itemAppeared(id: id)) + coordinator.send(viewAction: .itemAppeared(itemID: viewModel.id)) } .onDisappear { - coordinator.send(viewAction: .itemDisappeared(id: id)) + coordinator.send(viewAction: .itemDisappeared(itemID: viewModel.id)) } .environment(\.openURL, OpenURLAction { url in coordinator.send(viewAction: .linkClicked(url: url)) @@ -231,7 +231,7 @@ class TimelineTableViewController: UIViewController { return cell } - dataSource?.defaultRowAnimation = .fade +// dataSource?.defaultRowAnimation = .automatic tableView.delegate = self } @@ -248,6 +248,8 @@ class TimelineTableViewController: UIViewController { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(timelineItemsIDs) + + MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: dataSource.snapshot().itemIdentifiers))") dataSource.apply(snapshot, animatingDifferences: false) // Probably redundant now we observe content size changes… @@ -421,7 +423,7 @@ extension TimelineTableViewController { private extension UITableView { /// Returns the frame of the cell for a particular timeline item. func cellFrame(for id: String) -> CGRect? { - guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item?.id == id }) else { + guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item?.id.timelineID == id }) else { return nil } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 0a07dc4b5..c18af2e07 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -31,8 +31,8 @@ class ClientProxy: ClientProxyProtocol { private var roomListService: RoomListService? private var roomListStateUpdateTaskHandle: TaskHandle? - private var encryptionSyncService: EncryptionSync? - private var isEncryptionSyncing = false + private var appService: App? + private var appServiceUpdateTaskHandle: TaskHandle? var roomSummaryProvider: RoomSummaryProviderProtocol? var inviteSummaryProvider: RoomSummaryProviderProtocol? @@ -53,7 +53,7 @@ class ClientProxy: ClientProxyProtocol { deinit { client.setDelegate(delegate: nil) - stopSync() + pauseSync() } let callbacks = PassthroughSubject() @@ -73,7 +73,7 @@ class ClientProxy: ClientProxyProtocol { self?.callbacks.send(.updateRestorationToken) }) - await configureRoomListService() + await configureAppService() loadUserAvatarURLFromCache() } @@ -110,13 +110,7 @@ class ClientProxy: ClientProxyProtocol { } var isSyncing: Bool { - let isRoomListServiceSyncing = roomListService?.isSyncing() ?? false - - if ServiceLocator.shared.settings.isEncryptionSyncEnabled { - return isRoomListServiceSyncing && isEncryptionSyncing - } else { - return isRoomListServiceSyncing - } + roomListService?.isSyncing() ?? false } func startSync() { @@ -125,29 +119,22 @@ class ClientProxy: ClientProxyProtocol { return } - startEncryptionSyncService() - roomListService?.sync() + do { + try appService?.start() + } catch { + MXLog.error("Failed starting app service with error: \(error)") + } } - func stopSync() { + func pauseSync() { MXLog.info("Stopping sync") - stopEncryptionSyncService() do { - try roomListService?.stopSync() + try appService?.pause() } catch { - MXLog.error("Failed stopping room list service with error: \(error)") + MXLog.error("Failed pausing app service with error: \(error)") } } - - private func stopEncryptionSyncService() { - guard isEncryptionSyncing else { - return - } - isEncryptionSyncing = false - encryptionSyncService?.stop() - MXLog.info("Stopping Encryption Sync service") - } func directRoomForUserID(_ userID: String) async -> Result { await Task.dispatch(on: clientQueue) { @@ -366,7 +353,7 @@ class ClientProxy: ClientProxyProtocol { // MARK: Private private func restartSync() { - stopSync() + pauseSync() startSync() } @@ -385,89 +372,82 @@ class ClientProxy: ClientProxyProtocol { } } - private func startEncryptionSyncService() { - guard appSettings.isEncryptionSyncEnabled else { - return - } - configureEncryptionSyncService() - } - - private func configureEncryptionSyncService() { - do { - let listener = EncryptionSyncListenerProxy { [weak self] reason in - switch reason { - case .done: - MXLog.info("Encryption Sync has finished for user: \(self?.userID ?? "unknown")") - case .error(let msg): - MXLog.error("Encryption Sync has terminated for user: \(self?.userID ?? "unknown") for reason: \(msg)") - guard let self else { - return - } - Task { - self.configureEncryptionSyncService() - } - } - } - let encryptionSync = try client.mainEncryptionSync(id: "Main App", listener: listener) - isEncryptionSyncing = true - encryptionSyncService = encryptionSync - MXLog.info("Encryption sync started for user: \(userID)") - } catch { - MXLog.error("Configure encryption sync failed with error: \(error)") - } - } - - private func configureRoomListService() async { - guard roomListService == nil else { + private func configureAppService() async { + guard appService == nil else { fatalError("This shouldn't be called more than once") } do { - let roomListService = try appSettings.isEncryptionSyncEnabled ? client.roomListService() : client.roomListServiceWithEncryption() - roomListStateUpdateTaskHandle = roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in - guard let self else { return } - MXLog.info("Received room list update: \(state)") - - // Restart the room list sync on every error for now - if state == .error { - self.restartSync() - } - - // The invites are available only when entering `running` - if state == .running { - Task { - do { - // Subscribe to invites later as the underlying SlidingSync list is only added when entering AllRooms - try await self.inviteSummaryProvider?.setRoomList(roomListService.invites()) - } catch { - MXLog.error("Failed configuring invites room list with error: \(error)") - } - } - } - - // Anything that's not `running` is interpreted as "Loading data" - if state == .running { - self.callbacks.send(.receivedSyncUpdate) - } else { - self.callbacks.send(.startedUpdating) - } - }) - + let appService = try client + .app() + .withEncryptionSync(withCrossProcessLock: appSettings.isEncryptionSyncEnabled, + appIdentifier: "MainApp") + .finish() + let roomListService = appService.roomListService() + + let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, - eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)), + eventStringBuilder: eventStringBuilder, name: "AllRooms") - try await roomSummaryProvider?.setRoomList(roomListService.allRooms()) - inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService, - eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)), + eventStringBuilder: eventStringBuilder, name: "Invites") - + + self.appService = appService self.roomListService = roomListService + + appServiceUpdateTaskHandle = createAppServiceObserver(appService) + roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService) + } catch { MXLog.error("Failed building room list service with error: \(error)") } } + + private func createAppServiceObserver(_ appService: App) -> TaskHandle { + appService.state(listener: AppStateObserverProxy { [weak self] state in + guard let self else { return } + MXLog.info("Received app service update: \(state)") + switch state { + case .error: + restartSync() + case .terminated, .running: + break + } + }) + } + + private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle { + roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in + MXLog.info("Received room list update: \(state)") + guard let self, + state != .error, + state != .terminated else { + // The app service is responsible of handling error and termination + return + } + + // The invites are available only when entering `running` + if state == .running { + Task { + do { + guard let roomListService = self.roomListService else { + MXLog.error("Room list service is not configured") + return + } + // Subscribe to invites later as the underlying SlidingSync list is only added when entering AllRooms + try await self.inviteSummaryProvider?.setRoomList(roomListService.invites()) + } catch { + MXLog.error("Failed configuring invites room list with error: \(error)") + } + } + callbacks.send(.receivedSyncUpdate) + } else { + callbacks.send(.startedUpdating) + } + }) + } private func roomTupleForIdentifier(_ identifier: String) -> (RoomListItem?, Room?) { do { @@ -496,13 +476,25 @@ extension ClientProxy: MediaLoaderProtocol { } } -private class RoomListStateListenerProxy: RoomListServiceStateListener { - private let onUpdateClosure: (RoomListServiceState) -> Void - - init(_ onUpdateClosure: @escaping (RoomListServiceState) -> Void) { +private class AppStateObserverProxy: AppStateObserver { + private let onUpdateClosure: (AppState) -> Void + + init(onUpdateClosure: @escaping (AppState) -> Void) { self.onUpdateClosure = onUpdateClosure } - + + func onUpdate(state: AppState) { + onUpdateClosure(state) + } +} + +private class RoomListStateListenerProxy: RoomListServiceStateListener { + private let onUpdateClosure: (RoomListServiceState) -> Void + + init(onUpdateClosure: @escaping (RoomListServiceState) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + func onUpdate(state: RoomListServiceState) { onUpdateClosure(state) } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 9426ec697..8ccebcddd 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -84,7 +84,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func startSync() - func stopSync() + func pauseSync() func directRoomForUserID(_ userID: String) async -> Result diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 2d99c3349..5064adf4b 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -45,7 +45,7 @@ class MockClientProxy: ClientProxyProtocol { func stopSync(completionHandler: () -> Void) { } - func stopSync() { } + func pauseSync() { } func directRoomForUserID(_ userID: String) async -> Result { .failure(.failedRetrievingDirectRoom) diff --git a/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift b/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift deleted file mode 100644 index e01c6bbaf..000000000 --- a/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright 2023 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -import MatrixRustSDK - -final class EncryptionSyncListenerProxy: EncryptionSyncListener { - private let didTerminateClosure: (EncryptionSyncTerminationReason) -> Void - - init(_ didTerminateClosure: @escaping (EncryptionSyncTerminationReason) -> Void) { - self.didTerminateClosure = didTerminateClosure - } - - func didTerminate(reason: EncryptionSyncTerminationReason) { - didTerminateClosure(reason) - } -} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index ee763e499..2ca18954f 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -67,15 +67,12 @@ extension NotificationItemProxyProtocol { struct NotificationItemProxy: NotificationItemProxyProtocol { let notificationItem: NotificationItem let receiverID: String + let roomID: String var event: TimelineEventProxyProtocol { TimelineEventProxy(timelineEvent: notificationItem.event) } - var roomID: String { - notificationItem.roomInfo.id - } - var senderDisplayName: String? { notificationItem.senderInfo.displayName } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 5b2b96ae0..c36b90528 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -140,7 +140,23 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { MXLog.verbose("\(name): Finished applying \(diffs.count) diffs, new room list \(rooms.compactMap { $0.id ?? "Empty" })") } - + + private func fetchLastMessage(roomListItem: RoomListItemProtocol) -> EventTimelineItem? { + class FetchResult { + var latestRoomEvent: EventTimelineItem? + } + + let semaphore = DispatchSemaphore(value: 0) + let result = FetchResult() + + Task { + result.latestRoomEvent = await roomListItem.latestEvent() + semaphore.signal() + } + semaphore.wait() + return result.latestRoomEvent + } + private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary { guard let roomListItem = try? roomListService.room(roomId: identifier) else { MXLog.error("\(name): Failed finding room with id: \(identifier)") @@ -149,9 +165,9 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { var attributedLastMessage: AttributedString? var lastMessageFormattedTimestamp: String? - - if let latestRoomMessage = roomListItem.latestEvent() { - let lastMessage = EventTimelineItemProxy(item: latestRoomMessage) + + if let latestRoomMessage = fetchLastMessage(roomListItem: roomListItem) { + let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: 0) lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal() attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage) } diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index f9b521b04..981b23315 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -19,15 +19,15 @@ import Foundation enum RoomTimelineItemFixtures { /// The default timeline items used in Xcode previews etc. static var `default`: [RoomTimelineItemProtocol] = [ - SeparatorRoomTimelineItem(id: "Yesterday", text: "Yesterday"), - TextRoomTimelineItem(id: UUID().uuidString, + SeparatorRoomTimelineItem(id: .init(timelineID: "Yesterday"), text: "Yesterday"), + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "10:10 AM", isOutgoing: false, isEditable: false, sender: .init(id: "", displayName: "Jacob"), content: .init(body: "That looks so good!"), properties: RoomTimelineItemProperties(isEdited: true)), - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "10:11 AM", isOutgoing: false, isEditable: false, @@ -36,7 +36,7 @@ enum RoomTimelineItemFixtures { properties: RoomTimelineItemProperties(reactions: [ AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me"]) ])), - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "10:11 AM", isOutgoing: false, isEditable: false, @@ -46,21 +46,21 @@ enum RoomTimelineItemFixtures { AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]), AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob"]) ])), - SeparatorRoomTimelineItem(id: "Today", text: "Today"), - TextRoomTimelineItem(id: UUID().uuidString, + SeparatorRoomTimelineItem(id: .init(timelineID: "Today"), text: "Today"), + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "5 PM", isOutgoing: false, isEditable: false, sender: .init(id: "", displayName: "Helena"), content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"), properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])), - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "5 PM", isOutgoing: true, isEditable: true, sender: .init(id: "", displayName: "Bob"), content: .init(body: "And John's speech was amazing!")), - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "5 PM", isOutgoing: true, isEditable: true, @@ -71,7 +71,7 @@ enum RoomTimelineItemFixtures { ReadReceipt(userID: "bob", formattedTimestamp: nil), ReadReceipt(userID: "charlie", formattedTimestamp: nil), ReadReceipt(userID: "dan", formattedTimestamp: nil)])), - TextRoomTimelineItem(id: UUID().uuidString, + TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "5 PM", isOutgoing: false, isEditable: false, @@ -210,7 +210,7 @@ enum RoomTimelineItemFixtures { private extension TextRoomTimelineItem { init(text: String, senderDisplayName: String) { - self.init(id: UUID().uuidString, + self.init(id: .init(timelineID: UUID().uuidString), timestamp: "10:47 am", isOutgoing: senderDisplayName == "Alice", isEditable: false, diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index 941db7f1d..c99a517d1 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -169,37 +169,47 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { } private extension TimelineItem { - var debugIdentifier: String { + var debugIdentifier: DebugIdentifier { if let virtualTimelineItem = asVirtual() { return virtualTimelineItem.debugIdentifier } else if let eventTimelineItem = asEvent() { - return eventTimelineItem.uniqueIdentifier() + return .event(timelineID: String(uniqueId()), + eventID: eventTimelineItem.eventId(), + transactionID: eventTimelineItem.transactionId()) } - return "UnknownTimelineItem" + return .unknown } } private extension TimelineItemProxy { - var debugIdentifier: String { + var debugIdentifier: DebugIdentifier { switch self { case .event(let eventTimelineItem): - return eventTimelineItem.item.uniqueIdentifier() + return .event(timelineID: eventTimelineItem.id.timelineID, + eventID: eventTimelineItem.id.eventID, + transactionID: eventTimelineItem.id.transactionID) case .virtual(let virtualTimelineItem): return virtualTimelineItem.debugIdentifier case .unknown: - return "UnknownTimelineItem" + return .unknown } } } private extension VirtualTimelineItem { - var debugIdentifier: String { + var debugIdentifier: DebugIdentifier { switch self { case .dayDivider(let timestamp): - return "DayDiviver(\(timestamp))" + return .virtual("DayDiviver(\(timestamp))") case .readMarker: - return "ReadMarker" + return .virtual("ReadMarker") } } } + +enum DebugIdentifier { + case event(timelineID: String?, eventID: String?, transactionID: String?) + case virtual(String) + case unknown +} diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 6ecb3252a..1ed0ef6a5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -51,23 +51,23 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func markRoomAsRead() async -> Result { .success(()) } - func processItemAppearance(_ itemID: String) async { } + func processItemAppearance(_ itemID: TimelineItemIdentifier) async { } - func processItemDisappearance(_ itemID: String) async { } + func processItemDisappearance(_ itemID: TimelineItemIdentifier) async { } - func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction { .none } + func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { .none } - func sendMessage(_ message: String, inReplyTo itemID: String?) async { } + func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async { } - func toggleReaction(_ reaction: String, to itemID: String) async { } + func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { } - func editMessage(_ newMessage: String, original itemID: String) async { } + func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async { } - func redact(_ itemID: String) async { } + func redact(_ itemID: TimelineItemIdentifier) async { } - func cancelSend(_ transactionID: String) async { } + func cancelSend(_ itemID: TimelineItemIdentifier) async { } - func debugInfo(for itemID: String) -> TimelineItemDebugInfo { + func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo { .init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index c7a937c4f..98fbade41 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -84,7 +84,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { func markRoomAsRead() async -> Result { guard roomProxy.hasUnreadNotifications, - let eventID = timelineItems.last?.id + let eventID = timelineItems.last?.id.eventID else { return .success(()) } switch await roomProxy.sendReadReceipt(for: eventID) { @@ -95,7 +95,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func processItemAppearance(_ itemID: String) async { + func processItemAppearance(_ itemID: TimelineItemIdentifier) async { guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else { return } @@ -105,9 +105,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func processItemDisappearance(_ itemID: String) { } + func processItemDisappearance(_ itemID: TimelineItemIdentifier) { } - func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction { + func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else { return .none } @@ -121,14 +121,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func sendMessage(_ message: String, inReplyTo itemID: String?) async { + func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async { + var inReplyTo: String? if itemID == nil { MXLog.info("Send message in \(roomID)") - } else { + } else if let eventID = itemID?.eventID { + inReplyTo = eventID MXLog.info("Send reply in \(roomID)") + } else { + MXLog.error("Send reply in \(roomID) failed: missing event ID") + return } - - switch await roomProxy.sendMessage(message, inReplyTo: itemID) { + + switch await roomProxy.sendMessage(message, inReplyTo: inReplyTo) { case .success: MXLog.info("Finished sending message") case .failure(let error): @@ -136,9 +141,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func toggleReaction(_ reaction: String, to itemID: String) async { + func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { MXLog.info("Toggle reaction in \(roomID)") - switch await roomProxy.toggleReaction(reaction, to: itemID) { + guard let eventID = itemID.eventID else { + MXLog.error("Failed toggling reaction: missing eventID") + return + } + + switch await roomProxy.toggleReaction(reaction, to: eventID) { case .success: MXLog.info("Finished toggling reaction") case .failure(let error): @@ -146,28 +156,32 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func editMessage(_ newMessage: String, original itemID: String) async { + func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async { MXLog.info("Edit message in \(roomID)") if let timelineItem = timelineItems.first(where: { $0.id == itemID }), let item = timelineItem as? EventBasedTimelineItemProtocol, - item.hasFailedToSend, - let transactionID = item.properties.transactionID { + item.hasFailedToSend { MXLog.info("Editing a failed echo, will cancel and resend it as a new message") - await cancelSend(transactionID) + await cancelSend(itemID) await sendMessage(newMessage) - } else { - switch await roomProxy.editMessage(newMessage, original: itemID) { + } else if let eventID = itemID.eventID { + switch await roomProxy.editMessage(newMessage, original: eventID) { case .success: MXLog.info("Finished editing message") case .failure(let error): MXLog.error("Failed editing message with error: \(error)") } + } else { + MXLog.error("Editing failed: missing identifiers") } } - func redact(_ itemID: String) async { + func redact(_ itemID: TimelineItemIdentifier) async { MXLog.info("Send redaction in \(roomID)") - switch await roomProxy.redact(itemID) { + guard let eventID = itemID.eventID else { + return + } + switch await roomProxy.redact(eventID) { case .success: MXLog.info("Finished redacting message") case .failure(let error): @@ -175,14 +189,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func cancelSend(_ transactionID: String) async { + func cancelSend(_ itemID: TimelineItemIdentifier) async { + guard let transactionID = itemID.transactionID else { + MXLog.error("Failed cancelling send, missing transaction ID") + return + } MXLog.info("Cancelling send in \(roomID)") await roomProxy.cancelSend(transactionID: transactionID) } // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them - func debugInfo(for itemID: String) -> TimelineItemDebugInfo { + func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo { for timelineItemProxy in timelineProvider.itemProxies { switch timelineItemProxy { case .event(let item): @@ -339,7 +357,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { // Separators without stable identifiers cause UI glitches let identifier = "\(chunkIndex)-\(dateString)" - return SeparatorRoomTimelineItem(id: identifier, text: dateString) + return SeparatorRoomTimelineItem(id: .init(timelineID: identifier), text: dateString) case .readMarker: return ReadMarkerRoomTimelineItem() } @@ -373,12 +391,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) { + guard let eventID = timelineItem.id.eventID else { + return + } + switch timelineItem.replyDetails { case .notLoaded: - roomProxy.fetchDetails(for: timelineItem.id) + roomProxy.fetchDetails(for: eventID) case .error: if refetchOnError { - roomProxy.fetchDetails(for: timelineItem.id) + roomProxy.fetchDetails(for: eventID) } default: break diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 813471ea1..c31139110 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -41,27 +41,27 @@ protocol RoomTimelineControllerProtocol { var timelineItems: [RoomTimelineItemProtocol] { get } var callbacks: PassthroughSubject { get } - func processItemAppearance(_ itemID: String) async + func processItemAppearance(_ itemID: TimelineItemIdentifier) async - func processItemDisappearance(_ itemID: String) async + func processItemDisappearance(_ itemID: TimelineItemIdentifier) async - func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction + func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result func markRoomAsRead() async -> Result - func sendMessage(_ message: String, inReplyTo itemID: String?) async + func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async - func editMessage(_ newMessage: String, original itemID: String) async + func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async - func toggleReaction(_ reaction: String, to itemID: String) async + func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async - func redact(_ itemID: String) async + func redact(_ itemID: TimelineItemIdentifier) async - func cancelSend(_ transactionID: String) async + func cancelSend(_ itemID: TimelineItemIdentifier) async - func debugInfo(for itemID: String) -> TimelineItemDebugInfo + func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo func retryDecryption(for sessionID: String) async } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift index 63e58e4ca..ddbe6a044 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift @@ -26,6 +26,4 @@ struct RoomTimelineItemProperties: Hashable { var deliveryStatus: TimelineItemDeliveryStatus? /// The read receipts of the item, ordered from newest to oldest var orderedReadReceipts: [ReadReceipt] = [] - /// The original transaction id transmitted by the client - var transactionID: String? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 0b82acdc6..ba24f57e1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -17,6 +17,20 @@ import Foundation import MatrixRustSDK +struct TimelineItemIdentifier: Hashable { + /// Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline. + /// It's value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances. + let timelineID: String + + /// Uniquely identifies the timeline item from the server side. + /// Only available for EventTimelineItem and only when the item is returned by the server. + var eventID: String? + + /// Uniquely identfies the local echo of the timeline item. + /// Only available for sent EventTimelineItem that have not been returned by the server yet. + var transactionID: String? +} + /// A light wrapper around timeline items returned from Rust. enum TimelineItemProxy { case event(EventTimelineItemProxy) @@ -25,7 +39,7 @@ enum TimelineItemProxy { init(item: MatrixRustSDK.TimelineItem) { if let eventItem = item.asEvent() { - self = .event(EventTimelineItemProxy(item: eventItem)) + self = .event(EventTimelineItemProxy(item: eventItem, id: item.uniqueId())) } else if let virtualItem = item.asVirtual() { self = .virtual(virtualItem) } else { @@ -44,17 +58,11 @@ enum TimelineItemDeliveryStatus: Hashable { /// A light wrapper around event timeline items returned from Rust. struct EventTimelineItemProxy { let item: MatrixRustSDK.EventTimelineItem + let id: TimelineItemIdentifier - init(item: MatrixRustSDK.EventTimelineItem) { + init(item: MatrixRustSDK.EventTimelineItem, id: UInt64) { self.item = item - } - - var id: String { - item.uniqueIdentifier() - } - - var transactionID: String? { - item.transactionId() + self.id = TimelineItemIdentifier(timelineID: String(id), eventID: item.eventId(), transactionID: item.transactionId()) } var deliveryStatus: TimelineItemDeliveryStatus? { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift index 2a2cbf1f5..8eaff6d10 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift @@ -16,8 +16,8 @@ import Foundation -struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift index 5a898f3ed..2a3297c1e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { - let id: String +struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift index b79015766..1054b6208 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift @@ -17,8 +17,8 @@ import UIKit import UniformTypeIdentifiers -struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift index ba595da4f..1285c809b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift @@ -17,8 +17,8 @@ import UIKit import UniformTypeIdentifiers -struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift index f54b86f1a..a0cd6463c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift @@ -14,8 +14,8 @@ // limitations under the License. // -struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Hashable { - let id: String +struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift index d2db4f535..b050df8c2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { - let id: String +struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift index 8be020437..0ad3634cf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { - let id: String +struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index 33f3b4a4a..d232b908c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -17,8 +17,8 @@ import UIKit import UniformTypeIdentifiers -struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let timestamp: String let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CollapsibleTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CollapsibleTimelineItem.swift index 2bf3b70a2..532d92216 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CollapsibleTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/CollapsibleTimelineItem.swift @@ -16,10 +16,10 @@ import Foundation -struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let items: [RoomTimelineItemProtocol] - let itemIDs: [String] + let itemIDs: [TimelineItemIdentifier] init(items: [RoomTimelineItemProtocol]) { self.items = items @@ -38,12 +38,4 @@ struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable // Technically not a correct implementation of equality as the items themselves could be updated. lhs.id == rhs.id && lhs.itemIDs == rhs.itemIDs } - - // MARK: - Hashable - - func hash(into hasher: inout Hasher) { - // Technically not a correct implementation of hashing as the items themselves could be updated. - hasher.combine(id) - hasher.combine(itemIDs) - } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift index 388481c23..2276ee45e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift @@ -16,14 +16,14 @@ import UIKit -struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { +struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { enum EncryptionType: Hashable { case megolmV1AesSha2(sessionId: String) case olmV1Curve25519AesSha2(senderKey: String) case unknown } - let id: String + let id: TimelineItemIdentifier let body: String let encryptionType: EncryptionType let timestamp: String diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift index 299b9c30a..779dad201 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift @@ -17,8 +17,8 @@ import Foundation import UIKit -struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let body: String let timestamp: String let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift index ee58c488c..49aa4a790 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let body: String let timestamp: String let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift index e4ea33b0e..5069846d1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let body: String let timestamp: String let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift index a844d13c4..5f1636149 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift @@ -16,8 +16,8 @@ import UIKit -struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let body: String let eventType: String diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/EncryptedHistoryRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/EncryptedHistoryRoomTimelineItem.swift index 48030d57c..22da0afab 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/EncryptedHistoryRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/EncryptedHistoryRoomTimelineItem.swift @@ -16,6 +16,6 @@ import Foundation -struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift index 39e85274f..166cfb35d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/PaginationIndicatorRoomTimelineItem.swift @@ -16,6 +16,6 @@ import Foundation -struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { - let id = "paginationIndicatorTimelineItemIdentifier" +struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { + let id = TimelineItemIdentifier(timelineID: "paginationIndicatorTimelineItemIdentifier") } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/ReadMarkerRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/ReadMarkerRoomTimelineItem.swift index 497924c43..591908171 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/ReadMarkerRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/ReadMarkerRoomTimelineItem.swift @@ -16,6 +16,6 @@ import Foundation -struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { - let id = "readMarkerTimelineItemIdentifier" +struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { + let id = TimelineItemIdentifier(timelineID: "readMarkerTimelineItemIdentifier") } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/SeparatorRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/SeparatorRoomTimelineItem.swift index 81458ee29..176f2ce25 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/SeparatorRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/SeparatorRoomTimelineItem.swift @@ -16,7 +16,7 @@ import Foundation -struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { - let id: String +struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { + let id: TimelineItemIdentifier let text: String } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift index 0839a569f..05921874d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Virtual/TimelineStartRoomTimelineItem.swift @@ -16,7 +16,7 @@ import Foundation -struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { - let id: String = UUID().uuidString +struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { + let id = TimelineItemIdentifier(timelineID: UUID().uuidString) let name: String? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 7f7a3f2ad..649ef93d4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -138,8 +138,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { blurhash: imageInfo.blurhash, properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, @@ -190,8 +189,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -208,8 +206,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -226,8 +223,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -244,8 +240,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -262,8 +257,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -280,8 +274,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -298,8 +291,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -316,13 +308,12 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), deliveryStatus: eventItemProxy.deliveryStatus, - orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), - transactionID: eventItemProxy.transactionID)) + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { reactions.map { reaction in - AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: reaction.senders) + AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: reaction.senders.map(\.senderId)) } .sorted { a, b in // Sort by count and then by key for a consistence experience. diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift index bfc3c42e6..c52fbac0b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift @@ -18,5 +18,5 @@ import Foundation import UIKit protocol RoomTimelineItemProtocol { - var id: String { get } + var id: TimelineItemIdentifier { get } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewModel.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewModel.swift index 7c65bf0fe..1ed1ba1e8 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewModel.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewModel.swift @@ -24,7 +24,7 @@ final class RoomTimelineItemViewModel: Identifiable, Equatable, ObservableObject @Published var type: RoomTimelineItemType @Published var groupStyle: TimelineGroupStyle - var id: String { + var id: TimelineItemIdentifier { type.id } @@ -109,7 +109,7 @@ enum RoomTimelineItemType: Equatable { } } - var id: String { + var id: TimelineItemIdentifier { switch self { case .text(let item as RoomTimelineItemProtocol), .separator(let item as RoomTimelineItemProtocol), diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 753b5dfb6..b69fde2c7 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -474,7 +474,7 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .reportContent: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = ReportContentScreenCoordinator(parameters: .init(itemID: "test", + let coordinator = ReportContentScreenCoordinator(parameters: .init(eventID: "test", senderID: RoomMemberProxyMock.mockAlice.userID, roomProxy: RoomProxyMock(with: .init(displayName: "test")))) navigationStackCoordinator.setRootCoordinator(coordinator) diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index f1b62f4c5..0f9dc2709 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -71,26 +71,9 @@ class NotificationServiceExtension: UNNotificationServiceExtension { MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)") do { - let userSession = try NSEUserSession(credentials: credentials) + let userSession = try NSEUserSession(credentials: credentials, isEncryptionSyncEnabled: settings.isEncryptionSyncEnabled) self.userSession = userSession - var itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) - if settings.isEncryptionSyncEnabled, - itemProxy?.isEncrypted == true, - let _ = try? userSession.startEncryptionSync() { - // TODO: The following wait with a timeout should be handled by the SDK - // We try to decrypt the notification for 10 seconds at most - let date = Date() - repeat { - // if the sync terminated we try one last time then we break from the loop - guard userSession.isSyncing else { - itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) - break - } - itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) - } while itemProxy?.isEncrypted == true && date.timeIntervalSinceNow > -10 - } - - guard let itemProxy else { + guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else { MXLog.info("\(tag) no notification for the event, discard") return discard() } @@ -146,7 +129,6 @@ class NotificationServiceExtension: UNNotificationServiceExtension { private func cleanUp() { handler = nil modifiedContent = nil - userSession?.stopEncryptionSync() } deinit { diff --git a/NSE/Sources/Other/NSEUserSession.swift b/NSE/Sources/Other/NSEUserSession.swift index 97b6ac7f2..4ee5eef0e 100644 --- a/NSE/Sources/Other/NSEUserSession.swift +++ b/NSE/Sources/Other/NSEUserSession.swift @@ -18,56 +18,39 @@ import Foundation import MatrixRustSDK final class NSEUserSession { - private let client: ClientProtocol + private let baseClient: Client + private let notificationClient: NotificationClient private let userID: String - private var encryptionSyncService: EncryptionSync? - private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: client), + private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient), imageCache: .onlyOnDisk, backgroundTaskService: nil) - var isSyncing: Bool { - encryptionSyncService != nil - } - - init(credentials: KeychainCredentials) throws { + init(credentials: KeychainCredentials, isEncryptionSyncEnabled: Bool) throws { userID = credentials.userID - let builder = ClientBuilder() + baseClient = try ClientBuilder() .basePath(path: URL.sessionsBaseDirectory.path) .username(username: credentials.userID) + .build() - client = try builder.build() - try client.restoreSession(session: credentials.restorationToken.session) - } + try baseClient.restoreSession(session: credentials.restorationToken.session) - func startEncryptionSync() throws { - let listener = EncryptionSyncListenerProxy { [weak self] reason in - MXLog.info("NSE: Encryption sync terminated for user: \(self?.userID ?? "unknown") with reason: \(reason)") - self?.encryptionSyncService = nil - } - encryptionSyncService = try client.notificationEncryptionSync(id: "NSE", listener: listener, numIters: 2) - MXLog.info("NSE: Encryption sync started for user: \(userID)") + notificationClient = baseClient + .notificationClient() + .retryDecryption(withCrossProcessLock: isEncryptionSyncEnabled) + .finish() } func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { await Task.dispatch(on: .global()) { do { - guard let notification = try self.client.getNotificationItem(roomId: roomID, eventId: eventID, filterByPushRules: false) else { + guard let notification = try self.notificationClient.getNotification(roomId: roomID, eventId: eventID) else { return nil } - return NotificationItemProxy(notificationItem: notification, receiverID: self.userID) + return NotificationItemProxy(notificationItem: notification, receiverID: self.userID, roomID: roomID) } catch { MXLog.error("NSE: Could not get notification's content creating an empty notification instead, error: \(error)") return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: self.userID) } } } - - func stopEncryptionSync() { - encryptionSyncService?.stop() - } - - deinit { - MXLog.info("NSE: NSEUserSession deinit called for user: \(userID)") - stopEncryptionSync() - } } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index e37dfd325..5c01cfba7 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -252,39 +252,39 @@ class LoggingTests: XCTestCase { func testTimelineContentIsRedacted() throws { // Given timeline items that contain text let textAttributedString = "TextAttributed" - let textMessage = TextRoomTimelineItem(id: "mytextmessage", + let textMessage = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"), content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString))) let noticeAttributedString = "NoticeAttributed" - let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", + let noticeMessage = NoticeRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"), content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString))) let emoteAttributedString = "EmoteAttributed" - let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", + let emoteMessage = EmoteRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"), content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString))) - let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", + let imageMessage = ImageRoomTimelineItem(id: .init(timelineID: "myimagemessage"), timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"), content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil)) - let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", + let videoMessage = VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"), content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil)) - let fileMessage = FileRoomTimelineItem(id: "myfilemessage", + let fileMessage = FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "", isOutgoing: false, isEditable: false, @@ -310,25 +310,25 @@ class LoggingTests: XCTestCase { } let content = try String(contentsOf: logFile) - XCTAssertTrue(content.contains(textMessage.id)) + XCTAssertTrue(content.contains(textMessage.id.timelineID)) XCTAssertFalse(content.contains(textMessage.body)) XCTAssertFalse(content.contains(textAttributedString)) - XCTAssertTrue(content.contains(noticeMessage.id)) + XCTAssertTrue(content.contains(noticeMessage.id.timelineID)) XCTAssertFalse(content.contains(noticeMessage.body)) XCTAssertFalse(content.contains(noticeAttributedString)) - XCTAssertTrue(content.contains(emoteMessage.id)) + XCTAssertTrue(content.contains(emoteMessage.id.timelineID)) XCTAssertFalse(content.contains(emoteMessage.body)) XCTAssertFalse(content.contains(emoteAttributedString)) - XCTAssertTrue(content.contains(imageMessage.id)) + XCTAssertTrue(content.contains(imageMessage.id.timelineID)) XCTAssertFalse(content.contains(imageMessage.body)) - XCTAssertTrue(content.contains(videoMessage.id)) + XCTAssertTrue(content.contains(videoMessage.id.timelineID)) XCTAssertFalse(content.contains(videoMessage.body)) - XCTAssertTrue(content.contains(fileMessage.id)) + XCTAssertTrue(content.contains(fileMessage.id.timelineID)) XCTAssertFalse(content.contains(fileMessage.body)) } diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift index 8f47c960d..560235b6b 100644 --- a/UnitTests/Sources/ReportContentViewModelTests.swift +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -19,7 +19,7 @@ import XCTest @MainActor class ReportContentScreenViewModelTests: XCTestCase { - let itemID = "test-id" + let eventID = "test-id" let senderID = "@meany:server.com" let reportReason = "I don't like it." @@ -27,7 +27,7 @@ class ReportContentScreenViewModelTests: XCTestCase { // Given the report content view for some content. let roomProxy = RoomProxyMock(with: .init(displayName: "test")) roomProxy.reportContentReasonReturnValue = .success(()) - let viewModel = ReportContentScreenViewModel(itemID: itemID, + let viewModel = ReportContentScreenViewModel(eventID: eventID, senderID: senderID, roomProxy: roomProxy) @@ -43,7 +43,7 @@ class ReportContentScreenViewModelTests: XCTestCase { // Then the content should be reported, but the user should not be included. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") - XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, itemID, "The event ID should match the content being reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.") XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.") XCTAssertEqual(roomProxy.ignoreUserCallsCount, 0, "A call to ignore a user should not have been made.") XCTAssertNil(roomProxy.ignoreUserReceivedUserID, "The sender shouldn't have been ignored.") @@ -54,7 +54,7 @@ class ReportContentScreenViewModelTests: XCTestCase { let roomProxy = RoomProxyMock(with: .init(displayName: "test")) roomProxy.reportContentReasonReturnValue = .success(()) roomProxy.ignoreUserReturnValue = .success(()) - let viewModel = ReportContentScreenViewModel(itemID: itemID, + let viewModel = ReportContentScreenViewModel(eventID: eventID, senderID: senderID, roomProxy: roomProxy) @@ -69,7 +69,7 @@ class ReportContentScreenViewModelTests: XCTestCase { // Then the content should be reported, and the user should be ignored. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") - XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, itemID, "The event ID should match the content being reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.") XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.") XCTAssertEqual(roomProxy.ignoreUserCallsCount, 1, "A call should have been made to ignore the sender.") XCTAssertEqual(roomProxy.ignoreUserReceivedUserID, senderID, "The ignored user ID should match the sender.") diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 5bda66676..978d52b4b 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -289,7 +289,8 @@ class RoomScreenViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock) // Test - viewModel.context.send(viewAction: .retrySend(transactionID: "test retry send id")) + viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test retry send id"))) + await Task.yield() try? await Task.sleep(for: .microseconds(500)) XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.retrySendTransactionIDReceivedInvocations == ["test retry send id"]) @@ -308,7 +309,7 @@ class RoomScreenViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock) // Test - viewModel.context.send(viewAction: .retrySend(transactionID: nil)) + viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString))) await Task.yield() XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 0) } @@ -326,7 +327,7 @@ class RoomScreenViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock) // Test - viewModel.context.send(viewAction: .cancelSend(transactionID: "test cancel send id")) + viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test cancel send id"))) try? await Task.sleep(for: .microseconds(500)) XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"]) @@ -345,12 +346,12 @@ class RoomScreenViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock) // Test - viewModel.context.send(viewAction: .cancelSend(transactionID: nil)) + viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString))) await Task.yield() XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0) } - func testMarkAsRead() async { + func testMarkAsRead() async throws { // Setup let notificationCenterMock = NotificationCenterMock() let timelineController = MockRoomTimelineController() @@ -365,7 +366,7 @@ class RoomScreenViewModelTests: XCTestCase { notificationCenterProtocol: notificationCenterMock) viewModel.context.send(viewAction: .markRoomAsRead) - await Task.yield() + try await Task.sleep(for: .microseconds(100)) XCTAssertEqual(notificationCenterMock.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead) let roomID = notificationCenterMock.postNameObjectReceivedArguments?.anObject as? String XCTAssertEqual(roomID, roomProxyMock.id) @@ -375,7 +376,7 @@ class RoomScreenViewModelTests: XCTestCase { private extension TextRoomTimelineItem { init(text: String, sender: String, addReactions: Bool = false) { let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [sender])] : [] - self.init(id: UUID().uuidString, + self.init(id: .init(timelineID: UUID().uuidString), timestamp: "10:47 am", isOutgoing: sender == "bob", isEditable: sender == "bob", diff --git a/UnitTests/Sources/TextBasedRoomTimelineTests.swift b/UnitTests/Sources/TextBasedRoomTimelineTests.swift index 321d4c4f5..1e9c81873 100644 --- a/UnitTests/Sources/TextBasedRoomTimelineTests.swift +++ b/UnitTests/Sources/TextBasedRoomTimelineTests.swift @@ -20,7 +20,7 @@ import XCTest final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEnd() { let timestamp = "Now" - let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let timelineItem = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) let view = TextBasedRoomTimelineViewMock() view.underlyingTimelineItem = timelineItem view.timelineStyle = .bubbles @@ -30,7 +30,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndLonger() { let timestamp = "10:00 AM" - let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let timelineItem = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) let view = TextBasedRoomTimelineViewMock() view.underlyingTimelineItem = timelineItem view.timelineStyle = .bubbles @@ -39,7 +39,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { } func testTextRoomTimelineItemWhitespaceEndPlain() { - let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + let timelineItem = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) let view = TextBasedRoomTimelineViewMock() view.underlyingTimelineItem = timelineItem view.timelineStyle = .plain @@ -49,7 +49,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndWithEdit() { let timestamp = "Now" - var timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + var timelineItem = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) timelineItem.properties.isEdited = true let editedCount = L10n.commonEditedSuffix.count let view = TextBasedRoomTimelineViewMock() @@ -61,7 +61,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() { let timestamp = "Now" - var timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) + var timelineItem = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) timelineItem.properties.isEdited = true timelineItem.properties.deliveryStatus = .sendingFailed let editedCount = L10n.commonEditedSuffix.count diff --git a/project.yml b/project.yml index a510a6c20..c6feb599b 100644 --- a/project.yml +++ b/project.yml @@ -44,7 +44,7 @@ include: packages: MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.0.96-alpha + exactVersion: 1.0.98-alpha # path: ../matrix-rust-sdk DesignKit: path: DesignKit