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
83 changed files with 551 additions and 661 deletions
@@ -542,7 +542,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
// MARK: - Application State
private func stopSync() {
userSession?.clientProxy.stopSync()
userSession?.clientProxy.pauseSync()
backgroundAppRefreshTask?.setTaskCompleted(success: true)
backgroundAppRefreshTask = nil
@@ -602,7 +602,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
@objc
private func applicationWillTerminate() {
userSession?.clientProxy.stopSync()
userSession?.clientProxy.pauseSync()
}
@objc
@@ -616,7 +616,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in
guard let self else { return }
userSession?.clientProxy.stopSync()
userSession?.clientProxy.pauseSync()
backgroundTask?.stop()
backgroundTask = nil
@@ -207,8 +207,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomMemberDetails, .dismissRoomMemberDetails, .room):
break
case (.room, .presentMessageForwarding(let eventID), .messageForwarding):
presentMessageForwarding(for: eventID)
case (.room, .presentMessageForwarding(let itemID), .messageForwarding):
presentMessageForwarding(for: itemID)
case (.messageForwarding, .dismissMessageForwarding, .room):
break
case (.room, .presentMapNavigator(let mode), .mapNavigator):
@@ -401,14 +401,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentReportContent(for itemID: String, from senderID: String) {
guard let roomProxy else {
private func presentReportContent(for itemID: TimelineItemIdentifier, from senderID: String) {
guard let roomProxy, let eventID = itemID.eventID else {
fatalError()
}
let navigationCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator)
let parameters = ReportContentScreenCoordinatorParameters(itemID: itemID,
let parameters = ReportContentScreenCoordinatorParameters(eventID: eventID,
senderID: senderID,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController)
@@ -479,17 +479,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentEmojiPicker(for itemId: String) {
private func presentEmojiPicker(for itemID: TimelineItemIdentifier) {
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
itemId: itemId)
itemID: itemID)
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
coordinator.callback = { [weak self] action in
switch action {
case let .emojiSelected(emoji: emoji, itemId: itemId):
MXLog.debug("Selected \(emoji) for \(itemId)")
case let .emojiSelected(emoji: emoji, itemID: itemID):
MXLog.debug("Selected \(emoji) for \(itemID)")
self?.navigationStackCoordinator.setSheetCoordinator(nil)
Task {
await self?.timelineController?.toggleReaction(emoji, to: itemId)
await self?.timelineController?.toggleReaction(emoji, to: itemID)
}
case .dismiss:
self?.navigationStackCoordinator.setSheetCoordinator(nil)
@@ -547,8 +547,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentMessageForwarding(for eventID: String) {
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider else {
private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider, let eventID = itemID.eventID else {
fatalError()
}
@@ -624,14 +624,14 @@ private extension RoomFlowCoordinator {
enum State: StateType {
case initial
case room(roomID: String)
case reportContent(roomID: String, itemID: String, senderID: String)
case reportContent(roomID: String, itemID: TimelineItemIdentifier, senderID: String)
case roomDetails(roomID: String, isRoot: Bool)
case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource)
case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: String)
case emojiPicker(roomID: String, itemID: TimelineItemIdentifier)
case mapNavigator(roomID: String)
case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper)
case messageForwarding(roomID: String, itemID: String)
case messageForwarding(roomID: String, itemID: TimelineItemIdentifier)
}
struct EventUserInfo {
@@ -643,7 +643,7 @@ private extension RoomFlowCoordinator {
case presentRoom(roomID: String)
case dismissRoom
case presentReportContent(itemID: String, senderID: String)
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case dismissReportContent
case presentRoomDetails(roomID: String)
@@ -655,7 +655,7 @@ private extension RoomFlowCoordinator {
case presentMediaUploadPreview(fileURL: URL)
case dismissMediaUploadPreview
case presentEmojiPicker(itemID: String)
case presentEmojiPicker(itemID: TimelineItemIdentifier)
case dismissEmojiPicker
case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
@@ -664,7 +664,7 @@ private extension RoomFlowCoordinator {
case presentRoomMemberDetails(member: HashableRoomMemberWrapper)
case dismissRoomMemberDetails
case presentMessageForwarding(itemID: String)
case presentMessageForwarding(itemID: TimelineItemIdentifier)
case dismissMessageForwarding
}
}
@@ -31,6 +31,23 @@ class SDKClientMock: SDKClientProtocol {
return accountDataEventTypeReturnValue
}
}
//MARK: - `app`
public var appCallsCount = 0
public var appCalled: Bool {
return appCallsCount > 0
}
public var appReturnValue: AppBuilder!
public var appClosure: (() -> AppBuilder)?
public func `app`() -> AppBuilder {
appCallsCount += 1
if let appClosure = appClosure {
return appClosure()
} else {
return appReturnValue
}
}
//MARK: - `avatarUrl`
public var avatarUrlThrowableError: Error?
@@ -240,29 +257,21 @@ class SDKClientMock: SDKClientProtocol {
return getMediaThumbnailMediaSourceWidthHeightReturnValue
}
}
//MARK: - `getNotificationItem`
//MARK: - `getNotificationSettings`
public var getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError: Error?
public var getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount = 0
public var getNotificationItemRoomIdEventIdFilterByPushRulesCalled: Bool {
return getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount > 0
public var getNotificationSettingsCallsCount = 0
public var getNotificationSettingsCalled: Bool {
return getNotificationSettingsCallsCount > 0
}
public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedArguments: (`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)?
public var getNotificationItemRoomIdEventIdFilterByPushRulesReceivedInvocations: [(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool)] = []
public var getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue: NotificationItem?
public var getNotificationItemRoomIdEventIdFilterByPushRulesClosure: ((String, String, Bool) throws -> NotificationItem?)?
public var getNotificationSettingsReturnValue: NotificationSettings!
public var getNotificationSettingsClosure: (() -> NotificationSettings)?
public func `getNotificationItem`(`roomId`: String, `eventId`: String, `filterByPushRules`: Bool) throws -> NotificationItem? {
if let error = getNotificationItemRoomIdEventIdFilterByPushRulesThrowableError {
throw error
}
getNotificationItemRoomIdEventIdFilterByPushRulesCallsCount += 1
getNotificationItemRoomIdEventIdFilterByPushRulesReceivedArguments = (roomId: roomId, eventId: eventId, filterByPushRules: filterByPushRules)
getNotificationItemRoomIdEventIdFilterByPushRulesReceivedInvocations.append((roomId: roomId, eventId: eventId, filterByPushRules: filterByPushRules))
if let getNotificationItemRoomIdEventIdFilterByPushRulesClosure = getNotificationItemRoomIdEventIdFilterByPushRulesClosure {
return try getNotificationItemRoomIdEventIdFilterByPushRulesClosure(`roomId`, `eventId`, `filterByPushRules`)
public func `getNotificationSettings`() -> NotificationSettings {
getNotificationSettingsCallsCount += 1
if let getNotificationSettingsClosure = getNotificationSettingsClosure {
return getNotificationSettingsClosure()
} else {
return getNotificationItemRoomIdEventIdFilterByPushRulesReturnValue
return getNotificationSettingsReturnValue
}
}
//MARK: - `getProfile`
@@ -384,54 +393,21 @@ class SDKClientMock: SDKClientProtocol {
logoutCallsCount += 1
try logoutClosure?()
}
//MARK: - `mainEncryptionSync`
//MARK: - `notificationClient`
public var mainEncryptionSyncIdListenerThrowableError: Error?
public var mainEncryptionSyncIdListenerCallsCount = 0
public var mainEncryptionSyncIdListenerCalled: Bool {
return mainEncryptionSyncIdListenerCallsCount > 0
public var notificationClientCallsCount = 0
public var notificationClientCalled: Bool {
return notificationClientCallsCount > 0
}
public var mainEncryptionSyncIdListenerReceivedArguments: (`id`: String, `listener`: EncryptionSyncListener)?
public var mainEncryptionSyncIdListenerReceivedInvocations: [(`id`: String, `listener`: EncryptionSyncListener)] = []
public var mainEncryptionSyncIdListenerReturnValue: EncryptionSync!
public var mainEncryptionSyncIdListenerClosure: ((String, EncryptionSyncListener) throws -> EncryptionSync)?
public var notificationClientReturnValue: NotificationClientBuilder!
public var notificationClientClosure: (() -> NotificationClientBuilder)?
public func `mainEncryptionSync`(`id`: String, `listener`: EncryptionSyncListener) throws -> EncryptionSync {
if let error = mainEncryptionSyncIdListenerThrowableError {
throw error
}
mainEncryptionSyncIdListenerCallsCount += 1
mainEncryptionSyncIdListenerReceivedArguments = (id: id, listener: listener)
mainEncryptionSyncIdListenerReceivedInvocations.append((id: id, listener: listener))
if let mainEncryptionSyncIdListenerClosure = mainEncryptionSyncIdListenerClosure {
return try mainEncryptionSyncIdListenerClosure(`id`, `listener`)
public func `notificationClient`() -> NotificationClientBuilder {
notificationClientCallsCount += 1
if let notificationClientClosure = notificationClientClosure {
return notificationClientClosure()
} else {
return mainEncryptionSyncIdListenerReturnValue
}
}
//MARK: - `notificationEncryptionSync`
public var notificationEncryptionSyncIdListenerNumItersThrowableError: Error?
public var notificationEncryptionSyncIdListenerNumItersCallsCount = 0
public var notificationEncryptionSyncIdListenerNumItersCalled: Bool {
return notificationEncryptionSyncIdListenerNumItersCallsCount > 0
}
public var notificationEncryptionSyncIdListenerNumItersReceivedArguments: (`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8)?
public var notificationEncryptionSyncIdListenerNumItersReceivedInvocations: [(`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8)] = []
public var notificationEncryptionSyncIdListenerNumItersReturnValue: EncryptionSync!
public var notificationEncryptionSyncIdListenerNumItersClosure: ((String, EncryptionSyncListener, UInt8) throws -> EncryptionSync)?
public func `notificationEncryptionSync`(`id`: String, `listener`: EncryptionSyncListener, `numIters`: UInt8) throws -> EncryptionSync {
if let error = notificationEncryptionSyncIdListenerNumItersThrowableError {
throw error
}
notificationEncryptionSyncIdListenerNumItersCallsCount += 1
notificationEncryptionSyncIdListenerNumItersReceivedArguments = (id: id, listener: listener, numIters: numIters)
notificationEncryptionSyncIdListenerNumItersReceivedInvocations.append((id: id, listener: listener, numIters: numIters))
if let notificationEncryptionSyncIdListenerNumItersClosure = notificationEncryptionSyncIdListenerNumItersClosure {
return try notificationEncryptionSyncIdListenerNumItersClosure(`id`, `listener`, `numIters`)
} else {
return notificationEncryptionSyncIdListenerNumItersReturnValue
return notificationClientReturnValue
}
}
//MARK: - `restoreSession`
@@ -454,48 +430,6 @@ class SDKClientMock: SDKClientProtocol {
restoreSessionSessionReceivedInvocations.append(`session`)
try restoreSessionSessionClosure?(`session`)
}
//MARK: - `roomListService`
public var roomListServiceThrowableError: Error?
public var roomListServiceCallsCount = 0
public var roomListServiceCalled: Bool {
return roomListServiceCallsCount > 0
}
public var roomListServiceReturnValue: RoomListService!
public var roomListServiceClosure: (() throws -> RoomListService)?
public func `roomListService`() throws -> RoomListService {
if let error = roomListServiceThrowableError {
throw error
}
roomListServiceCallsCount += 1
if let roomListServiceClosure = roomListServiceClosure {
return try roomListServiceClosure()
} else {
return roomListServiceReturnValue
}
}
//MARK: - `roomListServiceWithEncryption`
public var roomListServiceWithEncryptionThrowableError: Error?
public var roomListServiceWithEncryptionCallsCount = 0
public var roomListServiceWithEncryptionCalled: Bool {
return roomListServiceWithEncryptionCallsCount > 0
}
public var roomListServiceWithEncryptionReturnValue: RoomListService!
public var roomListServiceWithEncryptionClosure: (() throws -> RoomListService)?
public func `roomListServiceWithEncryption`() throws -> RoomListService {
if let error = roomListServiceWithEncryptionThrowableError {
throw error
}
roomListServiceWithEncryptionCallsCount += 1
if let roomListServiceWithEncryptionClosure = roomListServiceWithEncryptionClosure {
return try roomListServiceWithEncryptionClosure()
} else {
return roomListServiceWithEncryptionReturnValue
}
}
//MARK: - `rooms`
public var roomsCallsCount = 0
@@ -615,22 +549,6 @@ class SDKClientMock: SDKClientProtocol {
setDisplayNameNameReceivedInvocations.append(`name`)
try setDisplayNameNameClosure?(`name`)
}
//MARK: - `setNotificationDelegate`
public var setNotificationDelegateNotificationDelegateCallsCount = 0
public var setNotificationDelegateNotificationDelegateCalled: Bool {
return setNotificationDelegateNotificationDelegateCallsCount > 0
}
public var setNotificationDelegateNotificationDelegateReceivedNotificationDelegate: NotificationDelegate?
public var setNotificationDelegateNotificationDelegateReceivedInvocations: [NotificationDelegate?] = []
public var setNotificationDelegateNotificationDelegateClosure: ((NotificationDelegate?) -> Void)?
public func `setNotificationDelegate`(`notificationDelegate`: NotificationDelegate?) {
setNotificationDelegateNotificationDelegateCallsCount += 1
setNotificationDelegateNotificationDelegateReceivedNotificationDelegate = notificationDelegate
setNotificationDelegateNotificationDelegateReceivedInvocations.append(`notificationDelegate`)
setNotificationDelegateNotificationDelegateClosure?(`notificationDelegate`)
}
//MARK: - `setPusher`
public var setPusherIdentifiersKindAppDisplayNameDeviceDisplayNameProfileTagLangThrowableError: Error?
@@ -18,11 +18,11 @@ import SwiftUI
struct EmojiPickerScreenCoordinatorParameters {
let emojiProvider: EmojiProviderProtocol
let itemId: String
let itemID: TimelineItemIdentifier
}
enum EmojiPickerScreenCoordinatorAction {
case emojiSelected(emoji: String, itemId: String)
case emojiSelected(emoji: String, itemID: TimelineItemIdentifier)
case dismiss
}
@@ -44,7 +44,7 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
switch action {
case let .emojiSelected(emoji: emoji):
self.callback?(.emojiSelected(emoji: emoji, itemId: self.parameters.itemId))
self.callback?(.emojiSelected(emoji: emoji, itemID: self.parameters.itemID))
case .dismiss:
self.callback?(.dismiss)
}
@@ -18,7 +18,7 @@ import Combine
import SwiftUI
struct ReportContentScreenCoordinatorParameters {
let itemID: String
let eventID: String
let senderID: String
let roomProxy: RoomProxyProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol?
@@ -39,7 +39,7 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol {
init(parameters: ReportContentScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = ReportContentScreenViewModel(itemID: parameters.itemID, senderID: parameters.senderID, roomProxy: parameters.roomProxy)
viewModel = ReportContentScreenViewModel(eventID: parameters.eventID, senderID: parameters.senderID, roomProxy: parameters.roomProxy)
}
// MARK: - Public
@@ -20,7 +20,7 @@ import SwiftUI
typealias ReportContentScreenViewModelType = StateStoreViewModel<ReportContentScreenViewState, ReportContentScreenViewAction>
class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportContentScreenViewModelProtocol {
private let itemID: String
private let eventID: String
private let senderID: String
private let roomProxy: RoomProxyProtocol
private let actionsSubject: PassthroughSubject<ReportContentScreenViewModelAction, Never> = .init()
@@ -29,8 +29,8 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont
actionsSubject.eraseToAnyPublisher()
}
init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) {
self.itemID = itemID
init(eventID: String, senderID: String, roomProxy: RoomProxyProtocol) {
self.eventID = eventID
self.senderID = senderID
self.roomProxy = roomProxy
@@ -53,7 +53,7 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont
private func submitReport() async {
actionsSubject.send(.submitStarted)
if case let .failure(error) = await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) {
if case let .failure(error) = await roomProxy.reportContent(eventID, reason: state.bindings.reasonText) {
MXLog.error("Submit Report Content failed: \(error)")
actionsSubject.send(.submitFailed(error: error))
return
@@ -84,7 +84,7 @@ struct ReportContentScreen: View {
// MARK: - Previews
struct ReportContentScreen_Previews: PreviewProvider {
static let viewModel = ReportContentScreenViewModel(itemID: "",
static let viewModel = ReportContentScreenViewModel(eventID: "",
senderID: "",
roomProxy: RoomProxyMock(with: .init(displayName: nil)))
@@ -25,15 +25,15 @@ struct RoomScreenCoordinatorParameters {
}
enum RoomScreenCoordinatorAction {
case presentReportContent(itemID: String, senderID: String)
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case presentMediaUploadPicker(MediaPickerScreenSource)
case presentMediaUploadPreviewScreen(URL)
case presentRoomDetails
case presentLocationPicker
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentEmojiPicker(itemID: String)
case presentEmojiPicker(itemID: TimelineItemIdentifier)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
case presentMessageForwarding(itemID: String)
case presentMessageForwarding(itemID: TimelineItemIdentifier)
}
final class RoomScreenCoordinator: CoordinatorProtocol {
@@ -22,22 +22,22 @@ import OrderedCollections
enum RoomScreenViewModelAction {
case displayRoomDetails
case displayEmojiPicker(itemID: String)
case displayReportContent(itemID: String, senderID: String)
case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
case displayCameraPicker
case displayMediaPicker
case displayDocumentPicker
case displayLocationPicker
case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case displayMessageForwarding(itemID: String)
case displayMessageForwarding(itemID: TimelineItemIdentifier)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
}
enum RoomScreenComposerMode: Equatable {
case `default`
case reply(itemID: String, replyDetails: TimelineItemReplyDetails)
case edit(originalItemId: String)
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails)
case edit(originalItemId: TimelineItemIdentifier)
var isEdit: Bool {
switch self {
@@ -52,21 +52,21 @@ enum RoomScreenComposerMode: Equatable {
enum RoomScreenViewAction {
case displayRoomDetails
case paginateBackwards
case itemAppeared(id: String)
case itemDisappeared(id: String)
case itemTapped(id: String)
case itemAppeared(itemID: TimelineItemIdentifier)
case itemDisappeared(itemID: TimelineItemIdentifier)
case itemTapped(itemID: TimelineItemIdentifier)
case linkClicked(url: URL)
case sendMessage
case toggleReaction(key: String, eventID: String)
case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case cancelReply
case cancelEdit
/// Mark the entire room as read - this is heavy handed as a starting point for now.
case markRoomAsRead
case timelineItemMenu(itemID: String)
case timelineItemMenuAction(itemID: String, action: TimelineItemMenuAction)
case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
case displayEmojiPicker(itemID: String)
case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayCameraPicker
case displayMediaPicker
@@ -76,10 +76,10 @@ enum RoomScreenViewAction {
case handlePasteOrDrop(provider: NSItemProvider)
case tappedOnUser(userID: String)
case reactionSummary(itemID: String, key: String)
case reactionSummary(itemID: TimelineItemIdentifier, key: String)
case retrySend(transactionID: String?)
case cancelSend(transactionID: String?)
case retrySend(itemID: TimelineItemIdentifier)
case cancelSend(itemID: TimelineItemIdentifier)
}
struct RoomScreenViewState: BindableState {
@@ -97,7 +97,7 @@ struct RoomScreenViewState: BindableState {
var bindings: RoomScreenViewStateBindings
var timelineItemMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)?
var timelineItemMenuActionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
var composerMode: RoomScreenComposerMode = .default
@@ -105,7 +105,7 @@ struct RoomScreenViewState: BindableState {
bindings.composerText.count == 0
}
var itemIDs: [String] {
var timelineIDs: [String] {
itemsDictionary.keys.elements
}
@@ -129,7 +129,7 @@ struct RoomScreenViewStateBindings {
/// The state of wether reactions listed on the timeline are expanded/collapsed.
/// Key is itemID, value is the collapsed state.
var reactionsCollapsed: [String: Bool]
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
/// A media item that will be previewed with QuickLook.
var mediaPreviewItem: MediaPreviewItem?
@@ -146,14 +146,14 @@ struct RoomScreenViewStateBindings {
var reactionSummaryInfo: ReactionSummaryInfo?
}
struct TimelineItemActionMenuInfo: Identifiable, Equatable {
struct TimelineItemActionMenuInfo: Equatable, Identifiable {
static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool {
lhs.id == rhs.id
}
let item: EventBasedTimelineItemProtocol
var id: String {
var id: TimelineItemIdentifier {
item.id
}
}
@@ -161,7 +161,7 @@ struct TimelineItemActionMenuInfo: Identifiable, Equatable {
struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol {
let title = L10n.screenRoomRetrySendMenuTitle
let transactionID: String?
let itemID: TimelineItemIdentifier
}
struct ReactionSummaryInfo: Identifiable {
@@ -120,14 +120,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .tappedOnUser(userID: let userID):
Task { await handleTappedUser(userID: userID) }
case .displayEmojiPicker(let itemID):
guard let item = state.itemsDictionary[itemID], item.isReactable else { return }
guard let item = state.itemsDictionary[itemID.timelineID], item.isReactable else { return }
callback?(.displayEmojiPicker(itemID: itemID))
case .reactionSummary(let itemId, let key):
showReactionSummary(for: itemId, selectedKey: key)
case .retrySend(let transactionID):
Task { await handleRetrySend(transactionID: transactionID) }
case .cancelSend(let transactionID):
Task { await handleCancelSend(transactionID: transactionID) }
case .reactionSummary(let itemID, let key):
showReactionSummary(for: itemID, selectedKey: key)
case .retrySend(let itemID):
Task { await handleRetrySend(itemID: itemID) }
case .cancelSend(let itemID):
Task { await handleCancelSend(itemID: itemID) }
}
}
@@ -221,9 +221,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
_ = await timelineController.markRoomAsRead()
}
private func itemTapped(with itemId: String) async {
private func itemTapped(with itemID: TimelineItemIdentifier) async {
state.showLoading = true
let action = await timelineController.processItemTap(itemId)
let action = await timelineController.processItemTap(itemID)
switch action {
case .displayMediaFile(let file, let title):
@@ -252,19 +252,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if itemGroup.count == 1 {
if let firstItem = itemGroup.first {
timelineItemsDictionary.updateValue(updateViewModel(item: firstItem, groupStyle: .single),
forKey: firstItem.id)
forKey: firstItem.id.timelineID)
}
} else {
for (index, item) in itemGroup.enumerated() {
if index == 0 {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .first),
forKey: item.id)
forKey: item.id.timelineID)
} else if index == itemGroup.count - 1 {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .last),
forKey: item.id)
forKey: item.id.timelineID)
} else {
timelineItemsDictionary.updateValue(updateViewModel(item: item, groupStyle: .middle),
forKey: item.id)
forKey: item.id.timelineID)
}
}
}
@@ -274,7 +274,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
private func updateViewModel(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewModel {
if let timelineItemViewModel = state.itemsDictionary[item.id] {
if let timelineItemViewModel = state.itemsDictionary[item.id.timelineID] {
timelineItemViewModel.groupStyle = groupStyle
timelineItemViewModel.type = .init(item: item)
return timelineItemViewModel
@@ -360,7 +360,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: TimelineItemActionMenu
private func showTimelineItemActionMenu(for itemID: String) {
private func showTimelineItemActionMenu(for itemID: TimelineItemIdentifier) {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
// Don't show a menu for non-event based items.
@@ -370,8 +370,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.actionMenuInfo = .init(item: eventTimelineItem)
}
private func timelineItemMenuActionsForItemId(_ itemId: String) -> TimelineItemMenuActions? {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
private func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let item = timelineItem as? EventBasedTimelineItemProtocol else {
// Don't show a context menu for non-event based items.
return nil
@@ -395,7 +395,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
]
if item.isMessage {
actions.append(.forward(itemID: itemId))
actions.append(.forward(itemID: itemID))
}
if item.isEditable {
@@ -426,7 +426,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
private func processTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: String) {
private func processTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return
@@ -449,7 +449,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
setComposerMode(.edit(originalItemId: messageTimelineItem.id))
case .copyPermalink:
do {
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventTimelineItem.id, roomIdentifier: timelineController.roomID,
guard let eventID = eventTimelineItem.id.eventID else {
displayError(.alert(L10n.errorFailedCreatingThePermalink))
break
}
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventID, roomIdentifier: timelineController.roomID,
baseURL: appSettings.permalinkBaseURL)
UIPasteboard.general.url = permalink
} catch {
@@ -457,9 +462,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
case .redact:
Task {
if eventTimelineItem.hasFailedToSend,
let transactionID = eventTimelineItem.properties.transactionID {
await timelineController.cancelSend(transactionID)
if eventTimelineItem.hasFailedToSend {
await timelineController.cancelSend(itemID)
} else {
await timelineController.redact(itemID)
}
@@ -567,16 +571,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
private func handleRetrySend(transactionID: String?) async {
guard let transactionID else {
private func handleRetrySend(itemID: TimelineItemIdentifier) async {
guard let transactionID = itemID.transactionID else {
MXLog.error("Failed Retry Send: missing transaction ID")
return
}
await roomProxy.retrySend(transactionID: transactionID)
}
private func handleCancelSend(transactionID: String?) async {
guard let transactionID else {
private func handleCancelSend(itemID: TimelineItemIdentifier) async {
guard let transactionID = itemID.transactionID else {
MXLog.error("Failed Cancel Send: missing transaction ID")
return
}
@@ -643,7 +649,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Reaction summary
private func showReactionSummary(for itemID: String, selectedKey: String) {
private func showReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return
@@ -664,7 +670,7 @@ extension RoomScreenViewModel.Context {
/// A function to make it easier to bind to reactions expand/collapsed state
/// - Parameter itemID: The id of the timeline item the reacted to
/// - Returns: Wether the reactions should show in the collapsed state, true by default.
func reactionsCollapsedBinding(for itemID: String) -> Binding<Bool> {
func reactionsCollapsedBinding(for itemID: TimelineItemIdentifier) -> Binding<Bool> {
Binding(get: {
self.reactionsCollapsed[itemID] ?? true
}, set: {
@@ -173,7 +173,7 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle {
struct MessageComposer_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static var previews: some View {
VStack {
MessageComposer(text: .constant(""),
@@ -184,7 +184,7 @@ struct MessageComposer_Previews: PreviewProvider {
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("This is a short message."),
focused: .constant(false),
sendingDisabled: false,
@@ -193,7 +193,7 @@ struct MessageComposer_Previews: PreviewProvider {
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("This is a very long message that will wrap to 2 lines on an iPhone 14."),
focused: .constant(false),
sendingDisabled: false,
@@ -202,7 +202,7 @@ struct MessageComposer_Previews: PreviewProvider {
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("This is an even longer message that will wrap to 3 lines on an iPhone 14, just to see the difference it makes."),
focused: .constant(false),
sendingDisabled: false,
@@ -211,20 +211,20 @@ struct MessageComposer_Previews: PreviewProvider {
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("Some message"),
focused: .constant(false),
sendingDisabled: false,
mode: .edit(originalItemId: UUID().uuidString),
mode: .edit(originalItemId: .init(timelineID: UUID().uuidString)),
sendAction: { },
pasteAction: { _ in },
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant(""),
focused: .constant(false),
sendingDisabled: false,
mode: .reply(itemID: UUID().uuidString,
mode: .reply(itemID: .init(timelineID: UUID().uuidString),
replyDetails: .loaded(sender: .init(id: "Kirk"),
contentType: .text(.init(body: "Text: Where the wild things are")))),
sendAction: { },
@@ -233,7 +233,7 @@ struct MessageComposer_Previews: PreviewProvider {
editCancellationAction: { })
}
.padding(.horizontal)
ScrollView {
VStack {
let replyTypes: [TimelineItemReplyDetails] = [
@@ -251,12 +251,12 @@ struct MessageComposer_Previews: PreviewProvider {
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))),
.loading(eventID: "")
]
ForEach(replyTypes, id: \.self) { replyDetails in
MessageComposer(text: .constant(""),
focused: .constant(false),
sendingDisabled: false,
mode: .reply(itemID: UUID().uuidString,
mode: .reply(itemID: .init(timelineID: UUID().uuidString),
replyDetails: replyDetails),
sendAction: { },
pasteAction: { _ in },
@@ -71,12 +71,12 @@ struct RoomScreen: View {
context.send(viewAction: .handlePasteOrDrop(provider: provider))
return true
}
.confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { item in
.confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { info in
Button(L10n.screenRoomRetrySendMenuSendAgainAction) {
context.send(viewAction: .retrySend(transactionID: item.transactionID))
context.send(viewAction: .retrySend(itemID: info.itemID))
}
Button(L10n.screenRoomRetrySendMenuRemoveAction, role: .destructive) {
context.send(viewAction: .cancelSend(transactionID: item.transactionID))
context.send(viewAction: .cancelSend(itemID: info.itemID))
}
}
}
@@ -125,7 +125,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
}
.onTapGesture {
context.send(viewAction: .itemTapped(id: timelineItem.id))
context.send(viewAction: .itemTapped(itemID: timelineItem.id))
}
// We need a tap gesture before this long one so that it doesn't
// steal away the gestures from the scroll view
@@ -178,7 +178,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
if timelineItem.hasFailedToSend {
backgroundedLocalizedSendInfo
.onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID)
context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id)
}
} else {
backgroundedLocalizedSendInfo
@@ -335,7 +335,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
static var replies: some View {
VStack {
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "",
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -344,7 +344,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: "",
RoomTimelineItemView(viewModel: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -67,7 +67,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
context.send(viewAction: .displayEmojiPicker(itemID: timelineItem.id))
}
.onTapGesture {
context.send(viewAction: .itemTapped(id: timelineItem.id))
context.send(viewAction: .itemTapped(itemID: timelineItem.id))
}
// We need a tap gesture before this long one so that it doesn't
// steal away the gestures from the scroll view
@@ -38,7 +38,7 @@ struct TimelineStyler<Content: View>: View {
struct TimelineItemStyler_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static let base = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
static let base = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
static let sentNonLast: TextRoomTimelineItem = {
var result = base
@@ -53,8 +53,8 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}()
static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.itemIDs.last ?? UUID().uuidString
var result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
let id = viewModel.state.timelineIDs.last ?? UUID().uuidString
var result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
result.properties.deliveryStatus = .sending
return result
}()
@@ -66,22 +66,22 @@ struct TimelineItemStyler_Previews: PreviewProvider {
}()
static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.itemIDs.last ?? UUID().uuidString
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
let id = viewModel.state.timelineIDs.last ?? UUID().uuidString
let result = TextRoomTimelineItem(id: .init(timelineID: id), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
return result
}()
static let ltrString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!"))
static let ltrString = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!"))
static let rtlString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!"))
static let rtlString = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!"))
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!"))
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!"))
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!"))
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!"))
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!"))
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString), timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!"))
static var testView: some View {
VStack {
@@ -23,7 +23,7 @@ struct TimelineItemStatusView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
private var isLastOutgoingMessage: Bool {
context.viewState.itemIDs.last == timelineItem.id &&
context.viewState.timelineIDs.last == timelineItem.id.timelineID &&
timelineItem.isOutgoing
}
@@ -53,7 +53,7 @@ struct TimelineItemStatusView: View {
.foregroundColor(.compound.iconCriticalPrimary)
.frame(width: 16, height: 16)
.onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID)
context.sendFailedConfirmationDialogInfo = .init(itemID: timelineItem.id)
}
}
}
@@ -25,7 +25,7 @@ struct TimelineReactionsView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
let itemID: String
let itemID: TimelineItemIdentifier
let reactions: [AggregatedReaction]
@Binding var collapsed: Bool
@@ -33,7 +33,7 @@ struct TimelineReactionsView: View {
CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
ForEach(reactions, id: \.self) { reaction in
TimelineReactionButton(itemID: itemID, reaction: reaction) { key in
context.send(viewAction: .toggleReaction(key: key, eventID: itemID))
context.send(viewAction: .toggleReaction(key: key, itemID: itemID))
} showReactionSummary: { key in
context.send(viewAction: .reactionSummary(itemID: itemID, key: key))
}
@@ -93,7 +93,7 @@ struct TimelineCollapseButtonLabel: View {
}
struct TimelineReactionButton: View {
let itemID: String
let itemID: TimelineItemIdentifier
let reaction: AggregatedReaction
let toggleReaction: (String) -> Void
let showReactionSummary: (String) -> Void
@@ -131,11 +131,11 @@ struct TimelineReactionViewPreviewsContainer: View {
var body: some View {
VStack {
TimelineReactionsView(itemID: "1", reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true))
TimelineReactionsView(itemID: .init(timelineID: "1"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true))
Divider()
TimelineReactionsView(itemID: "2", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1)
TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1)
Divider()
TimelineReactionsView(itemID: "3", reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2)
TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2)
.environment(\.layoutDirection, .rightToLeft)
}
.background(Color.red)
@@ -78,7 +78,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider {
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: true,
isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
@@ -44,7 +44,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider {
}
static var body: some View {
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: UUID().uuidString,
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -71,8 +71,8 @@ private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupS
struct CollapsibleRoomTimelineView_Previews: PreviewProvider {
static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: "First separator", text: "This is a separator"),
SeparatorRoomTimelineItem(id: "Second separator", text: "This is another separator")
SeparatorRoomTimelineItem(id: .init(timelineID: "First separator"), text: "This is a separator"),
SeparatorRoomTimelineItem(id: .init(timelineID: "Second separator"), text: "This is another separator")
])
static var previews: some View {
@@ -55,7 +55,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
EmoteRoomTimelineItem(id: UUID().uuidString,
EmoteRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: timestamp,
isOutgoing: false,
isEditable: false,
@@ -55,7 +55,7 @@ private struct EncryptedHistoryLabelStyle: LabelStyle {
struct EncryptedHistoryRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
let item = EncryptedHistoryRoomTimelineItem(id: UUID().uuidString)
let item = EncryptedHistoryRoomTimelineItem(id: .init(timelineID: UUID().uuidString))
EncryptedHistoryRoomTimelineView(timelineItem: item)
}
}
@@ -67,7 +67,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
EncryptedRoomTimelineItem(id: UUID().uuidString,
EncryptedRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: text,
encryptionType: .unknown,
timestamp: timestamp,
@@ -45,21 +45,21 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "document.pdf", source: nil, thumbnailSource: nil, contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "document.docx", source: nil, thumbnailSource: nil, contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -66,21 +66,21 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "Some other image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -91,14 +91,14 @@ struct LocationRoomTimelineView_Previews: PreviewProvider {
@ViewBuilder
static var body: some View {
LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString,
LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description")))
LocationRoomTimelineView(timelineItem: .init(id: UUID().uuidString,
LocationRoomTimelineView(timelineItem: .init(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -66,7 +66,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
NoticeRoomTimelineItem(id: UUID().uuidString,
NoticeRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: timestamp,
isOutgoing: false,
isEditable: false,
@@ -41,8 +41,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
static let item = ReadMarkerRoomTimelineItem()
static var previews: some View {
VStack(alignment: .leading, spacing: 0) {
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "",
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""),
timestamp: "",
isOutgoing: true,
isEditable: false,
@@ -51,8 +51,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: "Separator", text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: "",
RoomTimelineItemView(viewModel: .init(type: .separator(.init(id: .init(timelineID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewModel: .init(type: .text(.init(id: .init(timelineID: ""),
timestamp: "",
isOutgoing: false,
isEditable: false,
@@ -42,7 +42,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
RedactedRoomTimelineItem(id: UUID().uuidString,
RedactedRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: text,
timestamp: timestamp,
isOutgoing: false,
@@ -31,7 +31,7 @@ struct SeparatorRoomTimelineView: View {
struct SeparatorRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
let item = SeparatorRoomTimelineItem(id: "Separator", text: "This is a separator")
let item = SeparatorRoomTimelineItem(id: .init(timelineID: "Separator"), text: "This is a separator")
SeparatorRoomTimelineView(timelineItem: item)
}
}
@@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
StateRoomTimelineView(timelineItem: item)
}
static let item = StateRoomTimelineItem(id: UUID().uuidString,
static let item = StateRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Alice joined",
timestamp: "Now",
isOutgoing: false,
@@ -58,7 +58,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Some image",
timestamp: "Now",
isOutgoing: false,
@@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Some other image",
timestamp: "Now",
isOutgoing: false,
@@ -74,7 +74,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: "Blurhashed image",
timestamp: "Now",
isOutgoing: false,
@@ -71,7 +71,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: timestamp,
isOutgoing: isOutgoing,
isEditable: isOutgoing,
@@ -63,7 +63,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
}
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
UnsupportedRoomTimelineItem(id: UUID().uuidString,
UnsupportedRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
body: text,
eventType: "Some Event Type",
error: "Something went wrong",
@@ -77,21 +77,21 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "Some video", duration: 21, source: nil, thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
sender: .init(id: "Bob"),
content: .init(body: "Some other video", duration: 22, source: nil, thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -46,7 +46,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
case copyPermalink
case redact
case reply
case forward(itemID: String)
case forward(itemID: TimelineItemIdentifier)
case viewSource
case retryDecryption(sessionID: String)
case report
@@ -188,7 +188,7 @@ public struct TimelineItemMenu: View {
private func reactionButton(for emoji: String) -> some View {
Button {
presentationMode.wrappedValue.dismiss()
context.send(viewAction: .toggleReaction(key: emoji, eventID: item.id))
context.send(viewAction: .toggleReaction(key: emoji, itemID: item.id))
} label: {
Text(emoji)
.padding(8.0)
@@ -72,7 +72,7 @@ class TimelineTableViewController: UIViewController {
}
}
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemMenuActions?)?
var contextMenuActionProvider: (@MainActor (_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions?)?
@Binding private var scrollToBottomButtonVisible: Bool
@@ -214,10 +214,10 @@ class TimelineTableViewController: UIViewController {
.frame(maxWidth: .infinity, alignment: .leading)
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
.onAppear {
coordinator.send(viewAction: .itemAppeared(id: id))
coordinator.send(viewAction: .itemAppeared(itemID: viewModel.id))
}
.onDisappear {
coordinator.send(viewAction: .itemDisappeared(id: id))
coordinator.send(viewAction: .itemDisappeared(itemID: viewModel.id))
}
.environment(\.openURL, OpenURLAction { url in
coordinator.send(viewAction: .linkClicked(url: url))
@@ -231,7 +231,7 @@ class TimelineTableViewController: UIViewController {
return cell
}
dataSource?.defaultRowAnimation = .fade
// dataSource?.defaultRowAnimation = .automatic
tableView.delegate = self
}
@@ -248,6 +248,8 @@ class TimelineTableViewController: UIViewController {
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>()
snapshot.appendSections([.main])
snapshot.appendItems(timelineItemsIDs)
MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: dataSource.snapshot().itemIdentifiers))")
dataSource.apply(snapshot, animatingDifferences: false)
// Probably redundant now we observe content size changes…
@@ -421,7 +423,7 @@ extension TimelineTableViewController {
private extension UITableView {
/// Returns the frame of the cell for a particular timeline item.
func cellFrame(for id: String) -> CGRect? {
guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item?.id == id }) else {
guard let timelineCell = visibleCells.last(where: { ($0 as? TimelineItemCell)?.item?.id.timelineID == id }) else {
return nil
}
@@ -31,8 +31,8 @@ class ClientProxy: ClientProxyProtocol {
private var roomListService: RoomListService?
private var roomListStateUpdateTaskHandle: TaskHandle?
private var encryptionSyncService: EncryptionSync?
private var isEncryptionSyncing = false
private var appService: App?
private var appServiceUpdateTaskHandle: TaskHandle?
var roomSummaryProvider: RoomSummaryProviderProtocol?
var inviteSummaryProvider: RoomSummaryProviderProtocol?
@@ -53,7 +53,7 @@ class ClientProxy: ClientProxyProtocol {
deinit {
client.setDelegate(delegate: nil)
stopSync()
pauseSync()
}
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
@@ -73,7 +73,7 @@ class ClientProxy: ClientProxyProtocol {
self?.callbacks.send(.updateRestorationToken)
})
await configureRoomListService()
await configureAppService()
loadUserAvatarURLFromCache()
}
@@ -110,13 +110,7 @@ class ClientProxy: ClientProxyProtocol {
}
var isSyncing: Bool {
let isRoomListServiceSyncing = roomListService?.isSyncing() ?? false
if ServiceLocator.shared.settings.isEncryptionSyncEnabled {
return isRoomListServiceSyncing && isEncryptionSyncing
} else {
return isRoomListServiceSyncing
}
roomListService?.isSyncing() ?? false
}
func startSync() {
@@ -125,29 +119,22 @@ class ClientProxy: ClientProxyProtocol {
return
}
startEncryptionSyncService()
roomListService?.sync()
do {
try appService?.start()
} catch {
MXLog.error("Failed starting app service with error: \(error)")
}
}
func stopSync() {
func pauseSync() {
MXLog.info("Stopping sync")
stopEncryptionSyncService()
do {
try roomListService?.stopSync()
try appService?.pause()
} catch {
MXLog.error("Failed stopping room list service with error: \(error)")
MXLog.error("Failed pausing app service with error: \(error)")
}
}
private func stopEncryptionSyncService() {
guard isEncryptionSyncing else {
return
}
isEncryptionSyncing = false
encryptionSyncService?.stop()
MXLog.info("Stopping Encryption Sync service")
}
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> {
await Task.dispatch(on: clientQueue) {
@@ -366,7 +353,7 @@ class ClientProxy: ClientProxyProtocol {
// MARK: Private
private func restartSync() {
stopSync()
pauseSync()
startSync()
}
@@ -385,89 +372,82 @@ class ClientProxy: ClientProxyProtocol {
}
}
private func startEncryptionSyncService() {
guard appSettings.isEncryptionSyncEnabled else {
return
}
configureEncryptionSyncService()
}
private func configureEncryptionSyncService() {
do {
let listener = EncryptionSyncListenerProxy { [weak self] reason in
switch reason {
case .done:
MXLog.info("Encryption Sync has finished for user: \(self?.userID ?? "unknown")")
case .error(let msg):
MXLog.error("Encryption Sync has terminated for user: \(self?.userID ?? "unknown") for reason: \(msg)")
guard let self else {
return
}
Task {
self.configureEncryptionSyncService()
}
}
}
let encryptionSync = try client.mainEncryptionSync(id: "Main App", listener: listener)
isEncryptionSyncing = true
encryptionSyncService = encryptionSync
MXLog.info("Encryption sync started for user: \(userID)")
} catch {
MXLog.error("Configure encryption sync failed with error: \(error)")
}
}
private func configureRoomListService() async {
guard roomListService == nil else {
private func configureAppService() async {
guard appService == nil else {
fatalError("This shouldn't be called more than once")
}
do {
let roomListService = try appSettings.isEncryptionSyncEnabled ? client.roomListService() : client.roomListServiceWithEncryption()
roomListStateUpdateTaskHandle = roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in
guard let self else { return }
MXLog.info("Received room list update: \(state)")
// Restart the room list sync on every error for now
if state == .error {
self.restartSync()
}
// The invites are available only when entering `running`
if state == .running {
Task {
do {
// Subscribe to invites later as the underlying SlidingSync list is only added when entering AllRooms
try await self.inviteSummaryProvider?.setRoomList(roomListService.invites())
} catch {
MXLog.error("Failed configuring invites room list with error: \(error)")
}
}
}
// Anything that's not `running` is interpreted as "Loading data"
if state == .running {
self.callbacks.send(.receivedSyncUpdate)
} else {
self.callbacks.send(.startedUpdating)
}
})
let appService = try client
.app()
.withEncryptionSync(withCrossProcessLock: appSettings.isEncryptionSyncEnabled,
appIdentifier: "MainApp")
.finish()
let roomListService = appService.roomListService()
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)),
eventStringBuilder: eventStringBuilder,
name: "AllRooms")
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)),
eventStringBuilder: eventStringBuilder,
name: "Invites")
self.appService = appService
self.roomListService = roomListService
appServiceUpdateTaskHandle = createAppServiceObserver(appService)
roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
} catch {
MXLog.error("Failed building room list service with error: \(error)")
}
}
private func createAppServiceObserver(_ appService: App) -> TaskHandle {
appService.state(listener: AppStateObserverProxy { [weak self] state in
guard let self else { return }
MXLog.info("Received app service update: \(state)")
switch state {
case .error:
restartSync()
case .terminated, .running:
break
}
})
}
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in
MXLog.info("Received room list update: \(state)")
guard let self,
state != .error,
state != .terminated else {
// The app service is responsible of handling error and termination
return
}
// The invites are available only when entering `running`
if state == .running {
Task {
do {
guard let roomListService = self.roomListService else {
MXLog.error("Room list service is not configured")
return
}
// Subscribe to invites later as the underlying SlidingSync list is only added when entering AllRooms
try await self.inviteSummaryProvider?.setRoomList(roomListService.invites())
} catch {
MXLog.error("Failed configuring invites room list with error: \(error)")
}
}
callbacks.send(.receivedSyncUpdate)
} else {
callbacks.send(.startedUpdating)
}
})
}
private func roomTupleForIdentifier(_ identifier: String) -> (RoomListItem?, Room?) {
do {
@@ -496,13 +476,25 @@ extension ClientProxy: MediaLoaderProtocol {
}
}
private class RoomListStateListenerProxy: RoomListServiceStateListener {
private let onUpdateClosure: (RoomListServiceState) -> Void
init(_ onUpdateClosure: @escaping (RoomListServiceState) -> Void) {
private class AppStateObserverProxy: AppStateObserver {
private let onUpdateClosure: (AppState) -> Void
init(onUpdateClosure: @escaping (AppState) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(state: AppState) {
onUpdateClosure(state)
}
}
private class RoomListStateListenerProxy: RoomListServiceStateListener {
private let onUpdateClosure: (RoomListServiceState) -> Void
init(onUpdateClosure: @escaping (RoomListServiceState) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func onUpdate(state: RoomListServiceState) {
onUpdateClosure(state)
}
@@ -84,7 +84,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func startSync()
func stopSync()
func pauseSync()
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError>
@@ -45,7 +45,7 @@ class MockClientProxy: ClientProxyProtocol {
func stopSync(completionHandler: () -> Void) { }
func stopSync() { }
func pauseSync() { }
func directRoomForUserID(_ userID: String) async -> Result<String?, ClientProxyError> {
.failure(.failedRetrievingDirectRoom)
@@ -1,31 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixRustSDK
final class EncryptionSyncListenerProxy: EncryptionSyncListener {
private let didTerminateClosure: (EncryptionSyncTerminationReason) -> Void
init(_ didTerminateClosure: @escaping (EncryptionSyncTerminationReason) -> Void) {
self.didTerminateClosure = didTerminateClosure
}
func didTerminate(reason: EncryptionSyncTerminationReason) {
didTerminateClosure(reason)
}
}
@@ -67,15 +67,12 @@ extension NotificationItemProxyProtocol {
struct NotificationItemProxy: NotificationItemProxyProtocol {
let notificationItem: NotificationItem
let receiverID: String
let roomID: String
var event: TimelineEventProxyProtocol {
TimelineEventProxy(timelineEvent: notificationItem.event)
}
var roomID: String {
notificationItem.roomInfo.id
}
var senderDisplayName: String? {
notificationItem.senderInfo.displayName
}
@@ -140,7 +140,23 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
MXLog.verbose("\(name): Finished applying \(diffs.count) diffs, new room list \(rooms.compactMap { $0.id ?? "Empty" })")
}
private func fetchLastMessage(roomListItem: RoomListItemProtocol) -> EventTimelineItem? {
class FetchResult {
var latestRoomEvent: EventTimelineItem?
}
let semaphore = DispatchSemaphore(value: 0)
let result = FetchResult()
Task {
result.latestRoomEvent = await roomListItem.latestEvent()
semaphore.signal()
}
semaphore.wait()
return result.latestRoomEvent
}
private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary {
guard let roomListItem = try? roomListService.room(roomId: identifier) else {
MXLog.error("\(name): Failed finding room with id: \(identifier)")
@@ -149,9 +165,9 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
var attributedLastMessage: AttributedString?
var lastMessageFormattedTimestamp: String?
if let latestRoomMessage = roomListItem.latestEvent() {
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage)
if let latestRoomMessage = fetchLastMessage(roomListItem: roomListItem) {
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: 0)
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
}
@@ -19,15 +19,15 @@ import Foundation
enum RoomTimelineItemFixtures {
/// The default timeline items used in Xcode previews etc.
static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(id: "Yesterday", text: "Yesterday"),
TextRoomTimelineItem(id: UUID().uuidString,
SeparatorRoomTimelineItem(id: .init(timelineID: "Yesterday"), text: "Yesterday"),
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:10 AM",
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:11 AM",
isOutgoing: false,
isEditable: false,
@@ -36,7 +36,7 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me"])
])),
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "10:11 AM",
isOutgoing: false,
isEditable: false,
@@ -46,21 +46,21 @@ enum RoomTimelineItemFixtures {
AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]),
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob"])
])),
SeparatorRoomTimelineItem(id: "Today", text: "Today"),
TextRoomTimelineItem(id: UUID().uuidString,
SeparatorRoomTimelineItem(id: .init(timelineID: "Today"), text: "Today"),
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM",
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM",
isOutgoing: true,
isEditable: true,
sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM",
isOutgoing: true,
isEditable: true,
@@ -71,7 +71,7 @@ enum RoomTimelineItemFixtures {
ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString,
TextRoomTimelineItem(id: .init(timelineID: UUID().uuidString),
timestamp: "5 PM",
isOutgoing: false,
isEditable: false,
@@ -210,7 +210,7 @@ enum RoomTimelineItemFixtures {
private extension TextRoomTimelineItem {
init(text: String, senderDisplayName: String) {
self.init(id: UUID().uuidString,
self.init(id: .init(timelineID: UUID().uuidString),
timestamp: "10:47 am",
isOutgoing: senderDisplayName == "Alice",
isEditable: false,
@@ -169,37 +169,47 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
}
private extension TimelineItem {
var debugIdentifier: String {
var debugIdentifier: DebugIdentifier {
if let virtualTimelineItem = asVirtual() {
return virtualTimelineItem.debugIdentifier
} else if let eventTimelineItem = asEvent() {
return eventTimelineItem.uniqueIdentifier()
return .event(timelineID: String(uniqueId()),
eventID: eventTimelineItem.eventId(),
transactionID: eventTimelineItem.transactionId())
}
return "UnknownTimelineItem"
return .unknown
}
}
private extension TimelineItemProxy {
var debugIdentifier: String {
var debugIdentifier: DebugIdentifier {
switch self {
case .event(let eventTimelineItem):
return eventTimelineItem.item.uniqueIdentifier()
return .event(timelineID: eventTimelineItem.id.timelineID,
eventID: eventTimelineItem.id.eventID,
transactionID: eventTimelineItem.id.transactionID)
case .virtual(let virtualTimelineItem):
return virtualTimelineItem.debugIdentifier
case .unknown:
return "UnknownTimelineItem"
return .unknown
}
}
}
private extension VirtualTimelineItem {
var debugIdentifier: String {
var debugIdentifier: DebugIdentifier {
switch self {
case .dayDivider(let timestamp):
return "DayDiviver(\(timestamp))"
return .virtual("DayDiviver(\(timestamp))")
case .readMarker:
return "ReadMarker"
return .virtual("ReadMarker")
}
}
}
enum DebugIdentifier {
case event(timelineID: String?, eventID: String?, transactionID: String?)
case virtual(String)
case unknown
}
@@ -51,23 +51,23 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> { .success(()) }
func processItemAppearance(_ itemID: String) async { }
func processItemAppearance(_ itemID: TimelineItemIdentifier) async { }
func processItemDisappearance(_ itemID: String) async { }
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async { }
func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction { .none }
func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { .none }
func sendMessage(_ message: String, inReplyTo itemID: String?) async { }
func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async { }
func toggleReaction(_ reaction: String, to itemID: String) async { }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { }
func editMessage(_ newMessage: String, original itemID: String) async { }
func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async { }
func redact(_ itemID: String) async { }
func redact(_ itemID: TimelineItemIdentifier) async { }
func cancelSend(_ transactionID: String) async { }
func cancelSend(_ itemID: TimelineItemIdentifier) async { }
func debugInfo(for itemID: String) -> TimelineItemDebugInfo {
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo {
.init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil)
}
@@ -84,7 +84,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> {
guard roomProxy.hasUnreadNotifications,
let eventID = timelineItems.last?.id
let eventID = timelineItems.last?.id.eventID
else { return .success(()) }
switch await roomProxy.sendReadReceipt(for: eventID) {
@@ -95,7 +95,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func processItemAppearance(_ itemID: String) async {
func processItemAppearance(_ itemID: TimelineItemIdentifier) async {
guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return
}
@@ -105,9 +105,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func processItemDisappearance(_ itemID: String) { }
func processItemDisappearance(_ itemID: TimelineItemIdentifier) { }
func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction {
func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction {
guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return .none
}
@@ -121,14 +121,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func sendMessage(_ message: String, inReplyTo itemID: String?) async {
func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async {
var inReplyTo: String?
if itemID == nil {
MXLog.info("Send message in \(roomID)")
} else {
} else if let eventID = itemID?.eventID {
inReplyTo = eventID
MXLog.info("Send reply in \(roomID)")
} else {
MXLog.error("Send reply in \(roomID) failed: missing event ID")
return
}
switch await roomProxy.sendMessage(message, inReplyTo: itemID) {
switch await roomProxy.sendMessage(message, inReplyTo: inReplyTo) {
case .success:
MXLog.info("Finished sending message")
case .failure(let error):
@@ -136,9 +141,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func toggleReaction(_ reaction: String, to itemID: String) async {
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async {
MXLog.info("Toggle reaction in \(roomID)")
switch await roomProxy.toggleReaction(reaction, to: itemID) {
guard let eventID = itemID.eventID else {
MXLog.error("Failed toggling reaction: missing eventID")
return
}
switch await roomProxy.toggleReaction(reaction, to: eventID) {
case .success:
MXLog.info("Finished toggling reaction")
case .failure(let error):
@@ -146,28 +156,32 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func editMessage(_ newMessage: String, original itemID: String) async {
func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async {
MXLog.info("Edit message in \(roomID)")
if let timelineItem = timelineItems.first(where: { $0.id == itemID }),
let item = timelineItem as? EventBasedTimelineItemProtocol,
item.hasFailedToSend,
let transactionID = item.properties.transactionID {
item.hasFailedToSend {
MXLog.info("Editing a failed echo, will cancel and resend it as a new message")
await cancelSend(transactionID)
await cancelSend(itemID)
await sendMessage(newMessage)
} else {
switch await roomProxy.editMessage(newMessage, original: itemID) {
} else if let eventID = itemID.eventID {
switch await roomProxy.editMessage(newMessage, original: eventID) {
case .success:
MXLog.info("Finished editing message")
case .failure(let error):
MXLog.error("Failed editing message with error: \(error)")
}
} else {
MXLog.error("Editing failed: missing identifiers")
}
}
func redact(_ itemID: String) async {
func redact(_ itemID: TimelineItemIdentifier) async {
MXLog.info("Send redaction in \(roomID)")
switch await roomProxy.redact(itemID) {
guard let eventID = itemID.eventID else {
return
}
switch await roomProxy.redact(eventID) {
case .success:
MXLog.info("Finished redacting message")
case .failure(let error):
@@ -175,14 +189,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func cancelSend(_ transactionID: String) async {
func cancelSend(_ itemID: TimelineItemIdentifier) async {
guard let transactionID = itemID.transactionID else {
MXLog.error("Failed cancelling send, missing transaction ID")
return
}
MXLog.info("Cancelling send in \(roomID)")
await roomProxy.cancelSend(transactionID: transactionID)
}
// Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them
func debugInfo(for itemID: String) -> TimelineItemDebugInfo {
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo {
for timelineItemProxy in timelineProvider.itemProxies {
switch timelineItemProxy {
case .event(let item):
@@ -339,7 +357,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Separators without stable identifiers cause UI glitches
let identifier = "\(chunkIndex)-\(dateString)"
return SeparatorRoomTimelineItem(id: identifier, text: dateString)
return SeparatorRoomTimelineItem(id: .init(timelineID: identifier), text: dateString)
case .readMarker:
return ReadMarkerRoomTimelineItem()
}
@@ -373,12 +391,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) {
guard let eventID = timelineItem.id.eventID else {
return
}
switch timelineItem.replyDetails {
case .notLoaded:
roomProxy.fetchDetails(for: timelineItem.id)
roomProxy.fetchDetails(for: eventID)
case .error:
if refetchOnError {
roomProxy.fetchDetails(for: timelineItem.id)
roomProxy.fetchDetails(for: eventID)
}
default:
break
@@ -41,27 +41,27 @@ protocol RoomTimelineControllerProtocol {
var timelineItems: [RoomTimelineItemProtocol] { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
func processItemAppearance(_ itemID: String) async
func processItemAppearance(_ itemID: TimelineItemIdentifier) async
func processItemDisappearance(_ itemID: String) async
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async
func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction
func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError>
func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError>
func sendMessage(_ message: String, inReplyTo itemID: String?) async
func sendMessage(_ message: String, inReplyTo itemID: TimelineItemIdentifier?) async
func editMessage(_ newMessage: String, original itemID: String) async
func editMessage(_ newMessage: String, original itemID: TimelineItemIdentifier) async
func toggleReaction(_ reaction: String, to itemID: String) async
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async
func redact(_ itemID: String) async
func redact(_ itemID: TimelineItemIdentifier) async
func cancelSend(_ transactionID: String) async
func cancelSend(_ itemID: TimelineItemIdentifier) async
func debugInfo(for itemID: String) -> TimelineItemDebugInfo
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo
func retryDecryption(for sessionID: String) async
}
@@ -26,6 +26,4 @@ struct RoomTimelineItemProperties: Hashable {
var deliveryStatus: TimelineItemDeliveryStatus?
/// The read receipts of the item, ordered from newest to oldest
var orderedReadReceipts: [ReadReceipt] = []
/// The original transaction id transmitted by the client
var transactionID: String?
}
@@ -17,6 +17,20 @@
import Foundation
import MatrixRustSDK
struct TimelineItemIdentifier: Hashable {
/// Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline.
/// It's value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
let timelineID: String
/// Uniquely identifies the timeline item from the server side.
/// Only available for EventTimelineItem and only when the item is returned by the server.
var eventID: String?
/// Uniquely identfies the local echo of the timeline item.
/// Only available for sent EventTimelineItem that have not been returned by the server yet.
var transactionID: String?
}
/// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy {
case event(EventTimelineItemProxy)
@@ -25,7 +39,7 @@ enum TimelineItemProxy {
init(item: MatrixRustSDK.TimelineItem) {
if let eventItem = item.asEvent() {
self = .event(EventTimelineItemProxy(item: eventItem))
self = .event(EventTimelineItemProxy(item: eventItem, id: item.uniqueId()))
} else if let virtualItem = item.asVirtual() {
self = .virtual(virtualItem)
} else {
@@ -44,17 +58,11 @@ enum TimelineItemDeliveryStatus: Hashable {
/// A light wrapper around event timeline items returned from Rust.
struct EventTimelineItemProxy {
let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier
init(item: MatrixRustSDK.EventTimelineItem) {
init(item: MatrixRustSDK.EventTimelineItem, id: UInt64) {
self.item = item
}
var id: String {
item.uniqueIdentifier()
}
var transactionID: String? {
item.transactionId()
self.id = TimelineItemIdentifier(timelineID: String(id), eventID: item.eventId(), transactionID: item.transactionId())
}
var deliveryStatus: TimelineItemDeliveryStatus? {
@@ -16,8 +16,8 @@
import Foundation
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -16,8 +16,8 @@
import UIKit
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -17,8 +17,8 @@
import UIKit
import UniformTypeIdentifiers
struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -17,8 +17,8 @@
import UIKit
import UniformTypeIdentifiers
struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -14,8 +14,8 @@
// limitations under the License.
//
struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Hashable {
let id: String
struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
@@ -16,8 +16,8 @@
import UIKit
struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -16,8 +16,8 @@
import UIKit
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
@@ -17,8 +17,8 @@
import UIKit
import UniformTypeIdentifiers
struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: String
let isOutgoing: Bool
let isEditable: Bool
@@ -16,10 +16,10 @@
import Foundation
struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let items: [RoomTimelineItemProtocol]
let itemIDs: [String]
let itemIDs: [TimelineItemIdentifier]
init(items: [RoomTimelineItemProtocol]) {
self.items = items
@@ -38,12 +38,4 @@ struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable
// Technically not a correct implementation of equality as the items themselves could be updated.
lhs.id == rhs.id && lhs.itemIDs == rhs.itemIDs
}
// MARK: - Hashable
func hash(into hasher: inout Hasher) {
// Technically not a correct implementation of hashing as the items themselves could be updated.
hasher.combine(id)
hasher.combine(itemIDs)
}
}
@@ -16,14 +16,14 @@
import UIKit
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
enum EncryptionType: Hashable {
case megolmV1AesSha2(sessionId: String)
case olmV1Curve25519AesSha2(senderKey: String)
case unknown
}
let id: String
let id: TimelineItemIdentifier
let body: String
let encryptionType: EncryptionType
let timestamp: String
@@ -17,8 +17,8 @@
import Foundation
import UIKit
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: String
let isOutgoing: Bool
@@ -16,8 +16,8 @@
import UIKit
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: String
let isOutgoing: Bool
@@ -16,8 +16,8 @@
import UIKit
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: String
let isOutgoing: Bool
@@ -16,8 +16,8 @@
import UIKit
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let eventType: String
@@ -16,6 +16,6 @@
import Foundation
struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
}
@@ -16,6 +16,6 @@
import Foundation
struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
let id = "paginationIndicatorTimelineItemIdentifier"
struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(timelineID: "paginationIndicatorTimelineItemIdentifier")
}
@@ -16,6 +16,6 @@
import Foundation
struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
let id = "readMarkerTimelineItemIdentifier"
struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(timelineID: "readMarkerTimelineItemIdentifier")
}
@@ -16,7 +16,7 @@
import Foundation
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
let id: String
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let text: String
}
@@ -16,7 +16,7 @@
import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
let id: String = UUID().uuidString
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(timelineID: UUID().uuidString)
let name: String?
}
@@ -138,8 +138,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
@@ -190,8 +189,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -208,8 +206,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -226,8 +223,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -244,8 +240,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -262,8 +257,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -280,8 +274,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -298,8 +291,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -316,13 +308,12 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in
AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: reaction.senders)
AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: reaction.senders.map(\.senderId))
}
.sorted { a, b in
// Sort by count and then by key for a consistence experience.
@@ -18,5 +18,5 @@ import Foundation
import UIKit
protocol RoomTimelineItemProtocol {
var id: String { get }
var id: TimelineItemIdentifier { get }
}
@@ -24,7 +24,7 @@ final class RoomTimelineItemViewModel: Identifiable, Equatable, ObservableObject
@Published var type: RoomTimelineItemType
@Published var groupStyle: TimelineGroupStyle
var id: String {
var id: TimelineItemIdentifier {
type.id
}
@@ -109,7 +109,7 @@ enum RoomTimelineItemType: Equatable {
}
}
var id: String {
var id: TimelineItemIdentifier {
switch self {
case .text(let item as RoomTimelineItemProtocol),
.separator(let item as RoomTimelineItemProtocol),
@@ -474,7 +474,7 @@ class MockScreen: Identifiable {
return navigationStackCoordinator
case .reportContent:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = ReportContentScreenCoordinator(parameters: .init(itemID: "test",
let coordinator = ReportContentScreenCoordinator(parameters: .init(eventID: "test",
senderID: RoomMemberProxyMock.mockAlice.userID,
roomProxy: RoomProxyMock(with: .init(displayName: "test"))))
navigationStackCoordinator.setRootCoordinator(coordinator)