Refactor theTimelineItemIdentifier handling; stop relying on optional EventOrTransactionIds and be explicit when setting composer modes from the draft service.

This commit is contained in:
Stefan Ceriu
2024-10-16 14:07:54 +03:00
parent 3d797ccabb
commit 35d49c4d85
56 changed files with 313 additions and 298 deletions
+4
View File
@@ -611,6 +611,7 @@
86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; }; 86F9D3028A1F4AE819D75560 /* RoomChangePermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */; };
872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; };
874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F7CCC4A9D1927223F559D5 /* AuthenticationStartScreenViewModelProtocol.swift */; }; 874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0F7CCC4A9D1927223F559D5 /* AuthenticationStartScreenViewModelProtocol.swift */; };
877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */; };
878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; }; 878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; };
87B4E59A4467F8EC75F82372 /* VoiceMessageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */; }; 87B4E59A4467F8EC75F82372 /* VoiceMessageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */; };
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; }; 87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; };
@@ -2189,6 +2190,7 @@
E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; }; E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelTests.swift; sourceTree = "<group>"; }; E45EBAFF1A83538D54ABDF92 /* ServerSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelTests.swift; sourceTree = "<group>"; };
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; }; E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; };
E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemIdentifier.swift; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; };
E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
@@ -5473,6 +5475,7 @@
0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */, 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */,
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */, 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */,
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */, 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */,
E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */,
2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */, 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */,
55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */, 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */,
F9E543072DE58E751F028998 /* TimelineProxy.swift */, F9E543072DE58E751F028998 /* TimelineProxy.swift */,
@@ -6931,6 +6934,7 @@
E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */, E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */,
79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */, 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */,
A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */, A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */,
877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */,
C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */, C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */,
6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */, 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */,
AE07F215EBC2B9CBF17AA54B /* TimelineItemMenuAction.swift in Sources */, AE07F215EBC2B9CBF17AA54B /* TimelineItemMenuAction.swift in Sources */,
@@ -334,6 +334,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
switch await roomProxy.timeline.sendMessage(replyText, switch await roomProxy.timeline.sendMessage(replyText,
html: nil, html: nil,
inReplyToEventID: nil,
intentionalMentions: .empty) { intentionalMentions: .empty) {
case .success: case .success:
break break
@@ -13987,8 +13987,8 @@ class TimelineProxyMock: TimelineProxyProtocol {
var editNewContentCalled: Bool { var editNewContentCalled: Bool {
return editNewContentCallsCount > 0 return editNewContentCallsCount > 0
} }
var editNewContentReceivedArguments: (timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)? var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)?
var editNewContentReceivedInvocations: [(timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)] = [] var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)] = []
var editNewContentUnderlyingReturnValue: Result<Void, TimelineProxyError>! var editNewContentUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var editNewContentReturnValue: Result<Void, TimelineProxyError>! { var editNewContentReturnValue: Result<Void, TimelineProxyError>! {
@@ -14014,16 +14014,16 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
} }
} }
var editNewContentClosure: ((EventTimelineItem, RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>)? var editNewContentClosure: ((EventOrTransactionId, RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>)?
func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> { func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> {
editNewContentCallsCount += 1 editNewContentCallsCount += 1
editNewContentReceivedArguments = (timelineItem: timelineItem, newContent: newContent) editNewContentReceivedArguments = (eventOrTransactionID: eventOrTransactionID, newContent: newContent)
DispatchQueue.main.async { DispatchQueue.main.async {
self.editNewContentReceivedInvocations.append((timelineItem: timelineItem, newContent: newContent)) self.editNewContentReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, newContent: newContent))
} }
if let editNewContentClosure = editNewContentClosure { if let editNewContentClosure = editNewContentClosure {
return await editNewContentClosure(timelineItem, newContent) return await editNewContentClosure(eventOrTransactionID, newContent)
} else { } else {
return editNewContentReturnValue return editNewContentReturnValue
} }
@@ -14770,15 +14770,15 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
//MARK: - sendMessage //MARK: - sendMessage
var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = 0 var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = 0
var sendMessageHtmlInReplyToIntentionalMentionsCallsCount: Int { var sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -14786,29 +14786,29 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue
} }
} }
} }
} }
var sendMessageHtmlInReplyToIntentionalMentionsCalled: Bool { var sendMessageHtmlInReplyToEventIDIntentionalMentionsCalled: Bool {
return sendMessageHtmlInReplyToIntentionalMentionsCallsCount > 0 return sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount > 0
} }
var sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)? var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments: (message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)?
var sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)] = [] var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedInvocations: [(message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)] = []
var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue: Result<Void, TimelineProxyError>! var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendMessageHtmlInReplyToIntentionalMentionsReturnValue: Result<Void, TimelineProxyError>! { var sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue: Result<Void, TimelineProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue
} else { } else {
var returnValue: Result<Void, TimelineProxyError>? = nil var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -14816,26 +14816,26 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue = newValue sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue = newValue sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue = newValue
} }
} }
} }
} }
var sendMessageHtmlInReplyToIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result<Void, TimelineProxyError>)? var sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result<Void, TimelineProxyError>)?
func sendMessage(_ message: String, html: String?, inReplyTo eventID: String?, intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> { func sendMessage(_ message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
sendMessageHtmlInReplyToIntentionalMentionsCallsCount += 1 sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount += 1
sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions) sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments = (message: message, html: html, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions)
DispatchQueue.main.async { DispatchQueue.main.async {
self.sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations.append((message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) self.sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedInvocations.append((message: message, html: html, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions))
} }
if let sendMessageHtmlInReplyToIntentionalMentionsClosure = sendMessageHtmlInReplyToIntentionalMentionsClosure { if let sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure = sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure {
return await sendMessageHtmlInReplyToIntentionalMentionsClosure(message, html, eventID, intentionalMentions) return await sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure(message, html, inReplyToEventID, intentionalMentions)
} else { } else {
return sendMessageHtmlInReplyToIntentionalMentionsReturnValue return sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue
} }
} }
//MARK: - toggleReaction //MARK: - toggleReaction
+1 -1
View File
@@ -82,7 +82,7 @@ extension Poll.Option {
extension PollRoomTimelineItem { extension PollRoomTimelineItem {
static func mock(poll: Poll, isOutgoing: Bool = true, isEditable: Bool = false) -> Self { static func mock(poll: Poll, isOutgoing: Bool = true, isEditable: Bool = false) -> Self {
.init(id: .random, .init(id: .randomEvent,
poll: poll, poll: poll,
body: "poll", body: "poll",
timestamp: "Now", timestamp: "Now",
@@ -93,7 +93,7 @@ private struct MessageForwardingListRow: View {
struct MessageForwardingScreen_Previews: PreviewProvider, TestablePreview { struct MessageForwardingScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .init(uniqueID: ""), let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .randomEvent,
roomID: "", roomID: "",
content: .init(noPointer: .init())), content: .init(noPointer: .init())),
clientProxy: ClientProxyMock(.init()), clientProxy: ClientProxyMock(.init()),
@@ -94,7 +94,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP
static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel {
ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, ResolveVerifiedUserSendFailureScreenViewModel(failure: failure,
itemID: .random, itemID: .randomEvent,
roomProxy: JoinedRoomProxyMock(.init()), roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
} }
@@ -102,7 +102,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP
struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider { struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider {
static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]), static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]),
itemID: .random, itemID: .randomEvent,
roomProxy: JoinedRoomProxyMock(.init()), roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
@@ -6,6 +6,7 @@
// //
import Compound import Compound
import MatrixRustSDK
import SwiftUI import SwiftUI
import WysiwygComposer import WysiwygComposer
@@ -283,9 +284,14 @@ extension FormatType {
} }
enum ComposerMode: Equatable { enum ComposerMode: Equatable {
enum EditSource {
case timeline
case draftService
}
case `default` case `default`
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool) case reply(eventID: String, replyDetails: TimelineItemReplyDetails, isThread: Bool)
case edit(originalItemId: TimelineItemIdentifier) case edit(originalEventOrTransactionID: EventOrTransactionId, source: EditSource)
case recordVoiceMessage(state: AudioRecorderState) case recordVoiceMessage(state: AudioRecorderState)
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool)
@@ -323,8 +329,8 @@ enum ComposerMode: Equatable {
var replyEventID: String? { var replyEventID: String? {
switch self { switch self {
case .reply(let itemID, _, _): case .reply(let eventID, _, _):
return itemID.eventID return eventID
default: default:
return nil return nil
} }
@@ -258,9 +258,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
case .newMessage: case .newMessage:
set(mode: .default) set(mode: .default)
case .edit(let eventID): case .edit(let eventID):
set(mode: .edit(originalItemId: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: eventID)))) set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID), source: .draftService))
case .reply(let eventID): case .reply(let eventID):
set(mode: .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: eventID)), replyDetails: .loading(eventID: eventID), isThread: false)) set(mode: .reply(eventID: eventID, replyDetails: .loading(eventID: eventID), isThread: false))
replyLoadingTask = Task { replyLoadingTask = Task {
let reply = switch await draftService.getReply(eventID: eventID) { let reply = switch await draftService.getReply(eventID: eventID) {
case .success(let reply): case .success(let reply):
@@ -273,7 +273,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
return return
} }
set(mode: .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: eventID)), replyDetails: reply.details, isThread: reply.isThreaded)) set(mode: .reply(eventID: eventID, replyDetails: reply.details, isThread: reply.isThreaded))
} }
} }
} }
@@ -314,17 +314,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
switch state.composerMode { switch state.composerMode {
case .default: case .default:
type = .newMessage type = .newMessage
case .edit(let itemID): case .edit(.eventId(let originalEventID), _):
guard let eventID = itemID.eventID else { type = .edit(eventID: originalEventID)
MXLog.error("The event id for this message is missing") case .reply(let eventID, _, _):
return
}
type = .edit(eventID: eventID)
case .reply(let itemID, _, _):
guard let eventID = itemID.eventID else {
MXLog.error("The event id for this message is missing")
return
}
type = .reply(eventID: eventID) type = .reply(eventID: eventID)
default: default:
if isVolatile { if isVolatile {
@@ -415,10 +415,10 @@ extension ComposerToolbar {
mentionDisplayHelper: ComposerMentionDisplayHelper.mock, mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock()) composerDraftService: ComposerDraftServiceMock())
model.state.composerMode = isLoading ? .reply(itemID: .init(uniqueID: ""), model.state.composerMode = isLoading ? .reply(eventID: UUID().uuidString,
replyDetails: .loading(eventID: ""), replyDetails: .loading(eventID: ""),
isThread: false) : isThread: false) :
.reply(itemID: .init(uniqueID: ""), .reply(eventID: UUID().uuidString,
replyDetails: .loaded(sender: .init(id: "", replyDetails: .loaded(sender: .init(id: "",
displayName: "Test"), displayName: "Test"),
eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false) eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false)
@@ -275,9 +275,9 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
messageComposer() messageComposer()
messageComposer(.init(string: "Some message"), messageComposer(.init(string: "Some message"),
mode: .edit(originalItemId: .random)) mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), source: .timeline))
messageComposer(mode: .reply(itemID: .random, messageComposer(mode: .reply(eventID: UUID().uuidString,
replyDetails: .loaded(sender: .init(id: "Kirk"), replyDetails: .loaded(sender: .init(id: "Kirk"),
eventID: "123", eventID: "123",
eventContent: .message(.text(.init(body: "Text: Where the wild things are")))), eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
@@ -288,7 +288,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
ScrollView { ScrollView {
VStack(spacing: 8) { VStack(spacing: 8) {
ForEach(replyTypes, id: \.self) { replyDetails in ForEach(replyTypes, id: \.self) { replyDetails in
messageComposer(mode: .reply(itemID: .random, messageComposer(mode: .reply(eventID: UUID().uuidString,
replyDetails: replyDetails, isThread: false)) replyDetails: replyDetails, isThread: false))
} }
} }
@@ -300,7 +300,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
ScrollView { ScrollView {
VStack(spacing: 8) { VStack(spacing: 8) {
ForEach(replyTypes, id: \.self) { replyDetails in ForEach(replyTypes, id: \.self) { replyDetails in
messageComposer(mode: .reply(itemID: .random, messageComposer(mode: .reply(eventID: UUID().uuidString,
replyDetails: replyDetails, isThread: true)) replyDetails: replyDetails, isThread: true))
} }
} }
@@ -143,7 +143,7 @@ class TimelineInteractionHandler {
let replyInfo = buildReplyInfo(for: eventTimelineItem) let replyInfo = buildReplyInfo(for: eventTimelineItem)
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type) let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type)
actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread)))) actionsSubject.send(.composer(action: .setMode(mode: .reply(eventID: eventID, replyDetails: replyDetails, isThread: replyInfo.isThread))))
case .forward(let itemID): case .forward(let itemID):
actionsSubject.send(.displayMessageForwarding(itemID: itemID)) actionsSubject.send(.displayMessageForwarding(itemID: itemID))
case .viewSource: case .viewSource:
@@ -184,6 +184,11 @@ class TimelineInteractionHandler {
} }
private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) { private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) {
guard case let .event(_, eventOrTransactionID) = messageTimelineItem.id else {
MXLog.error("Failed editing message, missing event id")
return
}
let text: String let text: String
var htmlText: String? var htmlText: String?
switch messageTimelineItem.contentType { switch messageTimelineItem.contentType {
@@ -197,7 +202,7 @@ class TimelineInteractionHandler {
} }
// Always update the mode first and then the text so that the composer has time to save the text draft // Always update the mode first and then the text so that the composer has time to save the text draft
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) actionsSubject.send(.composer(action: .setMode(mode: .edit(originalEventOrTransactionID: eventOrTransactionID, source: .timeline))))
actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText))) actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText)))
} }
@@ -593,13 +593,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
actionsSubject.send(.composer(action: .clear)) actionsSubject.send(.composer(action: .clear))
switch mode { switch mode {
case .reply(let itemId, _, _): case .reply(let eventID, _, _):
await timelineController.sendMessage(message, await timelineController.sendMessage(message,
html: html, html: html,
inReplyTo: itemId, inReplyToEventID: eventID,
intentionalMentions: intentionalMentions) intentionalMentions: intentionalMentions)
case .edit(let originalItemId): case .edit(let originalEventOrTransactionID, let source):
await timelineController.edit(originalItemId, await timelineController.edit(originalEventOrTransactionID,
useTimeline: source == .timeline,
message: message, message: message,
html: html, html: html,
intentionalMentions: intentionalMentions) intentionalMentions: intentionalMentions)
@@ -610,6 +611,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .none: case .none:
await timelineController.sendMessage(message, await timelineController.sendMessage(message,
html: html, html: html,
inReplyToEventID: nil,
intentionalMentions: intentionalMentions) intentionalMentions: intentionalMentions)
} }
case .recordVoiceMessage, .previewVoiceMessage: case .recordVoiceMessage, .previewVoiceMessage:
@@ -19,7 +19,7 @@ struct SwipeToReplyView: View {
} }
struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview { struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview {
static let timelineItem = TextRoomTimelineItem(id: .init(uniqueID: ""), static let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -398,7 +398,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
static var replies: some View { static var replies: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -411,7 +411,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
eventContent: .message(.text(.init(body: "Short"))))), eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single)) groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -443,7 +443,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
static var encryptionAuthenticity: some View { static var encryptionAuthenticity: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -454,7 +454,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))), properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))),
groupStyle: .single)) groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -466,7 +466,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
encryptionAuthenticity: .unsignedDevice(color: .red))), encryptionAuthenticity: .unsignedDevice(color: .red))),
groupStyle: .single)) groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -477,7 +477,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))), properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))),
groupStyle: .first)) groupStyle: .first))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -488,7 +488,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
groupStyle: .last)) groupStyle: .last))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -501,7 +501,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray)))) properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""), VoiceMessageRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -514,7 +514,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil, source: nil,
contentType: nil), contentType: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))), properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage, title: L10n.commonVoiceMessage,
duration: 10, duration: 10,
waveform: EstimatedWaveform.mockWaveform)) waveform: EstimatedWaveform.mockWaveform))
@@ -616,14 +616,14 @@ private struct MockTimelineContent: View {
source: nil, source: nil,
contentType: nil), contentType: nil),
replyDetails: replyDetails), replyDetails: replyDetails),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage, title: L10n.commonVoiceMessage,
duration: 10, duration: 10,
waveform: EstimatedWaveform.mockWaveform)) waveform: EstimatedWaveform.mockWaveform))
} }
func makeItemIdentifier() -> TimelineItemIdentifier { func makeItemIdentifier() -> TimelineItemIdentifier {
isPinned ? .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .random isPinned ? .event(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .randomEvent
} }
var replyDetails: TimelineItemReplyDetails? { var replyDetails: TimelineItemReplyDetails? {
@@ -180,22 +180,22 @@ private extension EncryptionAuthenticity {
struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview { struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM", localizedString: "09:47 AM",
layoutType: .horizontal())) layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM", localizedString: "09:47 AM",
status: .sendingFailed, status: .sendingFailed,
layoutType: .horizontal())) layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM", localizedString: "09:47 AM",
status: .encryptionAuthenticity(.unsignedDevice(color: .red)), status: .encryptionAuthenticity(.unsignedDevice(color: .red)),
layoutType: .horizontal())) layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM", localizedString: "09:47 AM",
status: .encryptionAuthenticity(.notGuaranteed(color: .gray)), status: .encryptionAuthenticity(.notGuaranteed(color: .gray)),
layoutType: .horizontal())) layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random, TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM", localizedString: "09:47 AM",
status: .encryptionAuthenticity(.sentInClear(color: .red)), status: .encryptionAuthenticity(.sentInClear(color: .red)),
layoutType: .horizontal())) layoutType: .horizontal()))
@@ -57,7 +57,7 @@ struct TimelineStyler<Content: View>: View {
struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock static let viewModel = TimelineViewModel.mock
static let base = TextRoomTimelineItem(id: .random, static let base = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -80,7 +80,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let sendingLast: TextRoomTimelineItem = { static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString
var result = TextRoomTimelineItem(id: .init(uniqueID: id), var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -100,7 +100,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let sentLast: TextRoomTimelineItem = { static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString
let result = TextRoomTimelineItem(id: .init(uniqueID: id), let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -111,7 +111,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
return result return result
}() }()
static let ltrString = TextRoomTimelineItem(id: .random, static let ltrString = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -119,7 +119,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .test, content: .init(body: "house!")) sender: .test, content: .init(body: "house!"))
static let rtlString = TextRoomTimelineItem(id: .random, static let rtlString = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -127,7 +127,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isThreaded: false, isThreaded: false,
sender: .test, content: .init(body: "באמת!")) sender: .test, content: .init(body: "באמת!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random, static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test, sender: .test,
content: .init(body: "house! -- באמת‏! -- house!")) content: .init(body: "house! -- באמת‏! -- house!"))
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random, static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -145,7 +145,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test, sender: .test,
content: .init(body: "באמת‏! -- house! -- באמת!")) content: .init(body: "באמת‏! -- house! -- באמת!"))
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random, static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -154,7 +154,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test, sender: .test,
content: .init(body: "house! -- באמת!")) content: .init(body: "house! -- באמת!"))
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random, static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -196,20 +196,20 @@ struct TimelineReactionViewPreviewsContainer: View {
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
TimelineReactionsView(context: TimelineViewModel.mock.context, TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "1"), itemID: .randomEvent,
reactions: [AggregatedReaction.mockReactionWithLongText, reactions: [AggregatedReaction.mockReactionWithLongText,
AggregatedReaction.mockReactionWithLongTextRTL]) AggregatedReaction.mockReactionWithLongTextRTL])
Divider() Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context, TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "2"), itemID: .randomEvent,
reactions: Array(AggregatedReaction.mockReactions.prefix(3))) reactions: Array(AggregatedReaction.mockReactions.prefix(3)))
Divider() Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context, TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "3"), itemID: .randomEvent,
reactions: AggregatedReaction.mockReactions) reactions: AggregatedReaction.mockReactions)
Divider() Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context, TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "4"), itemID: .randomEvent,
reactions: AggregatedReaction.mockReactions, reactions: AggregatedReaction.mockReactions,
isLayoutRTL: true) isLayoutRTL: true)
} }
@@ -103,7 +103,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: .random, TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -34,7 +34,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
static var body: some View { static var body: some View {
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .random, AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -30,7 +30,7 @@ struct CallInviteRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
static var body: some View { static var body: some View {
CallInviteRoomTimelineView(timelineItem: .init(id: .random, CallInviteRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isEditable: false, isEditable: false,
canBeRepliedTo: false, canBeRepliedTo: false,
@@ -60,7 +60,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
} }
static var body: some View { static var body: some View {
CallNotificationRoomTimelineView(timelineItem: .init(id: .random, CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isEditable: false, isEditable: false,
canBeRepliedTo: false, canBeRepliedTo: false,
@@ -52,8 +52,8 @@ struct CollapsibleRoomTimelineView: View {
struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let item = CollapsibleTimelineItem(items: [ static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: .init(uniqueID: "First separator"), text: "This is a separator"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: "First separator"), text: "This is a separator"),
SeparatorRoomTimelineItem(id: .init(uniqueID: "Second separator"), text: "This is another separator") SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Second separator"), text: "This is another separator")
]) ])
static var previews: some View { static var previews: some View {
@@ -42,7 +42,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
EmoteRoomTimelineItem(id: .random, EmoteRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -77,7 +77,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
EncryptedRoomTimelineItem(id: .random, EncryptedRoomTimelineItem(id: .randomEvent,
body: text, body: text,
encryptionType: .unknown, encryptionType: .unknown,
timestamp: timestamp, timestamp: timestamp,
@@ -88,7 +88,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func expectedItemWith(timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { private static func expectedItemWith(timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
EncryptedRoomTimelineItem(id: .random, EncryptedRoomTimelineItem(id: .randomEvent,
body: L10n.commonUnableToDecryptNoAccess, body: L10n.commonUnableToDecryptNoAccess,
encryptionType: .megolmV1AesSha2(sessionID: "foo", cause: .membership), encryptionType: .megolmV1AesSha2(sessionID: "foo", cause: .membership),
timestamp: timestamp, timestamp: timestamp,
@@ -35,7 +35,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -47,7 +47,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
thumbnailSource: nil, thumbnailSource: nil,
contentType: nil))) contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -59,7 +59,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
thumbnailSource: nil, thumbnailSource: nil,
contentType: nil))) contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random, FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -52,7 +52,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -63,7 +63,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"), source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"),
thumbnailSource: nil))) thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -74,7 +74,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
thumbnailSource: nil))) thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random, ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -86,7 +86,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
@ViewBuilder @ViewBuilder
static var states: some View { static var states: some View {
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -95,7 +95,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description"))) content: .init(body: "Fallback geo uri description")))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -104,7 +104,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description", content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"))) geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description")))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -54,7 +54,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
NoticeRoomTimelineItem(id: .random, NoticeRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -29,12 +29,12 @@ struct ReadMarkerRoomTimelineView: View {
struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock static let viewModel = TimelineViewModel.mock
static let item = ReadMarkerRoomTimelineItem(id: .init(uniqueID: .init(UUID().uuidString))) static let item = ReadMarkerRoomTimelineItem(id: .randomVirtual)
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(uniqueID: "Separator"), text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .random, RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: true, isOutgoing: true,
isEditable: false, isEditable: false,
@@ -45,8 +45,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
ReadMarkerRoomTimelineView(timelineItem: item) ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(uniqueID: "Separator"), text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .random, RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -33,7 +33,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
RedactedRoomTimelineItem(id: .random, RedactedRoomTimelineItem(id: .randomEvent,
body: text, body: text,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: false, isOutgoing: false,
@@ -23,7 +23,8 @@ struct SeparatorRoomTimelineView: View {
struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
let item = SeparatorRoomTimelineItem(id: .init(uniqueID: "Separator"), text: "This is a separator") let item = SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Separator"),
text: "This is a separator")
SeparatorRoomTimelineView(timelineItem: item) SeparatorRoomTimelineView(timelineItem: item)
} }
} }
@@ -30,7 +30,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider, TestablePreview {
StateRoomTimelineView(timelineItem: item) StateRoomTimelineView(timelineItem: item)
} }
static let item = StateRoomTimelineItem(id: .random, static let item = StateRoomTimelineItem(id: .randomVirtual,
body: "Alice joined", body: "Alice joined",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -43,7 +43,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Some image", body: "Some image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -52,7 +52,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Some other image", body: "Some other image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -61,7 +61,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"), sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory)) imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random, StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Blurhashed image", body: "Blurhashed image",
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -80,7 +80,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: .random, TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: isOutgoing, isOutgoing: isOutgoing,
isEditable: isOutgoing, isEditable: isOutgoing,
@@ -94,7 +94,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
let attributedString = builder.fromHTML(html) let attributedString = builder.fromHTML(html)
return TextRoomTimelineItem(id: .random, return TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: isOutgoing, isOutgoing: isOutgoing,
isEditable: isOutgoing, isEditable: isOutgoing,
@@ -52,7 +52,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
} }
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
UnsupportedRoomTimelineItem(id: .random, UnsupportedRoomTimelineItem(id: .randomEvent,
body: text, body: text,
eventType: "Some Event Type", eventType: "Some Event Type",
error: "Something went wrong", error: "Something went wrong",
@@ -63,7 +63,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View { static var body: some View {
VStack(spacing: 20.0) { VStack(spacing: 20.0) {
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -75,7 +75,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: nil, source: nil,
thumbnailSource: nil))) thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -87,7 +87,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: nil, source: nil,
thumbnailSource: nil))) thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random, VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -10,9 +10,9 @@ import Foundation
enum RoomTimelineItemFixtures { enum RoomTimelineItemFixtures {
/// The default timeline items used in Xcode previews etc. /// The default timeline items used in Xcode previews etc.
static var `default`: [RoomTimelineItemProtocol] = [ static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(id: .init(uniqueID: "Yesterday"), text: "Yesterday"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Yesterday"), text: "Yesterday"),
TextRoomTimelineItem(id: .init(uniqueID: ".RoomTimelineItemFixtures.default.0", TextRoomTimelineItem(id: .event(uniqueID: ".RoomTimelineItemFixtures.default.0",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")),
timestamp: "10:10 AM", timestamp: "10:10 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -21,8 +21,8 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Jacob"), sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"), content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)), properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.1", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.1",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -33,8 +33,8 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [ properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())]) AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())])
])), ])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.2", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.2",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -52,9 +52,9 @@ enum RoomTimelineItemFixtures {
ReactionSender(id: "jacob", timestamp: Date()) ReactionSender(id: "jacob", timestamp: Date())
]) ])
])), ])),
SeparatorRoomTimelineItem(id: .init(uniqueID: "Today"), text: "Today"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Today"), text: "Today"),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.3", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.3",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -63,8 +63,8 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Helena"), sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"), 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)])), properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.4", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.4",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -72,8 +72,8 @@ enum RoomTimelineItemFixtures {
isThreaded: false, isThreaded: false,
sender: .init(id: "", displayName: "Bob"), sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")), content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.5", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.5",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -86,8 +86,8 @@ enum RoomTimelineItemFixtures {
ReadReceipt(userID: "bob", formattedTimestamp: nil), ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil), ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])), ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.6", TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.6",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -242,7 +242,7 @@ enum RoomTimelineItemFixtures {
static var permalinkChunk: [RoomTimelineItemProtocol] { static var permalinkChunk: [RoomTimelineItemProtocol] {
(1...20).map { index in (1...20).map { index in
TextRoomTimelineItem(id: .init(uniqueID: "\(index)", eventOrTransactionID: .eventId(eventId: "$\(index)")), TextRoomTimelineItem(id: .event(uniqueID: "\(index)", eventOrTransactionID: .eventId(eventId: "$\(index)")),
text: "Message ID \(index)", text: "Message ID \(index)",
senderDisplayName: index > 10 ? "Alice" : "Bob") senderDisplayName: index > 10 ? "Alice" : "Bob")
} }
@@ -250,7 +250,7 @@ enum RoomTimelineItemFixtures {
static var mediaChunk: [RoomTimelineItemProtocol] { static var mediaChunk: [RoomTimelineItemProtocol] {
[ [
VideoRoomTimelineItem(id: .random, VideoRoomTimelineItem(id: .randomEvent,
timestamp: "10:47 am", timestamp: "10:47 am",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -265,7 +265,7 @@ enum RoomTimelineItemFixtures {
height: 1080, height: 1080,
aspectRatio: 1.78, aspectRatio: 1.78,
blurhash: "KtI~70X5V?yss9oyrYs:t6")), blurhash: "KtI~70X5V?yss9oyrYs:t6")),
ImageRoomTimelineItem(id: .random, ImageRoomTimelineItem(id: .randomEvent,
timestamp: "10:47 am", timestamp: "10:47 am",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -285,7 +285,7 @@ enum RoomTimelineItemFixtures {
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(id: TimelineItemIdentifier? = nil, text: String, senderDisplayName: String) { init(id: TimelineItemIdentifier? = nil, text: String, senderDisplayName: String) {
self.init(id: id ?? .random, self.init(id: id ?? .randomEvent,
timestamp: "10:47 am", timestamp: "10:47 am",
isOutgoing: senderDisplayName == "Alice", isOutgoing: senderDisplayName == "Alice",
isEditable: false, isEditable: false,
@@ -81,12 +81,13 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func sendMessage(_ message: String, func sendMessage(_ message: String,
html: String?, html: String?,
inReplyTo itemID: TimelineItemIdentifier?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async { } intentionalMentions: IntentionalMentions) async { }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { } func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { }
func edit(_ timelineItemID: TimelineItemIdentifier, func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String, message: String,
html: String?, html: String?,
intentionalMentions: IntentionalMentions) async { } intentionalMentions: IntentionalMentions) async { }
@@ -143,22 +143,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
func sendMessage(_ message: String, func sendMessage(_ message: String,
html: String?, html: String?,
inReplyTo itemID: TimelineItemIdentifier?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async { intentionalMentions: IntentionalMentions) async {
var inReplyTo: String? MXLog.info("Send message in \(roomID)")
if itemID == nil {
MXLog.info("Send message in \(roomID)")
} 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 activeTimeline.sendMessage(message, switch await activeTimeline.sendMessage(message,
html: html, html: html,
inReplyTo: inReplyTo, inReplyToEventID: inReplyToEventID,
intentionalMentions: intentionalMentions) { intentionalMentions: intentionalMentions) {
case .success: case .success:
MXLog.info("Finished sending message") MXLog.info("Finished sending message")
@@ -178,37 +169,31 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
} }
func edit(_ timelineItemID: TimelineItemIdentifier, func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String, message: String,
html: String?, html: String?,
intentionalMentions: IntentionalMentions) async { intentionalMentions: IntentionalMentions) async {
MXLog.info("Edit message in \(roomID)") MXLog.info("Edit message in \(roomID)")
MXLog.info("Editing timeline item: \(timelineItemID)") MXLog.info("Editing timeline item: \(eventOrTransactionID)")
let editMode: EditMode
if !timelineItemID.uniqueID.isEmpty,
let timelineItem = liveTimelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID) {
editMode = .byEvent(timelineItem)
} else if let eventID = timelineItemID.eventID {
editMode = .byID(eventID)
} else {
MXLog.error("Unknown timeline item: \(timelineItemID)")
return
}
let messageContent = activeTimeline.buildMessageContentFor(message, let messageContent = activeTimeline.buildMessageContentFor(message,
html: html, html: html,
intentionalMentions: intentionalMentions.toRustMentions()) intentionalMentions: intentionalMentions.toRustMentions())
switch editMode { if useTimeline {
case let .byEvent(item): switch await activeTimeline.edit(eventOrTransactionID, newContent: messageContent) {
switch await activeTimeline.edit(item, newContent: messageContent) {
case .success: case .success:
MXLog.info("Finished editing message by event") MXLog.info("Finished editing message by event")
case let .failure(error): case let .failure(error):
MXLog.error("Failed editing message by event with error: \(error)") MXLog.error("Failed editing message by event with error: \(error)")
} }
case let .byID(eventID): } else {
guard case let .eventId(eventID) = eventOrTransactionID else {
MXLog.error("Failed editing message, missing eventID.")
return
}
switch await roomProxy.edit(eventID: eventID, newContent: messageContent) { switch await roomProxy.edit(eventID: eventID, newContent: messageContent) {
case .success: case .success:
MXLog.info("Finished editing message by event ID") MXLog.info("Finished editing message by event ID")
@@ -398,9 +383,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)) let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
let dateString = date.formatted(date: .complete, time: .omitted) let dateString = date.formatted(date: .complete, time: .omitted)
return SeparatorRoomTimelineItem(id: .init(uniqueID: dateString), text: dateString) return SeparatorRoomTimelineItem(id: .virtual(uniqueID: uniqueID), text: dateString)
case .readMarker: case .readMarker:
return ReadMarkerRoomTimelineItem(id: .init(uniqueID: uniqueID)) return ReadMarkerRoomTimelineItem(id: .virtual(uniqueID: uniqueID))
} }
case .unknown: case .unknown:
return nil return nil
@@ -453,8 +438,3 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return nil return nil
} }
} }
private enum EditMode {
case byEvent(EventTimelineItem)
case byID(String)
}
@@ -48,10 +48,11 @@ protocol RoomTimelineControllerProtocol {
func sendMessage(_ message: String, func sendMessage(_ message: String,
html: String?, html: String?,
inReplyTo itemID: TimelineItemIdentifier?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async intentionalMentions: IntentionalMentions) async
func edit(_ timelineItemID: TimelineItemIdentifier, func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String, message: String,
html: String?, html: String?,
intentionalMentions: IntentionalMentions) async intentionalMentions: IntentionalMentions) async
@@ -72,14 +73,3 @@ protocol RoomTimelineControllerProtocol {
func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date?
} }
extension RoomTimelineControllerProtocol {
func sendMessage(_ message: String,
html: String?,
intentionalMentions: IntentionalMentions) async {
await sendMessage(message,
html: html,
inReplyTo: nil,
intentionalMentions: intentionalMentions)
}
}
@@ -0,0 +1,66 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
/// A timeline item identifier
/// - uniqueID: Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline.
/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable {
case event(uniqueID: String, eventOrTransactionID: EventOrTransactionId)
case virtual(uniqueID: String)
var uniqueID: String {
switch self {
case .event(let uniqueID, _):
return uniqueID
case .virtual(let uniqueID):
return uniqueID
}
}
var eventID: String? {
guard case let .event(_, eventOrTransactionID) = self else {
return nil
}
switch eventOrTransactionID {
case .eventId(let eventID):
return eventID
default:
return nil
}
}
var transactionID: String? {
guard case let .event(_, eventOrTransactionID) = self else {
return nil
}
switch eventOrTransactionID {
case .transactionId(let transactionID):
return transactionID
default:
return nil
}
}
}
// MARK: - Mocks
extension TimelineItemIdentifier {
static var randomEvent: Self {
.event(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: UUID().uuidString))
}
static var randomVirtual: Self {
.virtual(uniqueID: UUID().uuidString)
}
}
@@ -8,40 +8,6 @@
import Foundation import Foundation
import MatrixRustSDK import MatrixRustSDK
struct TimelineItemIdentifier: Hashable {
/// Stable id across state changes of the timeline item, it uniquely identifies an item in a timeline.
/// It's value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
let uniqueID: String
/// Contains the 2 possible identifiers of an event, either it has a remote event id or a local transaction id, never both or none.
var eventOrTransactionID: EventOrTransactionId?
var eventID: String? {
switch eventOrTransactionID {
case .eventId(let eventId):
return eventId
default:
return nil
}
}
var transactionID: String? {
switch eventOrTransactionID {
case .transactionId(let transactionId):
return transactionId
default:
return nil
}
}
}
extension TimelineItemIdentifier {
/// Use only for mocks/tests
static var random: Self {
.init(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: UUID().uuidString))
}
}
/// A light wrapper around timeline items returned from Rust. /// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy { enum TimelineItemProxy {
case event(EventTimelineItemProxy) case event(EventTimelineItemProxy)
@@ -108,7 +74,7 @@ class EventTimelineItemProxy {
init(item: MatrixRustSDK.EventTimelineItem, uniqueID: String) { init(item: MatrixRustSDK.EventTimelineItem, uniqueID: String) {
self.item = item self.item = item
id = TimelineItemIdentifier(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId) id = .event(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId)
} }
lazy var deliveryStatus: TimelineItemDeliveryStatus? = { lazy var deliveryStatus: TimelineItemDeliveryStatus? = {
@@ -97,7 +97,7 @@ struct VoiceMessageRoomPlaybackView_Previews: PreviewProvider, TestablePreview {
294, 131, 19, 2, 3, 3, 1, 2, 0, 0, 294, 131, 19, 2, 3, 3, 1, 2, 0, 0,
0, 0, 0, 0, 0, 3]) 0, 0, 0, 0, 0, 3])
static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.random), static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage, title: L10n.commonVoiceMessage,
duration: 10.0, duration: 10.0,
waveform: waveform, waveform: waveform,
@@ -55,7 +55,7 @@ struct VoiceMessageRoomTimelineView: View {
struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock static let viewModel = TimelineViewModel.mock
static let timelineItemIdentifier = TimelineItemIdentifier.random static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent
static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier, static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -22,6 +22,6 @@ struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equa
} }
init(position: Position) { init(position: Position) {
id = TimelineItemIdentifier(uniqueID: position.id) id = .virtual(uniqueID: position.id)
} }
} }
@@ -8,6 +8,6 @@
import Foundation import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(uniqueID: UUID().uuidString) let id: TimelineItemIdentifier = .virtual(uniqueID: UUID().uuidString)
let name: String? let name: String?
} }
@@ -164,17 +164,17 @@ final class TimelineProxy: TimelineProxyProtocol {
} }
} }
func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> { func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> {
do { do {
guard try await timeline.edit(eventOrTransactionId: timelineItem.eventOrTransactionId, newContent: .roomMessage(content: newContent)) == true else { guard try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: .roomMessage(content: newContent)) == true else {
return .failure(.failedEditing) return .failure(.failedEditing)
} }
MXLog.info("Finished editing timeline item: \(timelineItem.eventOrTransactionId)") MXLog.info("Finished editing timeline item: \(eventOrTransactionID)")
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed editing timeline item: \(timelineItem.eventOrTransactionId) with error: \(error)") MXLog.error("Failed editing timeline item: \(eventOrTransactionID) with error: \(error)")
return .failure(.sdkError(error)) return .failure(.sdkError(error))
} }
} }
@@ -366,10 +366,10 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendMessage(_ message: String, func sendMessage(_ message: String,
html: String?, html: String?,
inReplyTo eventID: String? = nil, inReplyToEventID: String? = nil,
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> { intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
if let eventID { if let inReplyToEventID {
MXLog.info("Sending reply to eventID: \(eventID)") MXLog.info("Sending reply to eventID: \(inReplyToEventID)")
} else { } else {
MXLog.info("Sending message") MXLog.info("Sending message")
} }
@@ -379,16 +379,16 @@ final class TimelineProxy: TimelineProxyProtocol {
intentionalMentions: intentionalMentions.toRustMentions()) intentionalMentions: intentionalMentions.toRustMentions())
do { do {
if let eventID { if let inReplyToEventID {
try await timeline.sendReply(msg: messageContent, eventId: eventID) try await timeline.sendReply(msg: messageContent, eventId: inReplyToEventID)
MXLog.info("Finished sending reply to eventID: \(eventID)") MXLog.info("Finished sending reply to eventID: \(inReplyToEventID)")
} else { } else {
_ = try await timeline.send(msg: messageContent) _ = try await timeline.send(msg: messageContent)
MXLog.info("Finished sending message") MXLog.info("Finished sending message")
} }
} catch { } catch {
if let eventID { if let inReplyToEventID {
MXLog.error("Failed sending reply to eventID: \(eventID) with error: \(error)") MXLog.error("Failed sending reply to eventID: \(inReplyToEventID) with error: \(error)")
} else { } else {
MXLog.error("Failed sending message with error: \(error)") MXLog.error("Failed sending message with error: \(error)")
} }
@@ -631,4 +631,16 @@ extension Array where Element == TimelineItemProxy {
return nil return nil
} }
func firstEventTimelineItemUsingEventOrTransactionID(_ eventOrTransactionID: EventOrTransactionId) -> EventTimelineItem? {
for item in self {
if case let .event(eventTimelineItem) = item,
case let .event(_, identifier) = eventTimelineItem.id,
identifier == eventOrTransactionID {
return eventTimelineItem.item
}
}
return nil
}
} }
@@ -38,7 +38,8 @@ protocol TimelineProxyProtocol {
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError>
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError>
func edit(_ timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> func edit(_ eventOrTransactionID: EventOrTransactionId,
newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>
func redact(_ timelineItemID: TimelineItemIdentifier, func redact(_ timelineItemID: TimelineItemIdentifier,
reason: String?) async -> Result<Void, TimelineProxyError> reason: String?) async -> Result<Void, TimelineProxyError>
@@ -89,12 +90,11 @@ protocol TimelineProxyProtocol {
func sendMessage(_ message: String, func sendMessage(_ message: String,
html: String?, html: String?,
inReplyTo eventID: String?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError>
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result<Void, TimelineProxyError> func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result<Void, TimelineProxyError>
// Polls
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
func editPoll(original eventID: String, func editPoll(original eventID: String,
@@ -112,14 +112,3 @@ protocol TimelineProxyProtocol {
html: String?, html: String?,
intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation
} }
extension TimelineProxyProtocol {
func sendMessage(_ message: String,
html: String?,
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
await sendMessage(message,
html: html,
inReplyTo: nil,
intentionalMentions: intentionalMentions)
}
}
@@ -38,7 +38,7 @@ class AudioPlayerStateTests: XCTestCase {
override func setUp() async throws { override func setUp() async throws {
audioPlayerActionsSubject = .init() audioPlayerActionsSubject = .init()
audioPlayerSeekCallsSubject = .init() audioPlayerSeekCallsSubject = .init()
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: Self.audioDuration) audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: Self.audioDuration)
audioPlayerMock = buildAudioPlayerMock() audioPlayerMock = buildAudioPlayerMock()
audioPlayerMock.seekToClosure = { [weak self] progress in audioPlayerMock.seekToClosure = { [weak self] progress in
self?.audioPlayerMock.currentTime = Self.audioDuration * progress self?.audioPlayerMock.currentTime = Self.audioDuration * progress
@@ -162,7 +162,7 @@ class AudioPlayerStateTests: XCTestCase {
func testHandlingAudioPlayerActionDidFinishLoading() async throws { func testHandlingAudioPlayerActionDidFinishLoading() async throws {
audioPlayerMock.duration = 10.0 audioPlayerMock.duration = 10.0
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0) audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0)
audioPlayerState.attachAudioPlayer(audioPlayerMock) audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
@@ -41,14 +41,14 @@ class ComposerToolbarViewModelTests: XCTestCase {
} }
func testComposerFocus() { func testComposerFocus() {
viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(uniqueID: "mock")))) viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), source: .timeline)))
XCTAssertTrue(viewModel.state.bindings.composerFocused) XCTAssertTrue(viewModel.state.bindings.composerFocused)
viewModel.process(timelineAction: .removeFocus) viewModel.process(timelineAction: .removeFocus)
XCTAssertFalse(viewModel.state.bindings.composerFocused) XCTAssertFalse(viewModel.state.bindings.composerFocused)
} }
func testComposerMode() { func testComposerMode() {
let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(uniqueID: "mock")) let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), source: .timeline)
viewModel.process(timelineAction: .setMode(mode: mode)) viewModel.process(timelineAction: .setMode(mode: mode))
XCTAssertEqual(viewModel.state.composerMode, mode) XCTAssertEqual(viewModel.state.composerMode, mode)
viewModel.process(timelineAction: .clear) viewModel.process(timelineAction: .clear)
@@ -56,7 +56,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
} }
func testComposerModeIsPublished() { func testComposerModeIsPublished() {
let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(uniqueID: "mock")) let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), source: .timeline)
let expectation = expectation(description: "Composer mode is published") let expectation = expectation(description: "Composer mode is published")
let cancellable = viewModel let cancellable = viewModel
.context .context
@@ -236,7 +236,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
} }
viewModel.context.composerFormattingEnabled = false viewModel.context.composerFormattingEnabled = false
viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "testID"))))) viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), source: .timeline)))
viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.context.plainComposerText = .init(string: "Hello world!")
viewModel.saveDraft() viewModel.saveDraft()
@@ -257,7 +257,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
} }
viewModel.context.composerFormattingEnabled = false viewModel.context.composerFormattingEnabled = false
viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "testID")), viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID",
replyDetails: .loaded(sender: .init(id: ""), replyDetails: .loaded(sender: .init(id: ""),
eventID: "testID", eventID: "testID",
eventContent: .message(.text(.init(body: "reply text")))), eventContent: .message(.text(.init(body: "reply text")))),
@@ -282,7 +282,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
} }
viewModel.context.composerFormattingEnabled = false viewModel.context.composerFormattingEnabled = false
viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "testID")), viewModel.process(timelineAction: .setMode(mode: .reply(eventID: "testID",
replyDetails: .loaded(sender: .init(id: ""), replyDetails: .loaded(sender: .init(id: ""),
eventID: "testID", eventID: "testID",
eventContent: .message(.text(.init(body: "reply text")))), eventContent: .message(.text(.init(body: "reply text")))),
@@ -395,7 +395,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
await fulfillment(of: [expectation], timeout: 10) await fulfillment(of: [expectation], timeout: 10)
XCTAssertFalse(viewModel.context.composerFormattingEnabled) XCTAssertFalse(viewModel.context.composerFormattingEnabled)
XCTAssertEqual(viewModel.state.composerMode, .edit(originalItemId: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "testID")))) XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), source: .draftService))
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!")) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!"))
} }
@@ -429,13 +429,13 @@ class ComposerToolbarViewModelTests: XCTestCase {
await fulfillment(of: [draftExpectation], timeout: 10) await fulfillment(of: [draftExpectation], timeout: 10)
XCTAssertFalse(viewModel.context.composerFormattingEnabled) XCTAssertFalse(viewModel.context.composerFormattingEnabled)
// Testing the loading state first // Testing the loading state first
XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: testEventID)), XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
replyDetails: .loading(eventID: testEventID), replyDetails: .loading(eventID: testEventID),
isThread: false)) isThread: false))
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text))
await fulfillment(of: [loadReplyExpectation], timeout: 10) await fulfillment(of: [loadReplyExpectation], timeout: 10)
XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: testEventID)), XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
replyDetails: loadedReply, replyDetails: loadedReply,
isThread: true)) isThread: true))
} }
@@ -469,7 +469,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
await fulfillment(of: [draftExpectation], timeout: 10) await fulfillment(of: [draftExpectation], timeout: 10)
XCTAssertFalse(viewModel.context.composerFormattingEnabled) XCTAssertFalse(viewModel.context.composerFormattingEnabled)
// Testing the loading state first // Testing the loading state first
XCTAssertEqual(viewModel.state.composerMode, .reply(itemID: .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: testEventID)), XCTAssertEqual(viewModel.state.composerMode, .reply(eventID: testEventID,
replyDetails: .loading(eventID: testEventID), replyDetails: .loading(eventID: testEventID),
isThread: false)) isThread: false))
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: text))
@@ -483,7 +483,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
func testSaveVolatileDraftWhenEditing() { func testSaveVolatileDraftWhenEditing() {
viewModel.context.composerFormattingEnabled = false viewModel.context.composerFormattingEnabled = false
viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.context.plainComposerText = .init(string: "Hello world!")
viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .random))) viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), source: .timeline)))
let draft = draftServiceMock.saveVolatileDraftReceivedDraft let draft = draftServiceMock.saveVolatileDraftReceivedDraft
XCTAssertNotNil(draft) XCTAssertNotNil(draft)
+6 -6
View File
@@ -116,7 +116,7 @@ class LoggingTests: XCTestCase {
func validateTimelineContentIsRedacted() throws { func validateTimelineContentIsRedacted() throws {
// Given timeline items that contain text // Given timeline items that contain text
let textAttributedString = "TextAttributed" let textAttributedString = "TextAttributed"
let textMessage = TextRoomTimelineItem(id: .random, let textMessage = TextRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -125,7 +125,7 @@ class LoggingTests: XCTestCase {
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString))) content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
let noticeAttributedString = "NoticeAttributed" let noticeAttributedString = "NoticeAttributed"
let noticeMessage = NoticeRoomTimelineItem(id: .random, let noticeMessage = NoticeRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -134,7 +134,7 @@ class LoggingTests: XCTestCase {
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString))) content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
let emoteAttributedString = "EmoteAttributed" let emoteAttributedString = "EmoteAttributed"
let emoteMessage = EmoteRoomTimelineItem(id: .random, let emoteMessage = EmoteRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -142,7 +142,7 @@ class LoggingTests: XCTestCase {
isThreaded: false, isThreaded: false,
sender: .init(id: "sender"), sender: .init(id: "sender"),
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString))) content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
let imageMessage = ImageRoomTimelineItem(id: .init(uniqueID: "myimagemessage"), let imageMessage = ImageRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -153,7 +153,7 @@ class LoggingTests: XCTestCase {
caption: "ImageString", caption: "ImageString",
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"),
thumbnailSource: nil)) thumbnailSource: nil))
let videoMessage = VideoRoomTimelineItem(id: .random, let videoMessage = VideoRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -165,7 +165,7 @@ class LoggingTests: XCTestCase {
duration: 0, duration: 0,
source: nil, source: nil,
thumbnailSource: nil)) thumbnailSource: nil))
let fileMessage = FileRoomTimelineItem(id: .random, let fileMessage = FileRoomTimelineItem(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
@@ -60,7 +60,7 @@ class MediaPlayerProviderTests: XCTestCase {
} }
func testPlayerStates() async throws { func testPlayerStates() async throws {
let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.random) let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent)
// By default, there should be no player state // By default, there should be no player state
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId)) XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId))
@@ -76,7 +76,7 @@ class MediaPlayerProviderTests: XCTestCase {
let audioPlayer = AudioPlayerMock() let audioPlayer = AudioPlayerMock()
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher() audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0), count: 10) let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0), count: 10)
for audioPlayerState in audioPlayerStates { for audioPlayerState in audioPlayerStates {
mediaPlayerProvider.register(audioPlayerState: audioPlayerState) mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
audioPlayerState.attachAudioPlayer(audioPlayer) audioPlayerState.attachAudioPlayer(audioPlayer)
@@ -95,7 +95,7 @@ class MediaPlayerProviderTests: XCTestCase {
let audioPlayer = AudioPlayerMock() let audioPlayer = AudioPlayerMock()
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher() audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.random), title: "", duration: 0), count: 10) let audioPlayerStates = Array(repeating: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0), count: 10)
for audioPlayerState in audioPlayerStates { for audioPlayerState in audioPlayerStates {
mediaPlayerProvider.register(audioPlayerState: audioPlayerState) mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
audioPlayerState.attachAudioPlayer(audioPlayer) audioPlayerState.attachAudioPlayer(audioPlayer)
@@ -12,7 +12,7 @@ import XCTest
@MainActor @MainActor
class MessageForwardingScreenViewModelTests: XCTestCase { class MessageForwardingScreenViewModelTests: XCTestCase {
let forwardingItem = MessageForwardingItem(id: .init(uniqueID: "t1", eventOrTransactionID: .eventId(eventId: "t1")), let forwardingItem = MessageForwardingItem(id: .event(uniqueID: "t1", eventOrTransactionID: .eventId(eventId: "t1")),
roomID: "1", roomID: "1",
content: .init(noPointer: .init())) content: .init(noPointer: .init()))
var viewModel: MessageForwardingScreenViewModelProtocol! var viewModel: MessageForwardingScreenViewModelProtocol!
@@ -61,7 +61,7 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
private func makeViewModel(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { private func makeViewModel(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel {
ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, ResolveVerifiedUserSendFailureScreenViewModel(failure: failure,
itemID: .random, itemID: .randomEvent,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
} }
@@ -11,7 +11,7 @@ import XCTest
final class TextBasedRoomTimelineTests: XCTestCase { final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEnd() { func testTextRoomTimelineItemWhitespaceEnd() {
let timestamp = "Now" let timestamp = "Now"
let timelineItem = TextRoomTimelineItem(id: .random, let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -24,7 +24,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndLonger() { func testTextRoomTimelineItemWhitespaceEndLonger() {
let timestamp = "10:00 AM" let timestamp = "10:00 AM"
let timelineItem = TextRoomTimelineItem(id: .random, let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -37,7 +37,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndWithEdit() { func testTextRoomTimelineItemWhitespaceEndWithEdit() {
let timestamp = "Now" let timestamp = "Now"
var timelineItem = TextRoomTimelineItem(id: .random, var timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -52,7 +52,7 @@ final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() { func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() {
let timestamp = "Now" let timestamp = "Now"
var timelineItem = TextRoomTimelineItem(id: .random, var timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp, timestamp: timestamp,
isOutgoing: true, isOutgoing: true,
isEditable: true, isEditable: true,
@@ -423,7 +423,7 @@ class TimelineViewModelTests: XCTestCase {
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) { init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) {
let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(id: sender, timestamp: Date())])] : [] let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(id: sender, timestamp: Date())])] : []
self.init(id: .random, self.init(id: .randomEvent,
timestamp: "10:47 am", timestamp: "10:47 am",
isOutgoing: sender == "bob", isOutgoing: sender == "bob",
isEditable: sender == "bob", isEditable: sender == "bob",
@@ -437,13 +437,13 @@ private extension TextRoomTimelineItem {
private extension SeparatorRoomTimelineItem { private extension SeparatorRoomTimelineItem {
init(uniqueID: String) { init(uniqueID: String) {
self.init(id: .init(uniqueID: uniqueID), text: "") self.init(id: .virtual(uniqueID: uniqueID), text: "")
} }
} }
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(eventID: String) { init(eventID: String) {
self.init(id: .init(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: eventID)), self.init(id: .event(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: eventID)),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,