SDK Changes Refactor (#1312)

* internal id

* notification refactor

* client proxy refactor

* required self

* better identifier system

* using the event id when required

* tests fixed

* tested some stuff

* fixed merge conflict

* improved the test wait

* animation disabled

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

* code improvement

* pause sync

* pr suggestions

* result

* Apply suggestions from code review

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

* enum based debug identifier

---------

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

View File

@ -278,7 +278,6 @@
6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; };
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" */ = {

View File

@ -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"
}
},
{

View File

@ -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">

View File

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

View File

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

View File

@ -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?

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

@ -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: {

View File

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

View File

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

View File

@ -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,

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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"),

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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",

View File

@ -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,

View File

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

View File

@ -72,7 +72,7 @@ class TimelineTableViewController: UIViewController {
}
}
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)?
var contextMenuActionProvider: (@MainActor (_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
@Binding private var scrollToBottomButtonVisible: Bool
@ -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
}

View File

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

View File

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

View File

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

View File

@ -1,31 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixRustSDK
final class EncryptionSyncListenerProxy: EncryptionSyncListener {
private let didTerminateClosure: (EncryptionSyncTerminationReason) -> Void
init(_ didTerminateClosure: @escaping (EncryptionSyncTerminationReason) -> Void) {
self.didTerminateClosure = didTerminateClosure
}
func didTerminate(reason: EncryptionSyncTerminationReason) {
didTerminateClosure(reason)
}
}

View File

@ -67,15 +67,12 @@ extension NotificationItemProxyProtocol {
struct NotificationItemProxy: NotificationItemProxyProtocol {
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
}

View File

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

View File

@ -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? Heres the menu, let me know what you want its 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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? {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -24,7 +24,7 @@ final class RoomTimelineItemViewModel: Identifiable, Equatable, ObservableObject
@Published var type: RoomTimelineItemType
@Published var 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),

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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.")

View File

@ -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",

View File

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

View File

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