SDK Changes Refactor (#1312)

* internal id

* notification refactor

* client proxy refactor

* required self

* better identifier system

* using the event id when required

* tests fixed

* tested some stuff

* fixed merge conflict

* improved the test wait

* animation disabled

* Bump Rust SDK version to 1.0.98-alpha (#1310)

* code improvement

* pause sync

* pr suggestions

* result

* Apply suggestions from code review

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* enum based debug identifier

---------

Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro 2023-07-12 12:28:41 +02:00 committed by GitHub
parent adb253ff66
commit 76506752b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 551 additions and 661 deletions

View File

@ -278,7 +278,6 @@
6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; };
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; };
695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.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 */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; };
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; };
6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.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 */; }; F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; };
F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; };
F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.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 */; }; F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; };
F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; };
F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; 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 = "<group>"; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = "<group>"; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = "<group>"; };
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = "<group>"; };
68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSyncListenerProxy.swift; sourceTree = "<group>"; };
6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; };
686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = "<group>"; }; 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = "<group>"; };
69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = "<group>"; }; 69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = "<group>"; };
@ -2649,7 +2646,6 @@
832FC81F760220239E285294 /* Proxy */ = { 832FC81F760220239E285294 /* Proxy */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */,
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */, 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */,
); );
path = Proxy; path = Proxy;
@ -4004,7 +4000,6 @@
9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */,
DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */, DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */,
24A75F72EEB7561B82D726FD /* Date.swift in Sources */, 24A75F72EEB7561B82D726FD /* Date.swift in Sources */,
F91B4629E4AF51A4FE8E7608 /* EncryptionSyncListenerProxy.swift in Sources */,
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */,
59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */,
A3E390675E9730C176B59E1B /* ImageProviderProtocol.swift in Sources */, A3E390675E9730C176B59E1B /* ImageProviderProtocol.swift in Sources */,
@ -4262,7 +4257,6 @@
9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */, 9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */,
4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */, 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */,
B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */,
69ABFBAF05D7EF11E7C88CEA /* EncryptionSyncListenerProxy.swift in Sources */,
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */,
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */,
63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */,
@ -5289,7 +5283,7 @@
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = "1.0.96-alpha"; version = "1.0.98-alpha";
}; };
}; };
96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {

View File

@ -111,8 +111,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift", "location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "8bc8015237083035cb5f4a00d1eedb9ebbbb83c6", "revision" : "985708733af7d2db1684f90f0a954854ca3a83ad",
"version" : "1.0.96-alpha" "version" : "1.0.98-alpha"
} }
}, },
{ {

View File

@ -4,7 +4,8 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@ -29,6 +30,12 @@
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES" codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"> onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@ -38,6 +45,10 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets> <CodeCoverageTargets>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@ -47,12 +58,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -74,6 +79,8 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable
key = "RUST_BACKTRACE" key = "RUST_BACKTRACE"
@ -113,6 +120,8 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

View File

@ -542,7 +542,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
// MARK: - Application State // MARK: - Application State
private func stopSync() { private func stopSync() {
userSession?.clientProxy.stopSync() userSession?.clientProxy.pauseSync()
backgroundAppRefreshTask?.setTaskCompleted(success: true) backgroundAppRefreshTask?.setTaskCompleted(success: true)
backgroundAppRefreshTask = nil backgroundAppRefreshTask = nil
@ -602,7 +602,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
@objc @objc
private func applicationWillTerminate() { private func applicationWillTerminate() {
userSession?.clientProxy.stopSync() userSession?.clientProxy.pauseSync()
} }
@objc @objc
@ -616,7 +616,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in
guard let self else { return } guard let self else { return }
userSession?.clientProxy.stopSync() userSession?.clientProxy.pauseSync()
backgroundTask?.stop() backgroundTask?.stop()
backgroundTask = nil backgroundTask = nil

View File

@ -207,8 +207,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomMemberDetails, .dismissRoomMemberDetails, .room): case (.roomMemberDetails, .dismissRoomMemberDetails, .room):
break break
case (.room, .presentMessageForwarding(let eventID), .messageForwarding): case (.room, .presentMessageForwarding(let itemID), .messageForwarding):
presentMessageForwarding(for: eventID) presentMessageForwarding(for: itemID)
case (.messageForwarding, .dismissMessageForwarding, .room): case (.messageForwarding, .dismissMessageForwarding, .room):
break break
case (.room, .presentMapNavigator(let mode), .mapNavigator): case (.room, .presentMapNavigator(let mode), .mapNavigator):
@ -401,14 +401,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentReportContent(for itemID: String, from senderID: String) { private func presentReportContent(for itemID: TimelineItemIdentifier, from senderID: String) {
guard let roomProxy else { guard let roomProxy, let eventID = itemID.eventID else {
fatalError() fatalError()
} }
let navigationCoordinator = NavigationStackCoordinator() let navigationCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator) let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator)
let parameters = ReportContentScreenCoordinatorParameters(itemID: itemID, let parameters = ReportContentScreenCoordinatorParameters(eventID: eventID,
senderID: senderID, senderID: senderID,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: userIndicatorController) 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, let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
itemId: itemId) itemID: itemID)
let coordinator = EmojiPickerScreenCoordinator(parameters: params) let coordinator = EmojiPickerScreenCoordinator(parameters: params)
coordinator.callback = { [weak self] action in coordinator.callback = { [weak self] action in
switch action { switch action {
case let .emojiSelected(emoji: emoji, itemId: itemId): case let .emojiSelected(emoji: emoji, itemID: itemID):
MXLog.debug("Selected \(emoji) for \(itemId)") MXLog.debug("Selected \(emoji) for \(itemID)")
self?.navigationStackCoordinator.setSheetCoordinator(nil) self?.navigationStackCoordinator.setSheetCoordinator(nil)
Task { Task {
await self?.timelineController?.toggleReaction(emoji, to: itemId) await self?.timelineController?.toggleReaction(emoji, to: itemID)
} }
case .dismiss: case .dismiss:
self?.navigationStackCoordinator.setSheetCoordinator(nil) self?.navigationStackCoordinator.setSheetCoordinator(nil)
@ -547,8 +547,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentMessageForwarding(for eventID: String) { private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider else { guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider, let eventID = itemID.eventID else {
fatalError() fatalError()
} }
@ -624,14 +624,14 @@ private extension RoomFlowCoordinator {
enum State: StateType { enum State: StateType {
case initial case initial
case room(roomID: String) 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 roomDetails(roomID: String, isRoot: Bool)
case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource) case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource)
case mediaUploadPreview(roomID: String, fileURL: URL) case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: String) case emojiPicker(roomID: String, itemID: TimelineItemIdentifier)
case mapNavigator(roomID: String) case mapNavigator(roomID: String)
case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper)
case messageForwarding(roomID: String, itemID: String) case messageForwarding(roomID: String, itemID: TimelineItemIdentifier)
} }
struct EventUserInfo { struct EventUserInfo {
@ -643,7 +643,7 @@ private extension RoomFlowCoordinator {
case presentRoom(roomID: String) case presentRoom(roomID: String)
case dismissRoom case dismissRoom
case presentReportContent(itemID: String, senderID: String) case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case dismissReportContent case dismissReportContent
case presentRoomDetails(roomID: String) case presentRoomDetails(roomID: String)
@ -655,7 +655,7 @@ private extension RoomFlowCoordinator {
case presentMediaUploadPreview(fileURL: URL) case presentMediaUploadPreview(fileURL: URL)
case dismissMediaUploadPreview case dismissMediaUploadPreview
case presentEmojiPicker(itemID: String) case presentEmojiPicker(itemID: TimelineItemIdentifier)
case dismissEmojiPicker case dismissEmojiPicker
case presentMapNavigator(interactionMode: StaticLocationInteractionMode) case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
@ -664,7 +664,7 @@ private extension RoomFlowCoordinator {
case presentRoomMemberDetails(member: HashableRoomMemberWrapper) case presentRoomMemberDetails(member: HashableRoomMemberWrapper)
case dismissRoomMemberDetails case dismissRoomMemberDetails
case presentMessageForwarding(itemID: String) case presentMessageForwarding(itemID: TimelineItemIdentifier)
case dismissMessageForwarding case dismissMessageForwarding
} }
} }

View File

@ -31,6 +31,23 @@ class SDKClientMock: SDKClientProtocol {
return accountDataEventTypeReturnValue 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` //MARK: - `avatarUrl`
public var avatarUrlThrowableError: Error? public var avatarUrlThrowableError: Error?
@ -240,29 +257,21 @@ class SDKClientMock: SDKClientProtocol {
return getMediaThumbnailMediaSourceWidthHeightReturnValue return getMediaThumbnailMediaSourceWidthHeightReturnValue
} }
} }
//MARK: - `getNotificationItem` //MARK: - `getNotificationSettings`
public var getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError: Error? public var getNotificationSettingsCallsCount = 0
public var getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount = 0 public var getNotificationSettingsCalled: Bool {
public var getNotificationItemRoomIdEventIdFilterByPushRulesCalled: Bool { return getNotificationSettingsCallsCount > 0
return getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount > 0
} }
public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedArguments: (`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)? public var getNotificationSettingsReturnValue: NotificationSettings!
public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedInvocations: [(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)] = [] public var getNotificationSettingsClosure: (() -> NotificationSettings)?
public var getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue: NotificationItem?
public var getNotificationItemRoomIdEventIdFilterByPushRulesClosure: ((String, String, Bool) throws -> NotificationItem?)?
public func `getNotificationItem`(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool) throws -> NotificationItem? { public func `getNotificationSettings`() -> NotificationSettings {
if let error = getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError { getNotificationSettingsCallsCount += 1
throw error if let getNotificationSettingsClosure = getNotificationSettingsClosure {
} return getNotificationSettingsClosure()
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`)
} else { } else {
return getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue return getNotificationSettingsReturnValue
} }
} }
//MARK: - `getProfile` //MARK: - `getProfile`
@ -384,54 +393,21 @@ class SDKClientMock: SDKClientProtocol {
logoutCallsCount += 1 logoutCallsCount += 1
try logoutClosure?() try logoutClosure?()
} }
//MARK: - `mainEncryptionSync` //MARK: - `notificationClient`
public var mainEncryptionSyncIdListenerThrowableError: Error? public var notificationClientCallsCount = 0
public var mainEncryptionSyncIdListenerCallsCount = 0 public var notificationClientCalled: Bool {
public var mainEncryptionSyncIdListenerCalled: Bool { return notificationClientCallsCount > 0
return mainEncryptionSyncIdListenerCallsCount > 0
} }
public var mainEncryptionSyncIdListenerReceivedArguments: (`id`: String, `listener`: EncryptionSyncListener)? public var notificationClientReturnValue: NotificationClientBuilder!
public var mainEncryptionSyncIdListenerReceivedInvocations: [(`id`: String, `listener`: EncryptionSyncListener)] = [] public var notificationClientClosure: (() -> NotificationClientBuilder)?
public var mainEncryptionSyncIdListenerReturnValue: EncryptionSync!
public var mainEncryptionSyncIdListenerClosure: ((String, EncryptionSyncListener) throws -> EncryptionSync)?
public func `mainEncryptionSync`(`id`: String, `listener`: EncryptionSyncListener) throws -> EncryptionSync { public func `notificationClient`() -> NotificationClientBuilder {
if let error = mainEncryptionSyncIdListenerThrowableError { notificationClientCallsCount += 1
throw error if let notificationClientClosure = notificationClientClosure {
} return notificationClientClosure()
mainEncryptionSyncIdListenerCallsCount += 1
mainEncryptionSyncIdListenerReceivedArguments = (id: id, listener: listener)
mainEncryptionSyncIdListenerReceivedInvocations.append((id: id, listener: listener))
if let mainEncryptionSyncIdListenerClosure = mainEncryptionSyncIdListenerClosure {
return try mainEncryptionSyncIdListenerClosure(`id`, `listener`)
} else { } else {
return mainEncryptionSyncIdListenerReturnValue return notificationClientReturnValue
}
}
//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
} }
} }
//MARK: - `restoreSession` //MARK: - `restoreSession`
@ -454,48 +430,6 @@ class SDKClientMock: SDKClientProtocol {
restoreSessionSessionReceivedInvocations.append(`session`) restoreSessionSessionReceivedInvocations.append(`session`)
try restoreSessionSessionClosure?(`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` //MARK: - `rooms`
public var roomsCallsCount = 0 public var roomsCallsCount = 0
@ -615,22 +549,6 @@ class SDKClientMock: SDKClientProtocol {
setDisplayNameNameReceivedInvocations.append(`name`) setDisplayNameNameReceivedInvocations.append(`name`)
try setDisplayNameNameClosure?(`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` //MARK: - `setPusher`
public var setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangThrowableError: Error? public var setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangThrowableError: Error?

View File

@ -18,11 +18,11 @@ import SwiftUI
struct EmojiPickerScreenCoordinatorParameters { struct EmojiPickerScreenCoordinatorParameters {
let emojiProvider: EmojiProviderProtocol let emojiProvider: EmojiProviderProtocol
let itemId: String let itemID: TimelineItemIdentifier
} }
enum EmojiPickerScreenCoordinatorAction { enum EmojiPickerScreenCoordinatorAction {
case emojiSelected(emoji: String, itemId: String) case emojiSelected(emoji: String, itemID: TimelineItemIdentifier)
case dismiss case dismiss
} }
@ -44,7 +44,7 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
switch action { switch action {
case let .emojiSelected(emoji: emoji): 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: case .dismiss:
self.callback?(.dismiss) self.callback?(.dismiss)
} }

View File

@ -18,7 +18,7 @@ import Combine
import SwiftUI import SwiftUI
struct ReportContentScreenCoordinatorParameters { struct ReportContentScreenCoordinatorParameters {
let itemID: String let eventID: String
let senderID: String let senderID: String
let roomProxy: RoomProxyProtocol let roomProxy: RoomProxyProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol? weak var userIndicatorController: UserIndicatorControllerProtocol?
@ -39,7 +39,7 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol {
init(parameters: ReportContentScreenCoordinatorParameters) { init(parameters: ReportContentScreenCoordinatorParameters) {
self.parameters = parameters 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 // MARK: - Public

View File

@ -20,7 +20,7 @@ import SwiftUI
typealias ReportContentScreenViewModelType = StateStoreViewModel<ReportContentScreenViewState, ReportContentScreenViewAction> typealias ReportContentScreenViewModelType = StateStoreViewModel<ReportContentScreenViewState, ReportContentScreenViewAction>
class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportContentScreenViewModelProtocol { class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportContentScreenViewModelProtocol {
private let itemID: String private let eventID: String
private let senderID: String private let senderID: String
private let roomProxy: RoomProxyProtocol private let roomProxy: RoomProxyProtocol
private let actionsSubject: PassthroughSubject<ReportContentScreenViewModelAction, Never> = .init() private let actionsSubject: PassthroughSubject<ReportContentScreenViewModelAction, Never> = .init()
@ -29,8 +29,8 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) { init(eventID: String, senderID: String, roomProxy: RoomProxyProtocol) {
self.itemID = itemID self.eventID = eventID
self.senderID = senderID self.senderID = senderID
self.roomProxy = roomProxy self.roomProxy = roomProxy
@ -53,7 +53,7 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont
private func submitReport() async { private func submitReport() async {
actionsSubject.send(.submitStarted) 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)") MXLog.error("Submit Report Content failed: \(error)")
actionsSubject.send(.submitFailed(error: error)) actionsSubject.send(.submitFailed(error: error))
return return

View File

@ -84,7 +84,7 @@ struct ReportContentScreen: View {
// MARK: - Previews // MARK: - Previews
struct ReportContentScreen_Previews: PreviewProvider { struct ReportContentScreen_Previews: PreviewProvider {
static let viewModel = ReportContentScreenViewModel(itemID: "", static let viewModel = ReportContentScreenViewModel(eventID: "",
senderID: "", senderID: "",
roomProxy: RoomProxyMock(with: .init(displayName: nil))) roomProxy: RoomProxyMock(with: .init(displayName: nil)))

View File

@ -25,15 +25,15 @@ struct RoomScreenCoordinatorParameters {
} }
enum RoomScreenCoordinatorAction { enum RoomScreenCoordinatorAction {
case presentReportContent(itemID: String, senderID: String) case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case presentMediaUploadPicker(MediaPickerScreenSource) case presentMediaUploadPicker(MediaPickerScreenSource)
case presentMediaUploadPreviewScreen(URL) case presentMediaUploadPreviewScreen(URL)
case presentRoomDetails case presentRoomDetails
case presentLocationPicker case presentLocationPicker
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?) case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentEmojiPicker(itemID: String) case presentEmojiPicker(itemID: TimelineItemIdentifier)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol) case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
case presentMessageForwarding(itemID: String) case presentMessageForwarding(itemID: TimelineItemIdentifier)
} }
final class RoomScreenCoordinator: CoordinatorProtocol { final class RoomScreenCoordinator: CoordinatorProtocol {

View File

@ -22,22 +22,22 @@ import OrderedCollections
enum RoomScreenViewModelAction { enum RoomScreenViewModelAction {
case displayRoomDetails case displayRoomDetails
case displayEmojiPicker(itemID: String) case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayReportContent(itemID: String, senderID: String) case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
case displayDocumentPicker case displayDocumentPicker
case displayLocationPicker case displayLocationPicker
case displayMediaUploadPreviewScreen(url: URL) case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case displayMessageForwarding(itemID: String) case displayMessageForwarding(itemID: TimelineItemIdentifier)
case displayLocation(body: String, geoURI: GeoURI, description: String?) case displayLocation(body: String, geoURI: GeoURI, description: String?)
} }
enum RoomScreenComposerMode: Equatable { enum RoomScreenComposerMode: Equatable {
case `default` case `default`
case reply(itemID: String, replyDetails: TimelineItemReplyDetails) case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails)
case edit(originalItemId: String) case edit(originalItemId: TimelineItemIdentifier)
var isEdit: Bool { var isEdit: Bool {
switch self { switch self {
@ -52,21 +52,21 @@ enum RoomScreenComposerMode: Equatable {
enum RoomScreenViewAction { enum RoomScreenViewAction {
case displayRoomDetails case displayRoomDetails
case paginateBackwards case paginateBackwards
case itemAppeared(id: String) case itemAppeared(itemID: TimelineItemIdentifier)
case itemDisappeared(id: String) case itemDisappeared(itemID: TimelineItemIdentifier)
case itemTapped(id: String) case itemTapped(itemID: TimelineItemIdentifier)
case linkClicked(url: URL) case linkClicked(url: URL)
case sendMessage case sendMessage
case toggleReaction(key: String, eventID: String) case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case cancelReply case cancelReply
case cancelEdit case cancelEdit
/// Mark the entire room as read - this is heavy handed as a starting point for now. /// Mark the entire room as read - this is heavy handed as a starting point for now.
case markRoomAsRead case markRoomAsRead
case timelineItemMenu(itemID: String) case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: String, action: TimelineItemMenuAction) case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
case displayEmojiPicker(itemID: String) case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayCameraPicker case displayCameraPicker
case displayMediaPicker case displayMediaPicker
@ -76,10 +76,10 @@ enum RoomScreenViewAction {
case handlePasteOrDrop(provider: NSItemProvider) case handlePasteOrDrop(provider: NSItemProvider)
case tappedOnUser(userID: String) case tappedOnUser(userID: String)
case reactionSummary(itemID: String, key: String) case reactionSummary(itemID: TimelineItemIdentifier, key: String)
case retrySend(transactionID: String?) case retrySend(itemID: TimelineItemIdentifier)
case cancelSend(transactionID: String?) case cancelSend(itemID: TimelineItemIdentifier)
} }
struct RoomScreenViewState: BindableState { struct RoomScreenViewState: BindableState {
@ -97,7 +97,7 @@ struct RoomScreenViewState: BindableState {
var bindings: RoomScreenViewStateBindings var bindings: RoomScreenViewStateBindings
var timelineItemMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)? var timelineItemMenuActionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
var composerMode: RoomScreenComposerMode = .default var composerMode: RoomScreenComposerMode = .default
@ -105,7 +105,7 @@ struct RoomScreenViewState: BindableState {
bindings.composerText.count == 0 bindings.composerText.count == 0
} }
var itemIDs: [String] { var timelineIDs: [String] {
itemsDictionary.keys.elements itemsDictionary.keys.elements
} }
@ -129,7 +129,7 @@ struct RoomScreenViewStateBindings {
/// The state of wether reactions listed on the timeline are expanded/collapsed. /// The state of wether reactions listed on the timeline are expanded/collapsed.
/// Key is itemID, value is the collapsed state. /// 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. /// A media item that will be previewed with QuickLook.
var mediaPreviewItem: MediaPreviewItem? var mediaPreviewItem: MediaPreviewItem?
@ -146,14 +146,14 @@ struct RoomScreenViewStateBindings {
var reactionSummaryInfo: ReactionSummaryInfo? var reactionSummaryInfo: ReactionSummaryInfo?
} }
struct TimelineItemActionMenuInfo: Identifiable, Equatable { struct TimelineItemActionMenuInfo: Equatable, Identifiable {
static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool { static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool {
lhs.id == rhs.id lhs.id == rhs.id
} }
let item: EventBasedTimelineItemProtocol let item: EventBasedTimelineItemProtocol
var id: String { var id: TimelineItemIdentifier {
item.id item.id
} }
} }
@ -161,7 +161,7 @@ struct TimelineItemActionMenuInfo: Identifiable, Equatable {
struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol { struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol {
let title = L10n.screenRoomRetrySendMenuTitle let title = L10n.screenRoomRetrySendMenuTitle
let transactionID: String? let itemID: TimelineItemIdentifier
} }
struct ReactionSummaryInfo: Identifiable { struct ReactionSummaryInfo: Identifiable {

View File

@ -120,14 +120,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .tappedOnUser(userID: let userID): case .tappedOnUser(userID: let userID):
Task { await handleTappedUser(userID: userID) } Task { await handleTappedUser(userID: userID) }
case .displayEmojiPicker(let itemID): 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)) callback?(.displayEmojiPicker(itemID: itemID))
case .reactionSummary(let itemId, let key): case .reactionSummary(let itemID, let key):
showReactionSummary(for: itemId, selectedKey: key) showReactionSummary(for: itemID, selectedKey: key)
case .retrySend(let transactionID): case .retrySend(let itemID):
Task { await handleRetrySend(transactionID: transactionID) } Task { await handleRetrySend(itemID: itemID) }
case .cancelSend(let transactionID): case .cancelSend(let itemID):
Task { await handleCancelSend(transactionID: transactionID) } Task { await handleCancelSend(itemID: itemID) }
} }
} }
@ -221,9 +221,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
_ = await timelineController.markRoomAsRead() _ = await timelineController.markRoomAsRead()
} }
private func itemTapped(with itemId: String) async { private func itemTapped(with itemID: TimelineItemIdentifier) async {
state.showLoading = true state.showLoading = true
let action = await timelineController.processItemTap(itemId) let action = await timelineController.processItemTap(itemID)
switch action { switch action {
case .displayMediaFile(let file, let title): case .displayMediaFile(let file, let title):
@ -252,19 +252,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if itemGroup.count == 1 { if itemGroup.count == 1 {
if let firstItem = itemGroup.first { if let firstItem = itemGroup.first {
timelineItemsDictionary.updateValue(updateViewModel(item: firstItem, groupStyle: .single), timelineItemsDictionary.updateValue(updateViewModel(item: firstItem, groupStyle: .single),
forKey: firstItem.id) forKey: firstItem.id.timelineID)
} }
} else { } else {
for (index, item) in itemGroup.enumerated() { for (index, item) in itemGroup.enumerated() {
if index == 0 { if index == 0 {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .first), timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .first),
forKey: item.id) forKey: item.id.timelineID)
} else if index == itemGroup.count - 1 { } else if index == itemGroup.count - 1 {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .last), timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .last),
forKey: item.id) forKey: item.id.timelineID)
} else { } else {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .middle), 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 { 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.groupStyle = groupStyle
timelineItemViewModel.type = .init(item: item) timelineItemViewModel.type = .init(item: item)
return timelineItemViewModel return timelineItemViewModel
@ -360,7 +360,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: TimelineItemActionMenu // MARK: TimelineItemActionMenu
private func showTimelineItemActionMenu(for itemID: String) { private func showTimelineItemActionMenu(for itemID: TimelineItemIdentifier) {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }), guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
// Don't show a menu for non-event based items. // Don't show a menu for non-event based items.
@ -370,8 +370,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.actionMenuInfo = .init(item: eventTimelineItem) state.bindings.actionMenuInfo = .init(item: eventTimelineItem)
} }
private func timelineItemMenuActionsForItemId(_ itemId: String) -> TimelineItemMenuActions? { private func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }), guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let item = timelineItem as? EventBasedTimelineItemProtocol else { let item = timelineItem as? EventBasedTimelineItemProtocol else {
// Don't show a context menu for non-event based items. // Don't show a context menu for non-event based items.
return nil return nil
@ -395,7 +395,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
] ]
if item.isMessage { if item.isMessage {
actions.append(.forward(itemID: itemId)) actions.append(.forward(itemID: itemID))
} }
if item.isEditable { if item.isEditable {
@ -426,7 +426,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
// swiftlint:disable:next cyclomatic_complexity function_body_length // 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 }), guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return return
@ -449,7 +449,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
setComposerMode(.edit(originalItemId: messageTimelineItem.id)) setComposerMode(.edit(originalItemId: messageTimelineItem.id))
case .copyPermalink: case .copyPermalink:
do { 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) baseURL: appSettings.permalinkBaseURL)
UIPasteboard.general.url = permalink UIPasteboard.general.url = permalink
} catch { } catch {
@ -457,9 +462,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
case .redact: case .redact:
Task { Task {
if eventTimelineItem.hasFailedToSend, if eventTimelineItem.hasFailedToSend {
let transactionID = eventTimelineItem.properties.transactionID { await timelineController.cancelSend(itemID)
await timelineController.cancelSend(transactionID)
} else { } else {
await timelineController.redact(itemID) await timelineController.redact(itemID)
} }
@ -567,16 +571,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
} }
private func handleRetrySend(transactionID: String?) async { private func handleRetrySend(itemID: TimelineItemIdentifier) async {
guard let transactionID else { guard let transactionID = itemID.transactionID else {
MXLog.error("Failed Retry Send: missing transaction ID")
return return
} }
await roomProxy.retrySend(transactionID: transactionID) await roomProxy.retrySend(transactionID: transactionID)
} }
private func handleCancelSend(transactionID: String?) async { private func handleCancelSend(itemID: TimelineItemIdentifier) async {
guard let transactionID else { guard let transactionID = itemID.transactionID else {
MXLog.error("Failed Cancel Send: missing transaction ID")
return return
} }
@ -643,7 +649,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Reaction summary // 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 }), guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return return
@ -664,7 +670,7 @@ extension RoomScreenViewModel.Context {
/// A function to make it easier to bind to reactions expand/collapsed state /// A function to make it easier to bind to reactions expand/collapsed state
/// - Parameter itemID: The id of the timeline item the reacted to /// - Parameter itemID: The id of the timeline item the reacted to
/// - Returns: Wether the reactions should show in the collapsed state, true by default. /// - Returns: Wether the reactions should show in the collapsed state, true by default.
func reactionsCollapsedBinding(for itemID: String) -> Binding<Bool> { func reactionsCollapsedBinding(for itemID: TimelineItemIdentifier) -> Binding<Bool> {
Binding(get: { Binding(get: {
self.reactionsCollapsed[itemID] ?? true self.reactionsCollapsed[itemID] ?? true
}, set: { }, set: {

View File

@ -215,7 +215,7 @@ struct MessageComposer_Previews: PreviewProvider {
MessageComposer(text: .constant("Some message"), MessageComposer(text: .constant("Some message"),
focused: .constant(false), focused: .constant(false),
sendingDisabled: false, sendingDisabled: false,
mode: .edit(originalItemId: UUID().uuidString), mode: .edit(originalItemId: .init(timelineID: UUID().uuidString)),
sendAction: { }, sendAction: { },
pasteAction: { _ in }, pasteAction: { _ in },
replyCancellationAction: { }, replyCancellationAction: { },
@ -224,7 +224,7 @@ struct MessageComposer_Previews: PreviewProvider {
MessageComposer(text: .constant(""), MessageComposer(text: .constant(""),
focused: .constant(false), focused: .constant(false),
sendingDisabled: false, sendingDisabled: false,
mode: .reply(itemID: UUID().uuidString, mode: .reply(itemID: .init(timelineID: UUID().uuidString),
replyDetails: .loaded(sender: .init(id: "Kirk"), replyDetails: .loaded(sender: .init(id: "Kirk"),
contentType: .text(.init(body: "Text: Where the wild things are")))), contentType: .text(.init(body: "Text: Where the wild things are")))),
sendAction: { }, sendAction: { },
@ -256,7 +256,7 @@ struct MessageComposer_Previews: PreviewProvider {
MessageComposer(text: .constant(""), MessageComposer(text: .constant(""),
focused: .constant(false), focused: .constant(false),
sendingDisabled: false, sendingDisabled: false,
mode: .reply(itemID: UUID().uuidString, mode: .reply(itemID: .init(timelineID: UUID().uuidString),
replyDetails: replyDetails), replyDetails: replyDetails),
sendAction: { }, sendAction: { },
pasteAction: { _ in }, pasteAction: { _ in },

View File

@ -71,12 +71,12 @@ struct RoomScreen: View {
context.send(viewAction: .handlePasteOrDrop(provider: provider)) context.send(viewAction: .handlePasteOrDrop(provider: provider))
return true return true
} }
.confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { item in .confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { info in
Button(L10n.screenRoomRetrySendMenuSendAgainAction) { Button(L10n.screenRoomRetrySendMenuSendAgainAction) {
context.send(viewAction: .retrySend(transactionID: item.transactionID)) context.send(viewAction: .retrySend(itemID: info.itemID))
} }
Button(L10n.screenRoomRetrySendMenuRemoveAction, role: .destructive) { Button(L10n.screenRoomRetrySendMenuRemoveAction, role: .destructive) {
context.send(viewAction: .cancelSend(transactionID: item.transactionID)) context.send(viewAction: .cancelSend(itemID: info.itemID))
} }
} }
} }

View File

@ -125,7 +125,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id)) context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
} }
.onTapGesture { .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 // We need a tap gesture before this long one so that it doesn't
// steal away the gestures from the scroll view // steal away the gestures from the scroll view
@ -178,7 +178,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
if timelineItem.hasFailedToSend { if timelineItem.hasFailedToSend {
backgroundedLocalizedSendInfo backgroundedLocalizedSendInfo
.onTapGesture { .onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID) context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id)
} }
} else { } else {
backgroundedLocalizedSendInfo backgroundedLocalizedSendInfo
@ -335,7 +335,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
static var replies: some View { static var replies: some View {
VStack { VStack {
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "", RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@ -344,7 +344,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), groupStyle: .single)) contentType: .text(.init(body: "Short")))), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "", RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,

View File

@ -67,7 +67,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id)) context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
} }
.onTapGesture { .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 // We need a tap gesture before this long one so that it doesn't
// steal away the gestures from the scroll view // steal away the gestures from the scroll view

View File

@ -38,7 +38,7 @@ struct TimelineStyler<Content: View>: View {
struct TimelineItemStyler_Previews: PreviewProvider { struct TimelineItemStyler_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock 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 = { static let sentNonLast: TextRoomTimelineItem = {
var result = base var result = base
@ -53,8 +53,8 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}() }()
static let sendingLast: TextRoomTimelineItem = { static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.itemIDs.last ?? UUID().uuidString let id = viewModel.state.timelineIDs.last ?? UUID().uuidString
var result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) 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 result.properties.deliveryStatus = .sending
return result return result
}() }()
@ -66,22 +66,22 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}() }()
static let sentLast: TextRoomTimelineItem = { static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.itemIDs.last ?? UUID().uuidString let id = viewModel.state.timelineIDs.last ?? UUID().uuidString
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
return result 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 { static var testView: some View {
VStack { VStack {

View File

@ -23,7 +23,7 @@ struct TimelineItemStatusView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context @EnvironmentObject private var context: RoomScreenViewModel.Context
private var isLastOutgoingMessage: Bool { private var isLastOutgoingMessage: Bool {
context.viewState.itemIDs.last == timelineItem.id && context.viewState.timelineIDs.last == timelineItem.id.timelineID &&
timelineItem.isOutgoing timelineItem.isOutgoing
} }
@ -53,7 +53,7 @@ struct TimelineItemStatusView: View {
.foregroundColor(.compound.iconCriticalPrimary) .foregroundColor(.compound.iconCriticalPrimary)
.frame(width: 16, height: 16) .frame(width: 16, height: 16)
.onTapGesture { .onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID) context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id)
} }
} }
} }

View File

@ -25,7 +25,7 @@ struct TimelineReactionsView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context @EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
let itemID: String let itemID: TimelineItemIdentifier
let reactions: [AggregatedReaction] let reactions: [AggregatedReaction]
@Binding var collapsed: Bool @Binding var collapsed: Bool
@ -33,7 +33,7 @@ struct TimelineReactionsView: View {
CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) { CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
ForEach(reactions, id: \.self) { reaction in ForEach(reactions, id: \.self) { reaction in
TimelineReactionButton(itemID: itemID, reaction: reaction) { key 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 } showReactionSummary: { key in
context.send(viewAction: .reactionSummary(itemID: itemID, key: key)) context.send(viewAction: .reactionSummary(itemID: itemID, key: key))
} }
@ -93,7 +93,7 @@ struct TimelineCollapseButtonLabel: View {
} }
struct TimelineReactionButton: View { struct TimelineReactionButton: View {
let itemID: String let itemID: TimelineItemIdentifier
let reaction: AggregatedReaction let reaction: AggregatedReaction
let toggleReaction: (String) -> Void let toggleReaction: (String) -> Void
let showReactionSummary: (String) -> Void let showReactionSummary: (String) -> Void
@ -131,11 +131,11 @@ struct TimelineReactionViewPreviewsContainer: View {
var body: some View { var body: some View {
VStack { 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() Divider()
TimelineReactionsView(itemID: "2", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1) TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1)
Divider() Divider()
TimelineReactionsView(itemID: "3", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2) TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2)
.environment(\.layoutDirection, .rightToLeft) .environment(\.layoutDirection, .rightToLeft)
} }
.background(Color.red) .background(Color.red)

View File

@ -78,7 +78,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider {
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"), isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"),

View File

@ -44,7 +44,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider {
} }
static var body: some View { static var body: some View {
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: UUID().uuidString, AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -71,8 +71,8 @@ private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupS
struct CollapsibleRoomTimelineView_Previews: PreviewProvider { struct CollapsibleRoomTimelineView_Previews: PreviewProvider {
static let item = CollapsibleTimelineItem(items: [ static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: "First separator", text: "This is a separator"), SeparatorRoomTimelineItem(id: .init(timelineID: "First separator"), text: "This is a separator"),
SeparatorRoomTimelineItem(id: "Second separator", text: "This is another separator") SeparatorRoomTimelineItem(id: .init(timelineID: "Second separator"), text: "This is another separator")
]) ])
static var previews: some View { static var previews: some View {

View File

@ -55,7 +55,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
EmoteRoomTimelineItem(id: UUID().uuidString, EmoteRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -55,7 +55,7 @@ private struct EncryptedHistoryLabelStyle: LabelStyle {
struct EncryptedHistoryRoomTimelineView_Previews: PreviewProvider { struct EncryptedHistoryRoomTimelineView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let item = EncryptedHistoryRoomTimelineItem(id: UUID().uuidString) let item = EncryptedHistoryRoomTimelineItem(id: .init(timelineID: UUID().uuidString))
EncryptedHistoryRoomTimelineView(timelineItem: item) EncryptedHistoryRoomTimelineView(timelineItem: item)
} }
} }

View File

@ -67,7 +67,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { 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, body: text,
encryptionType: .unknown, encryptionType: .unknown,
timestamp: timestamp, timestamp: timestamp,

View File

@ -45,21 +45,21 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -66,21 +66,21 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -91,14 +91,14 @@ struct LocationRoomTimelineView_Previews: PreviewProvider {
@ViewBuilder @ViewBuilder
static var body: some View { static var body: some View {
LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description"))) content: .init(body: "Fallback geo uri description")))
LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString, LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -66,7 +66,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
NoticeRoomTimelineItem(id: UUID().uuidString, NoticeRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -41,8 +41,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
static let item = ReadMarkerRoomTimelineItem() static let item = ReadMarkerRoomTimelineItem()
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "", RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""),
timestamp: "", timestamp: "",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@ -51,8 +51,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
ReadMarkerRoomTimelineView(timelineItem: item) ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "", RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -42,7 +42,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
RedactedRoomTimelineItem(id: UUID().uuidString, RedactedRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: text, body: text,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,

View File

@ -31,7 +31,7 @@ struct SeparatorRoomTimelineView: View {
struct SeparatorRoomTimelineView_Previews: PreviewProvider { struct SeparatorRoomTimelineView_Previews: PreviewProvider {
static var previews: some View { 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) SeparatorRoomTimelineView(timelineItem: item)
} }
} }

View File

@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
StateRoomTimelineView(timelineItem: item) StateRoomTimelineView(timelineItem: item)
} }
static let item = StateRoomTimelineItem(id: UUID().uuidString, static let item = StateRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Alice joined", body: "Alice joined",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,

View File

@ -58,7 +58,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Some image", body: "Some image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Some other image", body: "Some other image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@ -74,7 +74,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Blurhashed image", body: "Blurhashed image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,

View File

@ -71,7 +71,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { 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, timestamp: timestamp,
isOutgoing: isOutgoing, isOutgoing: isOutgoing,
isEditable: isOutgoing, isEditable: isOutgoing,

View File

@ -63,7 +63,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem { 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, body: text,
eventType: "Some Event Type", eventType: "Some Event Type",
error: "Something went wrong", error: "Something went wrong",

View File

@ -77,21 +77,21 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil))) 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", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,

View File

@ -46,7 +46,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
case copyPermalink case copyPermalink
case redact case redact
case reply case reply
case forward(itemID: String) case forward(itemID: TimelineItemIdentifier)
case viewSource case viewSource
case retryDecryption(sessionID: String) case retryDecryption(sessionID: String)
case report case report
@ -188,7 +188,7 @@ public struct TimelineItemMenu: View {
private func reactionButton(for emoji: String) -> some View { private func reactionButton(for emoji: String) -> some View {
Button { Button {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
context.send(viewAction: .toggleReaction(key: emoji, eventID: item.id)) context.send(viewAction: .toggleReaction(key: emoji, itemID: item.id))
} label: { } label: {
Text(emoji) Text(emoji)
.padding(8.0) .padding(8.0)

View File

@ -72,7 +72,7 @@ class TimelineTableViewController: UIViewController {
} }
} }
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)? var contextMenuActionProvider: (@MainActor (_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
@Binding private var scrollToBottomButtonVisible: Bool @Binding private var scrollToBottomButtonVisible: Bool
@ -214,10 +214,10 @@ class TimelineTableViewController: UIViewController {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
.onAppear { .onAppear {
coordinator.send(viewAction: .itemAppeared(id: id)) coordinator.send(viewAction: .itemAppeared(itemID: viewModel.id))
} }
.onDisappear { .onDisappear {
coordinator.send(viewAction: .itemDisappeared(id: id)) coordinator.send(viewAction: .itemDisappeared(itemID: viewModel.id))
} }
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
coordinator.send(viewAction: .linkClicked(url: url)) coordinator.send(viewAction: .linkClicked(url: url))
@ -231,7 +231,7 @@ class TimelineTableViewController: UIViewController {
return cell return cell
} }
dataSource?.defaultRowAnimation = .fade // dataSource?.defaultRowAnimation = .automatic
tableView.delegate = self tableView.delegate = self
} }
@ -248,6 +248,8 @@ class TimelineTableViewController: UIViewController {
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>() var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems(timelineItemsIDs) snapshot.appendItems(timelineItemsIDs)
MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: dataSource.snapshot().itemIdentifiers))")
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
// Probably redundant now we observe content size changes // Probably redundant now we observe content size changes
@ -421,7 +423,7 @@ extension TimelineTableViewController {
private extension UITableView { private extension UITableView {
/// Returns the frame of the cell for a particular timeline item. /// Returns the frame of the cell for a particular timeline item.
func cellFrame(for id: String) -> CGRect? { 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 return nil
} }

View File

@ -31,8 +31,8 @@ class ClientProxy: ClientProxyProtocol {
private var roomListService: RoomListService? private var roomListService: RoomListService?
private var roomListStateUpdateTaskHandle: TaskHandle? private var roomListStateUpdateTaskHandle: TaskHandle?
private var encryptionSyncService: EncryptionSync? private var appService: App?
private var isEncryptionSyncing = false private var appServiceUpdateTaskHandle: TaskHandle?
var roomSummaryProvider: RoomSummaryProviderProtocol? var roomSummaryProvider: RoomSummaryProviderProtocol?
var inviteSummaryProvider: RoomSummaryProviderProtocol? var inviteSummaryProvider: RoomSummaryProviderProtocol?
@ -53,7 +53,7 @@ class ClientProxy: ClientProxyProtocol {
deinit { deinit {
client.setDelegate(delegate: nil) client.setDelegate(delegate: nil)
stopSync() pauseSync()
} }
let callbacks = PassthroughSubject<ClientProxyCallback, Never>() let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
@ -73,7 +73,7 @@ class ClientProxy: ClientProxyProtocol {
self?.callbacks.send(.updateRestorationToken) self?.callbacks.send(.updateRestorationToken)
}) })
await configureRoomListService() await configureAppService()
loadUserAvatarURLFromCache() loadUserAvatarURLFromCache()
} }
@ -110,13 +110,7 @@ class ClientProxy: ClientProxyProtocol {
} }
var isSyncing: Bool { var isSyncing: Bool {
let isRoomListServiceSyncing = roomListService?.isSyncing() ?? false roomListService?.isSyncing() ?? false
if ServiceLocator.shared.settings.isEncryptionSyncEnabled {
return isRoomListServiceSyncing && isEncryptionSyncing
} else {
return isRoomListServiceSyncing
}
} }
func startSync() { func startSync() {
@ -125,30 +119,23 @@ class ClientProxy: ClientProxyProtocol {
return return
} }
startEncryptionSyncService() do {
roomListService?.sync() try appService?.start()
} catch {
MXLog.error("Failed starting app service with error: \(error)")
}
} }
func stopSync() { func pauseSync() {
MXLog.info("Stopping sync") MXLog.info("Stopping sync")
stopEncryptionSyncService()
do { do {
try roomListService?.stopSync() try appService?.pause()
} catch { } 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<String?, ClientProxyError> { func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> {
await Task.dispatch(on: clientQueue) { await Task.dispatch(on: clientQueue) {
do { do {
@ -366,7 +353,7 @@ class ClientProxy: ClientProxyProtocol {
// MARK: Private // MARK: Private
private func restartSync() { private func restartSync() {
stopSync() pauseSync()
startSync() startSync()
} }
@ -385,88 +372,81 @@ class ClientProxy: ClientProxyProtocol {
} }
} }
private func startEncryptionSyncService() { private func configureAppService() async {
guard appSettings.isEncryptionSyncEnabled else { guard appService == nil 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 {
fatalError("This shouldn't be called more than once") fatalError("This shouldn't be called more than once")
} }
do { do {
let roomListService = try appSettings.isEncryptionSyncEnabled ? client.roomListService() : client.roomListServiceWithEncryption() let appService = try client
roomListStateUpdateTaskHandle = roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in .app()
guard let self else { return } .withEncryptionSync(withCrossProcessLock: appSettings.isEncryptionSyncEnabled,
MXLog.info("Received room list update: \(state)") appIdentifier: "MainApp")
.finish()
let roomListService = appService.roomListService()
// Restart the room list sync on every error for now let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
if state == .error { roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
self.restartSync() eventStringBuilder: eventStringBuilder,
name: "AllRooms")
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
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` // The invites are available only when entering `running`
if state == .running { if state == .running {
Task { Task {
do { 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 // Subscribe to invites later as the underlying SlidingSync list is only added when entering AllRooms
try await self.inviteSummaryProvider?.setRoomList(roomListService.invites()) try await self.inviteSummaryProvider?.setRoomList(roomListService.invites())
} catch { } catch {
MXLog.error("Failed configuring invites room list with error: \(error)") MXLog.error("Failed configuring invites room list with error: \(error)")
} }
} }
} callbacks.send(.receivedSyncUpdate)
// Anything that's not `running` is interpreted as "Loading data"
if state == .running {
self.callbacks.send(.receivedSyncUpdate)
} else { } else {
self.callbacks.send(.startedUpdating) callbacks.send(.startedUpdating)
} }
}) })
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)),
name: "AllRooms")
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)),
name: "Invites")
self.roomListService = roomListService
} catch {
MXLog.error("Failed building room list service with error: \(error)")
}
} }
private func roomTupleForIdentifier(_ identifier: String) -> (RoomListItem?, Room?) { private func roomTupleForIdentifier(_ identifier: String) -> (RoomListItem?, Room?) {
@ -496,10 +476,22 @@ extension ClientProxy: MediaLoaderProtocol {
} }
} }
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 class RoomListStateListenerProxy: RoomListServiceStateListener {
private let onUpdateClosure: (RoomListServiceState) -> Void private let onUpdateClosure: (RoomListServiceState) -> Void
init(_ onUpdateClosure: @escaping (RoomListServiceState) -> Void) { init(onUpdateClosure: @escaping (RoomListServiceState) -> Void) {
self.onUpdateClosure = onUpdateClosure self.onUpdateClosure = onUpdateClosure
} }

View File

@ -84,7 +84,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func startSync() func startSync()
func stopSync() func pauseSync()
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError>

View File

@ -45,7 +45,7 @@ class MockClientProxy: ClientProxyProtocol {
func stopSync(completionHandler: () -> Void) { } func stopSync(completionHandler: () -> Void) { }
func stopSync() { } func pauseSync() { }
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> { func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> {
.failure(.failedRetrievingDirectRoom) .failure(.failedRetrievingDirectRoom)

View File

@ -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)
}
}

View File

@ -67,15 +67,12 @@ extension NotificationItemProxyProtocol {
struct NotificationItemProxy: NotificationItemProxyProtocol { struct NotificationItemProxy: NotificationItemProxyProtocol {
let notificationItem: NotificationItem let notificationItem: NotificationItem
let receiverID: String let receiverID: String
let roomID: String
var event: TimelineEventProxyProtocol { var event: TimelineEventProxyProtocol {
TimelineEventProxy(timelineEvent: notificationItem.event) TimelineEventProxy(timelineEvent: notificationItem.event)
} }
var roomID: String {
notificationItem.roomInfo.id
}
var senderDisplayName: String? { var senderDisplayName: String? {
notificationItem.senderInfo.displayName notificationItem.senderInfo.displayName
} }

View File

@ -141,6 +141,22 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
MXLog.verbose("\(name): Finished applying \(diffs.count) diffs, new room list \(rooms.compactMap { $0.id ?? "Empty" })") 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 { private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary {
guard let roomListItem = try? roomListService.room(roomId: identifier) else { guard let roomListItem = try? roomListService.room(roomId: identifier) else {
MXLog.error("\(name): Failed finding room with id: \(identifier)") MXLog.error("\(name): Failed finding room with id: \(identifier)")
@ -150,8 +166,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
var attributedLastMessage: AttributedString? var attributedLastMessage: AttributedString?
var lastMessageFormattedTimestamp: String? var lastMessageFormattedTimestamp: String?
if let latestRoomMessage = roomListItem.latestEvent() { if let latestRoomMessage = fetchLastMessage(roomListItem: roomListItem) {
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage) let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: 0)
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal() lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage) attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
} }

View File

@ -19,15 +19,15 @@ import Foundation
enum RoomTimelineItemFixtures { enum RoomTimelineItemFixtures {
/// The default timeline items used in Xcode previews etc. /// The default timeline items used in Xcode previews etc.
static var `default`: [RoomTimelineItemProtocol] = [ static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(id: "Yesterday", text: "Yesterday"), SeparatorRoomTimelineItem(id: .init(timelineID: "Yesterday"), text: "Yesterday"),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:10 AM", timestamp: "10:10 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "", displayName: "Jacob"), sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"), content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)), properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@ -36,7 +36,7 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [ properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me"]) AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me"])
])), ])),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@ -46,21 +46,21 @@ enum RoomTimelineItemFixtures {
AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]), AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]),
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob"]) AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob"])
])), ])),
SeparatorRoomTimelineItem(id: "Today", text: "Today"), SeparatorRoomTimelineItem(id: .init(timelineID: "Today"), text: "Today"),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "", displayName: "Helena"), sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Heres the menu, let me know what you want its on me!"), content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Heres the menu, let me know what you want its on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])), properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
sender: .init(id: "", displayName: "Bob"), sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")), content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@ -71,7 +71,7 @@ enum RoomTimelineItemFixtures {
ReadReceipt(userID: "bob", formattedTimestamp: nil), ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil), ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])), ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString, TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@ -210,7 +210,7 @@ enum RoomTimelineItemFixtures {
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(text: String, senderDisplayName: String) { init(text: String, senderDisplayName: String) {
self.init(id: UUID().uuidString, self.init(id: .init(timelineID: UUID().uuidString),
timestamp: "10:47 am", timestamp: "10:47 am",
isOutgoing: senderDisplayName == "Alice", isOutgoing: senderDisplayName == "Alice",
isEditable: false, isEditable: false,

View File

@ -169,37 +169,47 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
} }
private extension TimelineItem { private extension TimelineItem {
var debugIdentifier: String { var debugIdentifier: DebugIdentifier {
if let virtualTimelineItem = asVirtual() { if let virtualTimelineItem = asVirtual() {
return virtualTimelineItem.debugIdentifier return virtualTimelineItem.debugIdentifier
} else if let eventTimelineItem = asEvent() { } 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 { private extension TimelineItemProxy {
var debugIdentifier: String { var debugIdentifier: DebugIdentifier {
switch self { switch self {
case .event(let eventTimelineItem): 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): case .virtual(let virtualTimelineItem):
return virtualTimelineItem.debugIdentifier return virtualTimelineItem.debugIdentifier
case .unknown: case .unknown:
return "UnknownTimelineItem" return .unknown
} }
} }
} }
private extension VirtualTimelineItem { private extension VirtualTimelineItem {
var debugIdentifier: String { var debugIdentifier: DebugIdentifier {
switch self { switch self {
case .dayDivider(let timestamp): case .dayDivider(let timestamp):
return "DayDiviver(\(timestamp))" return .virtual("DayDiviver(\(timestamp))")
case .readMarker: case .readMarker:
return "ReadMarker" return .virtual("ReadMarker")
} }
} }
} }
enum DebugIdentifier {
case event(timelineID: String?, eventID: String?, transactionID: String?)
case virtual(String)
case unknown
}

View File

@ -51,23 +51,23 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> { .success(()) } func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> { .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) .init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil)
} }

View File

@ -84,7 +84,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> { func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> {
guard roomProxy.hasUnreadNotifications, guard roomProxy.hasUnreadNotifications,
let eventID = timelineItems.last?.id let eventID = timelineItems.last?.id.eventID
else { return .success(()) } else { return .success(()) }
switch await roomProxy.sendReadReceipt(for: eventID) { 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 { guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return 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 { guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return .none 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 { if itemID == nil {
MXLog.info("Send message in \(roomID)") MXLog.info("Send message in \(roomID)")
} else { } else if let eventID = itemID?.eventID {
inReplyTo = eventID
MXLog.info("Send reply in \(roomID)") 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: case .success:
MXLog.info("Finished sending message") MXLog.info("Finished sending message")
case .failure(let error): 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)") 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: case .success:
MXLog.info("Finished toggling reaction") MXLog.info("Finished toggling reaction")
case .failure(let error): 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)") MXLog.info("Edit message in \(roomID)")
if let timelineItem = timelineItems.first(where: { $0.id == itemID }), if let timelineItem = timelineItems.first(where: { $0.id == itemID }),
let item = timelineItem as? EventBasedTimelineItemProtocol, let item = timelineItem as? EventBasedTimelineItemProtocol,
item.hasFailedToSend, item.hasFailedToSend {
let transactionID = item.properties.transactionID {
MXLog.info("Editing a failed echo, will cancel and resend it as a new message") MXLog.info("Editing a failed echo, will cancel and resend it as a new message")
await cancelSend(transactionID) await cancelSend(itemID)
await sendMessage(newMessage) await sendMessage(newMessage)
} else { } else if let eventID = itemID.eventID {
switch await roomProxy.editMessage(newMessage, original: itemID) { switch await roomProxy.editMessage(newMessage, original: eventID) {
case .success: case .success:
MXLog.info("Finished editing message") MXLog.info("Finished editing message")
case .failure(let error): case .failure(let error):
MXLog.error("Failed editing message with error: \(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)") 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: case .success:
MXLog.info("Finished redacting message") MXLog.info("Finished redacting message")
case .failure(let error): 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)") MXLog.info("Cancelling send in \(roomID)")
await roomProxy.cancelSend(transactionID: transactionID) await roomProxy.cancelSend(transactionID: transactionID)
} }
// Handle this parallel to the timeline items so we're not forced // Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them // 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 { for timelineItemProxy in timelineProvider.itemProxies {
switch timelineItemProxy { switch timelineItemProxy {
case .event(let item): case .event(let item):
@ -339,7 +357,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Separators without stable identifiers cause UI glitches // Separators without stable identifiers cause UI glitches
let identifier = "\(chunkIndex)-\(dateString)" let identifier = "\(chunkIndex)-\(dateString)"
return SeparatorRoomTimelineItem(id: identifier, text: dateString) return SeparatorRoomTimelineItem(id: .init(timelineID: identifier), text: dateString)
case .readMarker: case .readMarker:
return ReadMarkerRoomTimelineItem() return ReadMarkerRoomTimelineItem()
} }
@ -373,12 +391,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) { private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) {
guard let eventID = timelineItem.id.eventID else {
return
}
switch timelineItem.replyDetails { switch timelineItem.replyDetails {
case .notLoaded: case .notLoaded:
roomProxy.fetchDetails(for: timelineItem.id) roomProxy.fetchDetails(for: eventID)
case .error: case .error:
if refetchOnError { if refetchOnError {
roomProxy.fetchDetails(for: timelineItem.id) roomProxy.fetchDetails(for: eventID)
} }
default: default:
break break

View File

@ -41,27 +41,27 @@ protocol RoomTimelineControllerProtocol {
var timelineItems: [RoomTimelineItemProtocol] { get } var timelineItems: [RoomTimelineItemProtocol] { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { 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<Void, RoomTimelineControllerError> func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError>
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError>
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 func retryDecryption(for sessionID: String) async
} }

View File

@ -26,6 +26,4 @@ struct RoomTimelineItemProperties: Hashable {
var deliveryStatus: TimelineItemDeliveryStatus? var deliveryStatus: TimelineItemDeliveryStatus?
/// The read receipts of the item, ordered from newest to oldest /// The read receipts of the item, ordered from newest to oldest
var orderedReadReceipts: [ReadReceipt] = [] var orderedReadReceipts: [ReadReceipt] = []
/// The original transaction id transmitted by the client
var transactionID: String?
} }

View File

@ -17,6 +17,20 @@
import Foundation import Foundation
import MatrixRustSDK 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. /// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy { enum TimelineItemProxy {
case event(EventTimelineItemProxy) case event(EventTimelineItemProxy)
@ -25,7 +39,7 @@ enum TimelineItemProxy {
init(item: MatrixRustSDK.TimelineItem) { init(item: MatrixRustSDK.TimelineItem) {
if let eventItem = item.asEvent() { if let eventItem = item.asEvent() {
self = .event(EventTimelineItemProxy(item: eventItem)) self = .event(EventTimelineItemProxy(item: eventItem, id: item.uniqueId()))
} else if let virtualItem = item.asVirtual() { } else if let virtualItem = item.asVirtual() {
self = .virtual(virtualItem) self = .virtual(virtualItem)
} else { } else {
@ -44,17 +58,11 @@ enum TimelineItemDeliveryStatus: Hashable {
/// A light wrapper around event timeline items returned from Rust. /// A light wrapper around event timeline items returned from Rust.
struct EventTimelineItemProxy { struct EventTimelineItemProxy {
let item: MatrixRustSDK.EventTimelineItem let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier
init(item: MatrixRustSDK.EventTimelineItem) { init(item: MatrixRustSDK.EventTimelineItem, id: UInt64) {
self.item = item self.item = item
} self.id = TimelineItemIdentifier(timelineID: String(id), eventID: item.eventId(), transactionID: item.transactionId())
var id: String {
item.uniqueIdentifier()
}
var transactionID: String? {
item.transactionId()
} }
var deliveryStatus: TimelineItemDeliveryStatus? { var deliveryStatus: TimelineItemDeliveryStatus? {

View File

@ -16,8 +16,8 @@
import Foundation import Foundation
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -17,8 +17,8 @@
import UIKit import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -17,8 +17,8 @@
import UIKit import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -14,8 +14,8 @@
// limitations under the License. // limitations under the License.
// //
struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Hashable { struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool

View File

@ -17,8 +17,8 @@
import UIKit import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable { struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool
let isEditable: Bool let isEditable: Bool

View File

@ -16,10 +16,10 @@
import Foundation import Foundation
struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable { struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let items: [RoomTimelineItemProtocol] let items: [RoomTimelineItemProtocol]
let itemIDs: [String] let itemIDs: [TimelineItemIdentifier]
init(items: [RoomTimelineItemProtocol]) { init(items: [RoomTimelineItemProtocol]) {
self.items = items 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. // Technically not a correct implementation of equality as the items themselves could be updated.
lhs.id == rhs.id && lhs.itemIDs == rhs.itemIDs 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)
}
} }

View File

@ -16,14 +16,14 @@
import UIKit import UIKit
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
enum EncryptionType: Hashable { enum EncryptionType: Hashable {
case megolmV1AesSha2(sessionId: String) case megolmV1AesSha2(sessionId: String)
case olmV1Curve25519AesSha2(senderKey: String) case olmV1Curve25519AesSha2(senderKey: String)
case unknown case unknown
} }
let id: String let id: TimelineItemIdentifier
let body: String let body: String
let encryptionType: EncryptionType let encryptionType: EncryptionType
let timestamp: String let timestamp: String

View File

@ -17,8 +17,8 @@
import Foundation import Foundation
import UIKit import UIKit
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let body: String let body: String
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let body: String let body: String
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let body: String let body: String
let timestamp: String let timestamp: String
let isOutgoing: Bool let isOutgoing: Bool

View File

@ -16,8 +16,8 @@
import UIKit import UIKit
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let body: String let body: String
let eventType: String let eventType: String

View File

@ -16,6 +16,6 @@
import Foundation import Foundation
struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
} }

View File

@ -16,6 +16,6 @@
import Foundation import Foundation
struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = "paginationIndicatorTimelineItemIdentifier" let id = TimelineItemIdentifier(timelineID: "paginationIndicatorTimelineItemIdentifier")
} }

View File

@ -16,6 +16,6 @@
import Foundation import Foundation
struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = "readMarkerTimelineItemIdentifier" let id = TimelineItemIdentifier(timelineID: "readMarkerTimelineItemIdentifier")
} }

View File

@ -16,7 +16,7 @@
import Foundation import Foundation
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: String let id: TimelineItemIdentifier
let text: String let text: String
} }

View File

@ -16,7 +16,7 @@
import Foundation import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable { struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: String = UUID().uuidString let id = TimelineItemIdentifier(timelineID: UUID().uuidString)
let name: String? let name: String?
} }

View File

@ -138,8 +138,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
blurhash: imageInfo.blurhash, blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
@ -190,8 +189,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -208,8 +206,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -226,8 +223,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -244,8 +240,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -262,8 +257,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -280,8 +274,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -298,8 +291,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy, private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@ -316,13 +308,12 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions), reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus, deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts), orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
transactionID: eventItemProxy.transactionID))
} }
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in 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 .sorted { a, b in
// Sort by count and then by key for a consistence experience. // Sort by count and then by key for a consistence experience.

View File

@ -18,5 +18,5 @@ import Foundation
import UIKit import UIKit
protocol RoomTimelineItemProtocol { protocol RoomTimelineItemProtocol {
var id: String { get } var id: TimelineItemIdentifier { get }
} }

View File

@ -24,7 +24,7 @@ final class RoomTimelineItemViewModel: Identifiable, Equatable, ObservableObject
@Published var type: RoomTimelineItemType @Published var type: RoomTimelineItemType
@Published var groupStyle: TimelineGroupStyle @Published var groupStyle: TimelineGroupStyle
var id: String { var id: TimelineItemIdentifier {
type.id type.id
} }
@ -109,7 +109,7 @@ enum RoomTimelineItemType: Equatable {
} }
} }
var id: String { var id: TimelineItemIdentifier {
switch self { switch self {
case .text(let item as RoomTimelineItemProtocol), case .text(let item as RoomTimelineItemProtocol),
.separator(let item as RoomTimelineItemProtocol), .separator(let item as RoomTimelineItemProtocol),

View File

@ -474,7 +474,7 @@ class MockScreen: Identifiable {
return navigationStackCoordinator return navigationStackCoordinator
case .reportContent: case .reportContent:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = ReportContentScreenCoordinator(parameters: .init(itemID: "test", let coordinator = ReportContentScreenCoordinator(parameters: .init(eventID: "test",
senderID: RoomMemberProxyMock.mockAlice.userID, senderID: RoomMemberProxyMock.mockAlice.userID,
roomProxy: RoomProxyMock(with: .init(displayName: "test")))) roomProxy: RoomProxyMock(with: .init(displayName: "test"))))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)

View File

@ -71,26 +71,9 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)") MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)")
do { do {
let userSession = try NSEUserSession(credentials: credentials) let userSession = try NSEUserSession(credentials: credentials, isEncryptionSyncEnabled: settings.isEncryptionSyncEnabled)
self.userSession = userSession self.userSession = userSession
var itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else {
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 {
MXLog.info("\(tag) no notification for the event, discard") MXLog.info("\(tag) no notification for the event, discard")
return discard() return discard()
} }
@ -146,7 +129,6 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
private func cleanUp() { private func cleanUp() {
handler = nil handler = nil
modifiedContent = nil modifiedContent = nil
userSession?.stopEncryptionSync()
} }
deinit { deinit {

View File

@ -18,56 +18,39 @@ import Foundation
import MatrixRustSDK import MatrixRustSDK
final class NSEUserSession { final class NSEUserSession {
private let client: ClientProtocol private let baseClient: Client
private let notificationClient: NotificationClient
private let userID: String private let userID: String
private var encryptionSyncService: EncryptionSync? private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient),
private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: client),
imageCache: .onlyOnDisk, imageCache: .onlyOnDisk,
backgroundTaskService: nil) backgroundTaskService: nil)
var isSyncing: Bool { init(credentials: KeychainCredentials, isEncryptionSyncEnabled: Bool) throws {
encryptionSyncService != nil
}
init(credentials: KeychainCredentials) throws {
userID = credentials.userID userID = credentials.userID
let builder = ClientBuilder() baseClient = try ClientBuilder()
.basePath(path: URL.sessionsBaseDirectory.path) .basePath(path: URL.sessionsBaseDirectory.path)
.username(username: credentials.userID) .username(username: credentials.userID)
.build()
client = try builder.build() try baseClient.restoreSession(session: credentials.restorationToken.session)
try client.restoreSession(session: credentials.restorationToken.session)
}
func startEncryptionSync() throws { notificationClient = baseClient
let listener = EncryptionSyncListenerProxy { [weak self] reason in .notificationClient()
MXLog.info("NSE: Encryption sync terminated for user: \(self?.userID ?? "unknown") with reason: \(reason)") .retryDecryption(withCrossProcessLock: isEncryptionSyncEnabled)
self?.encryptionSyncService = nil .finish()
}
encryptionSyncService = try client.notificationEncryptionSync(id: "NSE", listener: listener, numIters: 2)
MXLog.info("NSE: Encryption sync started for user: \(userID)")
} }
func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? {
await Task.dispatch(on: .global()) { await Task.dispatch(on: .global()) {
do { 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 nil
} }
return NotificationItemProxy(notificationItem: notification, receiverID: self.userID) return NotificationItemProxy(notificationItem: notification, receiverID: self.userID, roomID: roomID)
} catch { } catch {
MXLog.error("NSE: Could not get notification's content creating an empty notification instead, error: \(error)") 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) return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: self.userID)
} }
} }
} }
func stopEncryptionSync() {
encryptionSyncService?.stop()
}
deinit {
MXLog.info("NSE: NSEUserSession deinit called for user: \(userID)")
stopEncryptionSync()
}
} }

View File

@ -252,39 +252,39 @@ class LoggingTests: XCTestCase {
func testTimelineContentIsRedacted() throws { func testTimelineContentIsRedacted() throws {
// Given timeline items that contain text // Given timeline items that contain text
let textAttributedString = "TextAttributed" let textAttributedString = "TextAttributed"
let textMessage = TextRoomTimelineItem(id: "mytextmessage", let textMessage = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString))) content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
let noticeAttributedString = "NoticeAttributed" let noticeAttributedString = "NoticeAttributed"
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", let noticeMessage = NoticeRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString))) content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
let emoteAttributedString = "EmoteAttributed" let emoteAttributedString = "EmoteAttributed"
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", let emoteMessage = EmoteRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString))) content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", let imageMessage = ImageRoomTimelineItem(id: .init(timelineID: "myimagemessage"),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil)) 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: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil)) content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil))
let fileMessage = FileRoomTimelineItem(id: "myfilemessage", let fileMessage = FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@ -310,25 +310,25 @@ class LoggingTests: XCTestCase {
} }
let content = try String(contentsOf: logFile) 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(textMessage.body))
XCTAssertFalse(content.contains(textAttributedString)) XCTAssertFalse(content.contains(textAttributedString))
XCTAssertTrue(content.contains(noticeMessage.id)) XCTAssertTrue(content.contains(noticeMessage.id.timelineID))
XCTAssertFalse(content.contains(noticeMessage.body)) XCTAssertFalse(content.contains(noticeMessage.body))
XCTAssertFalse(content.contains(noticeAttributedString)) XCTAssertFalse(content.contains(noticeAttributedString))
XCTAssertTrue(content.contains(emoteMessage.id)) XCTAssertTrue(content.contains(emoteMessage.id.timelineID))
XCTAssertFalse(content.contains(emoteMessage.body)) XCTAssertFalse(content.contains(emoteMessage.body))
XCTAssertFalse(content.contains(emoteAttributedString)) XCTAssertFalse(content.contains(emoteAttributedString))
XCTAssertTrue(content.contains(imageMessage.id)) XCTAssertTrue(content.contains(imageMessage.id.timelineID))
XCTAssertFalse(content.contains(imageMessage.body)) XCTAssertFalse(content.contains(imageMessage.body))
XCTAssertTrue(content.contains(videoMessage.id)) XCTAssertTrue(content.contains(videoMessage.id.timelineID))
XCTAssertFalse(content.contains(videoMessage.body)) XCTAssertFalse(content.contains(videoMessage.body))
XCTAssertTrue(content.contains(fileMessage.id)) XCTAssertTrue(content.contains(fileMessage.id.timelineID))
XCTAssertFalse(content.contains(fileMessage.body)) XCTAssertFalse(content.contains(fileMessage.body))
} }

