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
@@ -334,6 +334,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
switch await roomProxy.timeline.sendMessage(replyText,
html: nil,
inReplyToEventID: nil,
intentionalMentions: .empty) {
case .success:
break
@@ -13987,8 +13987,8 @@ class TimelineProxyMock: TimelineProxyProtocol {
var editNewContentCalled: Bool {
return editNewContentCallsCount > 0
}
var editNewContentReceivedArguments: (timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)?
var editNewContentReceivedInvocations: [(timelineItem: EventTimelineItem, newContent: RoomMessageEventContentWithoutRelation)] = []
var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)?
var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)] = []
var editNewContentUnderlyingReturnValue: 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
editNewContentReceivedArguments = (timelineItem: timelineItem, newContent: newContent)
editNewContentReceivedArguments = (eventOrTransactionID: eventOrTransactionID, newContent: newContent)
DispatchQueue.main.async {
self.editNewContentReceivedInvocations.append((timelineItem: timelineItem, newContent: newContent))
self.editNewContentReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, newContent: newContent))
}
if let editNewContentClosure = editNewContentClosure {
return await editNewContentClosure(timelineItem, newContent)
return await editNewContentClosure(eventOrTransactionID, newContent)
} else {
return editNewContentReturnValue
}
@@ -14770,15 +14770,15 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
//MARK: - sendMessage
var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = 0
var sendMessageHtmlInReplyToIntentionalMentionsCallsCount: Int {
var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = 0
var sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount: Int {
get {
if Thread.isMainThread {
return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount
return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount
returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount
}
return returnValue!
@@ -14786,29 +14786,29 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
set {
if Thread.isMainThread {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue
sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingCallsCount = newValue
sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingCallsCount = newValue
}
}
}
}
var sendMessageHtmlInReplyToIntentionalMentionsCalled: Bool {
return sendMessageHtmlInReplyToIntentionalMentionsCallsCount > 0
var sendMessageHtmlInReplyToEventIDIntentionalMentionsCalled: Bool {
return sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount > 0
}
var sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)?
var sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)] = []
var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments: (message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)?
var sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedInvocations: [(message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions)] = []
var sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendMessageHtmlInReplyToIntentionalMentionsReturnValue: Result<Void, TimelineProxyError>! {
var sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue
return sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue
returnValue = sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue
}
return returnValue!
@@ -14816,26 +14816,26 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
set {
if Thread.isMainThread {
sendMessageHtmlInReplyToIntentionalMentionsUnderlyingReturnValue = newValue
sendMessageHtmlInReplyToEventIDIntentionalMentionsUnderlyingReturnValue = newValue
} else {
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> {
sendMessageHtmlInReplyToIntentionalMentionsCallsCount += 1
sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)
func sendMessage(_ message: String, html: String?, inReplyToEventID: String?, intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
sendMessageHtmlInReplyToEventIDIntentionalMentionsCallsCount += 1
sendMessageHtmlInReplyToEventIDIntentionalMentionsReceivedArguments = (message: message, html: html, inReplyToEventID: inReplyToEventID, intentionalMentions: intentionalMentions)
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 {
return await sendMessageHtmlInReplyToIntentionalMentionsClosure(message, html, eventID, intentionalMentions)
if let sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure = sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure {
return await sendMessageHtmlInReplyToEventIDIntentionalMentionsClosure(message, html, inReplyToEventID, intentionalMentions)
} else {
return sendMessageHtmlInReplyToIntentionalMentionsReturnValue
return sendMessageHtmlInReplyToEventIDIntentionalMentionsReturnValue
}
}
//MARK: - toggleReaction
+1 -1
View File
@@ -82,7 +82,7 @@ extension Poll.Option {
extension PollRoomTimelineItem {
static func mock(poll: Poll, isOutgoing: Bool = true, isEditable: Bool = false) -> Self {
.init(id: .random,
.init(id: .randomEvent,
poll: poll,
body: "poll",
timestamp: "Now",
@@ -93,7 +93,7 @@ private struct MessageForwardingListRow: View {
struct MessageForwardingScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .init(uniqueID: ""),
let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .randomEvent,
roomID: "",
content: .init(noPointer: .init())),
clientProxy: ClientProxyMock(.init()),
@@ -94,7 +94,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP
static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel {
ResolveVerifiedUserSendFailureScreenViewModel(failure: failure,
itemID: .random,
itemID: .randomEvent,
roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock())
}
@@ -102,7 +102,7 @@ struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestableP
struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider {
static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]),
itemID: .random,
itemID: .randomEvent,
roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock())
@@ -6,6 +6,7 @@
//
import Compound
import MatrixRustSDK
import SwiftUI
import WysiwygComposer
@@ -283,9 +284,14 @@ extension FormatType {
}
enum ComposerMode: Equatable {
enum EditSource {
case timeline
case draftService
}
case `default`
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool)
case edit(originalItemId: TimelineItemIdentifier)
case reply(eventID: String, replyDetails: TimelineItemReplyDetails, isThread: Bool)
case edit(originalEventOrTransactionID: EventOrTransactionId, source: EditSource)
case recordVoiceMessage(state: AudioRecorderState)
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool)
@@ -323,8 +329,8 @@ enum ComposerMode: Equatable {
var replyEventID: String? {
switch self {
case .reply(let itemID, _, _):
return itemID.eventID
case .reply(let eventID, _, _):
return eventID
default:
return nil
}
@@ -258,9 +258,9 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
case .newMessage:
set(mode: .default)
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):
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 {
let reply = switch await draftService.getReply(eventID: eventID) {
case .success(let reply):
@@ -273,7 +273,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
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 {
case .default:
type = .newMessage
case .edit(let itemID):
guard let eventID = itemID.eventID else {
MXLog.error("The event id for this message is missing")
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
}
case .edit(.eventId(let originalEventID), _):
type = .edit(eventID: originalEventID)
case .reply(let eventID, _, _):
type = .reply(eventID: eventID)
default:
if isVolatile {
@@ -415,10 +415,10 @@ extension ComposerToolbar {
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.composerMode = isLoading ? .reply(itemID: .init(uniqueID: ""),
model.state.composerMode = isLoading ? .reply(eventID: UUID().uuidString,
replyDetails: .loading(eventID: ""),
isThread: false) :
.reply(itemID: .init(uniqueID: ""),
.reply(eventID: UUID().uuidString,
replyDetails: .loaded(sender: .init(id: "",
displayName: "Test"),
eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false)
@@ -275,9 +275,9 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
messageComposer()
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"),
eventID: "123",
eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
@@ -288,7 +288,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
ScrollView {
VStack(spacing: 8) {
ForEach(replyTypes, id: \.self) { replyDetails in
messageComposer(mode: .reply(itemID: .random,
messageComposer(mode: .reply(eventID: UUID().uuidString,
replyDetails: replyDetails, isThread: false))
}
}
@@ -300,7 +300,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
ScrollView {
VStack(spacing: 8) {
ForEach(replyTypes, id: \.self) { replyDetails in
messageComposer(mode: .reply(itemID: .random,
messageComposer(mode: .reply(eventID: UUID().uuidString,
replyDetails: replyDetails, isThread: true))
}
}
@@ -143,7 +143,7 @@ class TimelineInteractionHandler {
let replyInfo = buildReplyInfo(for: eventTimelineItem)
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):
actionsSubject.send(.displayMessageForwarding(itemID: itemID))
case .viewSource:
@@ -184,6 +184,11 @@ class TimelineInteractionHandler {
}
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
var htmlText: String?
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
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)))
}
@@ -593,13 +593,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
actionsSubject.send(.composer(action: .clear))
switch mode {
case .reply(let itemId, _, _):
case .reply(let eventID, _, _):
await timelineController.sendMessage(message,
html: html,
inReplyTo: itemId,
inReplyToEventID: eventID,
intentionalMentions: intentionalMentions)
case .edit(let originalItemId):
await timelineController.edit(originalItemId,
case .edit(let originalEventOrTransactionID, let source):
await timelineController.edit(originalEventOrTransactionID,
useTimeline: source == .timeline,
message: message,
html: html,
intentionalMentions: intentionalMentions)
@@ -610,6 +611,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .none:
await timelineController.sendMessage(message,
html: html,
inReplyToEventID: nil,
intentionalMentions: intentionalMentions)
}
case .recordVoiceMessage, .previewVoiceMessage:
@@ -19,7 +19,7 @@ struct SwipeToReplyView: View {
}
struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview {
static let timelineItem = TextRoomTimelineItem(id: .init(uniqueID: ""),
static let timelineItem = TextRoomTimelineItem(id: .randomEvent,
timestamp: "",
isOutgoing: true,
isEditable: true,
@@ -398,7 +398,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
static var replies: some View {
VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -411,7 +411,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -443,7 +443,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
static var encryptionAuthenticity: some View {
VStack(spacing: 0) {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -454,7 +454,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))),
groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -466,7 +466,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
encryptionAuthenticity: .unsignedDevice(color: .red))),
groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: false,
isEditable: false,
@@ -477,7 +477,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))),
groupStyle: .first))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(uniqueID: ""),
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: "10:42",
isOutgoing: false,
isEditable: false,
@@ -488,7 +488,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
groupStyle: .last))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -501,7 +501,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(uniqueID: ""),
VoiceMessageRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
@@ -514,7 +514,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil,
contentType: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
@@ -616,14 +616,14 @@ private struct MockTimelineContent: View {
source: nil,
contentType: nil),
replyDetails: replyDetails),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
waveform: EstimatedWaveform.mockWaveform))
}
func makeItemIdentifier() -> TimelineItemIdentifier {
isPinned ? .init(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .random
isPinned ? .event(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .randomEvent
}
var replyDetails: TimelineItemReplyDetails? {
@@ -180,22 +180,22 @@ private extension EncryptionAuthenticity {
struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 16) {
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .sendingFailed,
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.unsignedDevice(color: .red)),
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.notGuaranteed(color: .gray)),
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.sentInClear(color: .red)),
layoutType: .horizontal()))
@@ -57,7 +57,7 @@ struct TimelineStyler<Content: View>: View {
struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static let base = TextRoomTimelineItem(id: .random,
static let base = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -80,7 +80,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let sendingLast: TextRoomTimelineItem = {
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",
isOutgoing: true,
isEditable: false,
@@ -100,7 +100,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
static let sentLast: TextRoomTimelineItem = {
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",
isOutgoing: true,
isEditable: false,
@@ -111,7 +111,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
return result
}()
static let ltrString = TextRoomTimelineItem(id: .random,
static let ltrString = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -119,7 +119,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isThreaded: false,
sender: .test, content: .init(body: "house!"))
static let rtlString = TextRoomTimelineItem(id: .random,
static let rtlString = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -127,7 +127,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isThreaded: false,
sender: .test, content: .init(body: "באמת!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .random,
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test,
content: .init(body: "house! -- באמת‏! -- house!"))
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .random,
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -145,7 +145,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test,
content: .init(body: "באמת‏! -- house! -- באמת!"))
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .random,
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -154,7 +154,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
sender: .test,
content: .init(body: "house! -- באמת!"))
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .random,
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -196,20 +196,20 @@ struct TimelineReactionViewPreviewsContainer: View {
var body: some View {
VStack(spacing: 8) {
TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "1"),
itemID: .randomEvent,
reactions: [AggregatedReaction.mockReactionWithLongText,
AggregatedReaction.mockReactionWithLongTextRTL])
Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "2"),
itemID: .randomEvent,
reactions: Array(AggregatedReaction.mockReactions.prefix(3)))
Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "3"),
itemID: .randomEvent,
reactions: AggregatedReaction.mockReactions)
Divider()
TimelineReactionsView(context: TimelineViewModel.mock.context,
itemID: .init(uniqueID: "4"),
itemID: .randomEvent,
reactions: AggregatedReaction.mockReactions,
isLayoutRTL: true)
}
@@ -103,7 +103,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: .random,
TextRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: true,
isEditable: false,
@@ -34,7 +34,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
static var body: some View {
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .random,
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -30,7 +30,7 @@ struct CallInviteRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
static var body: some View {
CallInviteRoomTimelineView(timelineItem: .init(id: .random,
CallInviteRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now",
isEditable: false,
canBeRepliedTo: false,
@@ -60,7 +60,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
}
static var body: some View {
CallNotificationRoomTimelineView(timelineItem: .init(id: .random,
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now",
isEditable: false,
canBeRepliedTo: false,
@@ -52,8 +52,8 @@ struct CollapsibleRoomTimelineView: View {
struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: .init(uniqueID: "First separator"), text: "This is a separator"),
SeparatorRoomTimelineItem(id: .init(uniqueID: "Second separator"), text: "This is another separator")
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "First separator"), text: "This is a separator"),
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Second separator"), text: "This is another separator")
])
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 {
EmoteRoomTimelineItem(id: .random,
EmoteRoomTimelineItem(id: .randomEvent,
timestamp: timestamp,
isOutgoing: 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 {
EncryptedRoomTimelineItem(id: .random,
EncryptedRoomTimelineItem(id: .randomEvent,
body: text,
encryptionType: .unknown,
timestamp: timestamp,
@@ -88,7 +88,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
private static func expectedItemWith(timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
EncryptedRoomTimelineItem(id: .random,
EncryptedRoomTimelineItem(id: .randomEvent,
body: L10n.commonUnableToDecryptNoAccess,
encryptionType: .megolmV1AesSha2(sessionID: "foo", cause: .membership),
timestamp: timestamp,
@@ -35,7 +35,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View {
VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -47,7 +47,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -59,7 +59,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .random,
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -52,7 +52,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View {
VStack(spacing: 20.0) {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -63,7 +63,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/jpg"),
thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -74,7 +74,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
thumbnailSource: nil)))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .random,
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -86,7 +86,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
@ViewBuilder
static var states: some View {
LocationRoomTimelineView(timelineItem: .init(id: .random,
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -95,7 +95,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description")))
LocationRoomTimelineView(timelineItem: .init(id: .random,
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -104,7 +104,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"),
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")))
LocationRoomTimelineView(timelineItem: .init(id: .random,
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -54,7 +54,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
NoticeRoomTimelineItem(id: .random,
NoticeRoomTimelineItem(id: .randomEvent,
timestamp: timestamp,
isOutgoing: false,
isEditable: false,
@@ -29,12 +29,12 @@ struct ReadMarkerRoomTimelineView: View {
struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
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 {
VStack(alignment: .leading, spacing: 0) {
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .random,
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "",
isOutgoing: true,
isEditable: false,
@@ -45,8 +45,8 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .init(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .random,
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "",
isOutgoing: false,
isEditable: false,
@@ -33,7 +33,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
RedactedRoomTimelineItem(id: .random,
RedactedRoomTimelineItem(id: .randomEvent,
body: text,
timestamp: timestamp,
isOutgoing: false,
@@ -23,7 +23,8 @@ struct SeparatorRoomTimelineView: View {
struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview {
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)
}
}
@@ -30,7 +30,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider, TestablePreview {
StateRoomTimelineView(timelineItem: item)
}
static let item = StateRoomTimelineItem(id: .random,
static let item = StateRoomTimelineItem(id: .randomVirtual,
body: "Alice joined",
timestamp: "Now",
isOutgoing: false,
@@ -43,7 +43,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View {
VStack(spacing: 20.0) {
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Some image",
timestamp: "Now",
isOutgoing: false,
@@ -52,7 +52,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Some other image",
timestamp: "Now",
isOutgoing: false,
@@ -61,7 +61,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "Bob"),
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .random,
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
body: "Blurhashed image",
timestamp: "Now",
isOutgoing: false,
@@ -80,7 +80,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
}
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: .random,
TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp,
isOutgoing: isOutgoing,
isEditable: isOutgoing,
@@ -94,7 +94,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
let attributedString = builder.fromHTML(html)
return TextRoomTimelineItem(id: .random,
return TextRoomTimelineItem(id: .randomEvent,
timestamp: timestamp,
isOutgoing: 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 {
UnsupportedRoomTimelineItem(id: .random,
UnsupportedRoomTimelineItem(id: .randomEvent,
body: text,
eventType: "Some Event Type",
error: "Something went wrong",
@@ -63,7 +63,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var body: some View {
VStack(spacing: 20.0) {
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -75,7 +75,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: nil,
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -87,7 +87,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
source: nil,
thumbnailSource: nil)))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .random,
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
@@ -10,9 +10,9 @@ import Foundation
enum RoomTimelineItemFixtures {
/// The default timeline items used in Xcode previews etc.
static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(id: .init(uniqueID: "Yesterday"), text: "Yesterday"),
TextRoomTimelineItem(id: .init(uniqueID: ".RoomTimelineItemFixtures.default.0",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")),
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Yesterday"), text: "Yesterday"),
TextRoomTimelineItem(id: .event(uniqueID: ".RoomTimelineItemFixtures.default.0",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")),
timestamp: "10:10 AM",
isOutgoing: false,
isEditable: false,
@@ -21,8 +21,8 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.1",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.1",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")),
timestamp: "10:11 AM",
isOutgoing: false,
isEditable: false,
@@ -33,8 +33,8 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())])
])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.2",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.2",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")),
timestamp: "10:11 AM",
isOutgoing: false,
isEditable: false,
@@ -52,9 +52,9 @@ enum RoomTimelineItemFixtures {
ReactionSender(id: "jacob", timestamp: Date())
])
])),
SeparatorRoomTimelineItem(id: .init(uniqueID: "Today"), text: "Today"),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.3",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")),
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Today"), text: "Today"),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.3",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")),
timestamp: "5 PM",
isOutgoing: false,
isEditable: false,
@@ -63,8 +63,8 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.4",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.4",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")),
timestamp: "5 PM",
isOutgoing: true,
isEditable: true,
@@ -72,8 +72,8 @@ enum RoomTimelineItemFixtures {
isThreaded: false,
sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.5",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.5",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")),
timestamp: "5 PM",
isOutgoing: true,
isEditable: true,
@@ -86,8 +86,8 @@ enum RoomTimelineItemFixtures {
ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .init(uniqueID: "RoomTimelineItemFixtures.default.6",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.6",
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")),
timestamp: "5 PM",
isOutgoing: false,
isEditable: false,
@@ -242,7 +242,7 @@ enum RoomTimelineItemFixtures {
static var permalinkChunk: [RoomTimelineItemProtocol] {
(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)",
senderDisplayName: index > 10 ? "Alice" : "Bob")
}
@@ -250,7 +250,7 @@ enum RoomTimelineItemFixtures {
static var mediaChunk: [RoomTimelineItemProtocol] {
[
VideoRoomTimelineItem(id: .random,
VideoRoomTimelineItem(id: .randomEvent,
timestamp: "10:47 am",
isOutgoing: false,
isEditable: false,
@@ -265,7 +265,7 @@ enum RoomTimelineItemFixtures {
height: 1080,
aspectRatio: 1.78,
blurhash: "KtI~70X5V?yss9oyrYs:t6")),
ImageRoomTimelineItem(id: .random,
ImageRoomTimelineItem(id: .randomEvent,
timestamp: "10:47 am",
isOutgoing: false,
isEditable: false,
@@ -285,7 +285,7 @@ enum RoomTimelineItemFixtures {
private extension TextRoomTimelineItem {
init(id: TimelineItemIdentifier? = nil, text: String, senderDisplayName: String) {
self.init(id: id ?? .random,
self.init(id: id ?? .randomEvent,
timestamp: "10:47 am",
isOutgoing: senderDisplayName == "Alice",
isEditable: false,
@@ -81,12 +81,13 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func sendMessage(_ message: String,
html: String?,
inReplyTo itemID: TimelineItemIdentifier?,
inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async { }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { }
func edit(_ timelineItemID: TimelineItemIdentifier,
func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String,
html: String?,
intentionalMentions: IntentionalMentions) async { }
@@ -143,22 +143,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
func sendMessage(_ message: String,
html: String?,
inReplyTo itemID: TimelineItemIdentifier?,
inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async {
var inReplyTo: String?
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
}
MXLog.info("Send message in \(roomID)")
switch await activeTimeline.sendMessage(message,
html: html,
inReplyTo: inReplyTo,
inReplyToEventID: inReplyToEventID,
intentionalMentions: intentionalMentions) {
case .success:
MXLog.info("Finished sending message")
@@ -178,37 +169,31 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func edit(_ timelineItemID: TimelineItemIdentifier,
func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String,
html: String?,
intentionalMentions: IntentionalMentions) async {
MXLog.info("Edit message in \(roomID)")
MXLog.info("Editing timeline item: \(timelineItemID)")
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
}
MXLog.info("Editing timeline item: \(eventOrTransactionID)")
let messageContent = activeTimeline.buildMessageContentFor(message,
html: html,
intentionalMentions: intentionalMentions.toRustMentions())
switch editMode {
case let .byEvent(item):
switch await activeTimeline.edit(item, newContent: messageContent) {
if useTimeline {
switch await activeTimeline.edit(eventOrTransactionID, newContent: messageContent) {
case .success:
MXLog.info("Finished editing message by event")
case let .failure(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) {
case .success:
MXLog.info("Finished editing message by event ID")
@@ -398,9 +383,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
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:
return ReadMarkerRoomTimelineItem(id: .init(uniqueID: uniqueID))
return ReadMarkerRoomTimelineItem(id: .virtual(uniqueID: uniqueID))
}
case .unknown:
return nil
@@ -453,8 +438,3 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return nil
}
}
private enum EditMode {
case byEvent(EventTimelineItem)
case byID(String)
}
@@ -48,10 +48,11 @@ protocol RoomTimelineControllerProtocol {
func sendMessage(_ message: String,
html: String?,
inReplyTo itemID: TimelineItemIdentifier?,
inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async
func edit(_ timelineItemID: TimelineItemIdentifier,
func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool,
message: String,
html: String?,
intentionalMentions: IntentionalMentions) async
@@ -72,14 +73,3 @@ protocol RoomTimelineControllerProtocol {
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 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.
enum TimelineItemProxy {
case event(EventTimelineItemProxy)
@@ -108,7 +74,7 @@ class EventTimelineItemProxy {
init(item: MatrixRustSDK.EventTimelineItem, uniqueID: String) {
self.item = item
id = TimelineItemIdentifier(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId)
id = .event(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId)
}
lazy var deliveryStatus: TimelineItemDeliveryStatus? = {
@@ -97,7 +97,7 @@ struct VoiceMessageRoomPlaybackView_Previews: PreviewProvider, TestablePreview {
294, 131, 19, 2, 3, 3, 1, 2, 0, 0,
0, 0, 0, 0, 0, 3])
static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.random),
static var playerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10.0,
waveform: waveform,
@@ -55,7 +55,7 @@ struct VoiceMessageRoomTimelineView: View {
struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static let timelineItemIdentifier = TimelineItemIdentifier.random
static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent
static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier,
timestamp: "Now",
isOutgoing: false,
@@ -22,6 +22,6 @@ struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equa
}
init(position: Position) {
id = TimelineItemIdentifier(uniqueID: position.id)
id = .virtual(uniqueID: position.id)
}
}
@@ -8,6 +8,6 @@
import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(uniqueID: UUID().uuidString)
let id: TimelineItemIdentifier = .virtual(uniqueID: UUID().uuidString)
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 {
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)
}
MXLog.info("Finished editing timeline item: \(timelineItem.eventOrTransactionId)")
MXLog.info("Finished editing timeline item: \(eventOrTransactionID)")
return .success(())
} 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))
}
}
@@ -366,10 +366,10 @@ final class TimelineProxy: TimelineProxyProtocol {
func sendMessage(_ message: String,
html: String?,
inReplyTo eventID: String? = nil,
inReplyToEventID: String? = nil,
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
if let eventID {
MXLog.info("Sending reply to eventID: \(eventID)")
if let inReplyToEventID {
MXLog.info("Sending reply to eventID: \(inReplyToEventID)")
} else {
MXLog.info("Sending message")
}
@@ -379,16 +379,16 @@ final class TimelineProxy: TimelineProxyProtocol {
intentionalMentions: intentionalMentions.toRustMentions())
do {
if let eventID {
try await timeline.sendReply(msg: messageContent, eventId: eventID)
MXLog.info("Finished sending reply to eventID: \(eventID)")
if let inReplyToEventID {
try await timeline.sendReply(msg: messageContent, eventId: inReplyToEventID)
MXLog.info("Finished sending reply to eventID: \(inReplyToEventID)")
} else {
_ = try await timeline.send(msg: messageContent)
MXLog.info("Finished sending message")
}
} catch {
if let eventID {
MXLog.error("Failed sending reply to eventID: \(eventID) with error: \(error)")
if let inReplyToEventID {
MXLog.error("Failed sending reply to eventID: \(inReplyToEventID) with error: \(error)")
} else {
MXLog.error("Failed sending message with error: \(error)")
}
@@ -631,4 +631,16 @@ extension Array where Element == TimelineItemProxy {
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 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,
reason: String?) async -> Result<Void, TimelineProxyError>
@@ -89,12 +90,11 @@ protocol TimelineProxyProtocol {
func sendMessage(_ message: String,
html: String?,
inReplyTo eventID: String?,
inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) 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 editPoll(original eventID: String,
@@ -112,14 +112,3 @@ protocol TimelineProxyProtocol {
html: String?,
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)
}
}