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