View File

@ -19,7 +19,7 @@ import XCTest
@MainActor @MainActor
class ReportContentScreenViewModelTests: XCTestCase { class ReportContentScreenViewModelTests: XCTestCase {
let itemID = "test-id" let eventID = "test-id"
let senderID = "@meany:server.com" let senderID = "@meany:server.com"
let reportReason = "I don't like it." let reportReason = "I don't like it."
@ -27,7 +27,7 @@ class ReportContentScreenViewModelTests: XCTestCase {
// Given the report content view for some content. // Given the report content view for some content.
let roomProxy = RoomProxyMock(with: .init(displayName: "test")) let roomProxy = RoomProxyMock(with: .init(displayName: "test"))
roomProxy.reportContentReasonReturnValue = .success(()) roomProxy.reportContentReasonReturnValue = .success(())
let viewModel = ReportContentScreenViewModel(itemID: itemID, let viewModel = ReportContentScreenViewModel(eventID: eventID,
senderID: senderID, senderID: senderID,
roomProxy: roomProxy) roomProxy: roomProxy)
@ -43,7 +43,7 @@ class ReportContentScreenViewModelTests: XCTestCase {
// Then the content should be reported, but the user should not be included. // 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.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.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.") 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.") XCTAssertNil(roomProxy.ignoreUserReceivedUserID, "The sender shouldn't have been ignored.")
@ -54,7 +54,7 @@ class ReportContentScreenViewModelTests: XCTestCase {
let roomProxy = RoomProxyMock(with: .init(displayName: "test")) let roomProxy = RoomProxyMock(with: .init(displayName: "test"))
roomProxy.reportContentReasonReturnValue = .success(()) roomProxy.reportContentReasonReturnValue = .success(())
roomProxy.ignoreUserReturnValue = .success(()) roomProxy.ignoreUserReturnValue = .success(())
let viewModel = ReportContentScreenViewModel(itemID: itemID, let viewModel = ReportContentScreenViewModel(eventID: eventID,
senderID: senderID, senderID: senderID,
roomProxy: roomProxy) roomProxy: roomProxy)
@ -69,7 +69,7 @@ class ReportContentScreenViewModelTests: XCTestCase {
// Then the content should be reported, and the user should be ignored. // Then the content should be reported, and the user should be ignored.
XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") 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.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.ignoreUserCallsCount, 1, "A call should have been made to ignore the sender.")
XCTAssertEqual(roomProxy.ignoreUserReceivedUserID, senderID, "The ignored user ID should match the sender.") XCTAssertEqual(roomProxy.ignoreUserReceivedUserID, senderID, "The ignored user ID should match the sender.")

View File

@ -289,7 +289,8 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Test // 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)) try? await Task.sleep(for: .microseconds(500))
XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 1)
XCTAssert(roomProxyMock.retrySendTransactionIDReceivedInvocations == ["test retry send id"]) XCTAssert(roomProxyMock.retrySendTransactionIDReceivedInvocations == ["test retry send id"])
@ -308,7 +309,7 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Test // Test
viewModel.context.send(viewAction: .retrySend(transactionID: nil)) viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString)))
await Task.yield() await Task.yield()
XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 0) XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 0)
} }
@ -326,7 +327,7 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Test // 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)) try? await Task.sleep(for: .microseconds(500))
XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 1) XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 1)
XCTAssert(roomProxyMock.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"]) XCTAssert(roomProxyMock.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"])
@ -345,12 +346,12 @@ class RoomScreenViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock) userIndicatorController: userIndicatorControllerMock)
// Test // Test
viewModel.context.send(viewAction: .cancelSend(transactionID: nil)) viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString)))
await Task.yield() await Task.yield()
XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0) XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0)
} }
func testMarkAsRead() async { func testMarkAsRead() async throws {
// Setup // Setup
let notificationCenterMock = NotificationCenterMock() let notificationCenterMock = NotificationCenterMock()
let timelineController = MockRoomTimelineController() let timelineController = MockRoomTimelineController()
@ -365,7 +366,7 @@ class RoomScreenViewModelTests: XCTestCase {
notificationCenterProtocol: notificationCenterMock) notificationCenterProtocol: notificationCenterMock)
viewModel.context.send(viewAction: .markRoomAsRead) viewModel.context.send(viewAction: .markRoomAsRead)
await Task.yield() try await Task.sleep(for: .microseconds(100))
XCTAssertEqual(notificationCenterMock.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead) XCTAssertEqual(notificationCenterMock.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead)
let roomID = notificationCenterMock.postNameObjectReceivedArguments?.anObject as? String let roomID = notificationCenterMock.postNameObjectReceivedArguments?.anObject as? String
XCTAssertEqual(roomID, roomProxyMock.id) XCTAssertEqual(roomID, roomProxyMock.id)
@ -375,7 +376,7 @@ class RoomScreenViewModelTests: XCTestCase {
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(text: String, sender: String, addReactions: Bool = false) { init(text: String, sender: String, addReactions: Bool = false) {
let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [sender])] : [] 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", timestamp: "10:47 am",
isOutgoing: sender == "bob", isOutgoing: sender == "bob",
isEditable: sender == "bob", isEditable: sender == "bob",

View File

@ -20,7 +20,7 @@ import XCTest
final class TextBasedRoomTimelineTests: XCTestCase { final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEnd() { func testTextRoomTimelineItemWhitespaceEnd() {
let timestamp = "Now" 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<TextRoomTimelineItem>() let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles view.timelineStyle = .bubbles
@ -30,7 +30,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndLonger() { func testTextRoomTimelineItemWhitespaceEndLonger() {
let timestamp = "10:00 AM" 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<TextRoomTimelineItem>() let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles view.timelineStyle = .bubbles
@ -39,7 +39,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
} }
func testTextRoomTimelineItemWhitespaceEndPlain() { 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<TextRoomTimelineItem>() let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem view.underlyingTimelineItem = timelineItem
view.timelineStyle = .plain view.timelineStyle = .plain
@ -49,7 +49,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndWithEdit() { func testTextRoomTimelineItemWhitespaceEndWithEdit() {
let timestamp = "Now" 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.isEdited = true
let editedCount = L10n.commonEditedSuffix.count let editedCount = L10n.commonEditedSuffix.count
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>() let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
@ -61,7 +61,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() { func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() {
let timestamp = "Now" 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.isEdited = true
timelineItem.properties.deliveryStatus = .sendingFailed timelineItem.properties.deliveryStatus = .sendingFailed
let editedCount = L10n.commonEditedSuffix.count let editedCount = L10n.commonEditedSuffix.count

View File

@ -44,7 +44,7 @@ include:
packages: packages:
MatrixRustSDK: MatrixRustSDK:
url: https://github.com/matrix-org/matrix-rust-components-swift url: https://github.com/matrix-org/matrix-rust-components-swift
exactVersion: 1.0.96-alpha exactVersion: 1.0.98-alpha
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
DesignKit: DesignKit:
path: DesignKit path: DesignKit