Bump the SDK and adopt the new MsgLike timeline item types (#4000)

* Bump the RustSDK to v25.04.09

* Adopt new MsgLike based timeline item structure.

* Move the `replyDetails` and `isThreaded` to the `RoomTimelineItemProperties`

* Restructure the TimelineItemFactory

* Fix line length warning

* Rename `msgLikeContent` to `messageLikeContent` wherever possible.

* Move the `EventTimelineItem` mocks to the SDK mocks folder.
This commit is contained in:
Stefan Ceriu
2025-04-10 14:04:16 +03:00
committed by GitHub
parent aaea51e3f1
commit 83136af7c4
49 changed files with 515 additions and 597 deletions
+5 -5
View File
@@ -247,7 +247,6 @@
2DD9D0FE7CB5CFC80D071451 /* AppLockScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */; };
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; };
2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; };
2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */; };
2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; };
2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; };
2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; };
@@ -972,6 +971,7 @@
BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; };
BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
BEC6DFEA506085D3027E353C /* MediaEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002399C6CB875C4EBB01CBC0 /* MediaEventsTimelineScreen.swift */; };
BED59052E5C5163D2B065CA6 /* EventTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */; };
BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; };
C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; };
C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
@@ -1417,6 +1417,7 @@
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; };
0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; };
0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -1491,7 +1492,6 @@
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = "<group>"; };
1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = "<group>"; };
1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; };
1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = "<group>"; };
1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetView.swift; sourceTree = "<group>"; };
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = "<group>"; };
@@ -3264,7 +3264,6 @@
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */,
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */,
9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */,
@@ -5841,6 +5840,7 @@
isa = PBXGroup;
children = (
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */,
0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */,
5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */,
);
path = SDK;
@@ -7179,7 +7179,7 @@
50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */,
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */,
02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */,
2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */,
BED59052E5C5163D2B065CA6 /* EventTimelineItem.swift in Sources */,
63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */,
36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */,
5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */,
@@ -8665,7 +8665,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = {
kind = exactVersion;
version = 25.04.08;
version = 25.04.09;
};
};
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
@@ -158,8 +158,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : {
"revision" : "ed17777f10cca2282b2331e6e61f0b1c281f52ea",
"version" : "25.4.8"
"revision" : "5c16b97c7753bd9b6512877b66b580ffebe4bc89",
"version" : "25.4.9"
}
},
{
@@ -13,7 +13,7 @@ struct EventTimelineItemSDKMockConfiguration {
var eventID: String = UUID().uuidString
var sender = ""
var isOwn = false
var content: TimelineItemContent = .redactedMessage
var content: TimelineItemContent = .msgLike(content: .init(kind: .redacted, reactions: [], threadRoot: nil, inReplyTo: nil))
}
extension EventTimelineItem {
@@ -26,7 +26,6 @@ extension EventTimelineItem {
isEditable: false,
content: configuration.content,
timestamp: 0,
reactions: [],
localSendState: nil,
localCreatedAt: nil,
readReceipts: [:],
@@ -39,12 +38,13 @@ extension EventTimelineItem {
let body = Lorem.sentences(Int.random(in: 1...5))
let messageType = MessageType.text(content: .init(body: body, formatted: nil))
let content = TimelineItemContent.message(content: .init(msgType: messageType,
body: body,
inReplyTo: nil,
let content = TimelineItemContent.msgLike(content: .init(kind: .message(content: .init(msgType: messageType,
body: body,
isEdited: false,
mentions: nil)),
reactions: [],
threadRoot: nil,
isEdited: false,
mentions: nil))
inReplyTo: nil))
return .init(configuration: .init(content: content))
}
@@ -154,7 +154,6 @@ struct TimelineMediaPreviewModifier_Previews: PreviewProvider {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Sally Sanderson"),
content: .init(filename: "Important document.pdf",
caption: "A caption goes right here.",
@@ -213,7 +213,6 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
isOutgoing: isOutgoing,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "@alice:matrix.org",
displayName: "Alice",
avatarURL: .mockMXCUserAvatar),
@@ -135,7 +135,6 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
isOutgoing: false,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "@alice:matrix.org",
displayName: "Alice",
avatarURL: .mockMXCUserAvatar),
@@ -45,7 +45,6 @@ struct AudioMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -54,7 +54,6 @@ struct FileMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -71,7 +71,6 @@ struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "image.jpg",
imageInfo: .mockImage,
@@ -82,7 +82,6 @@ struct VideoMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "video.mp4",
videoInfo: .mockVideo,
@@ -31,7 +31,6 @@ struct VoiceMessageMediaEventsTimelineView_Previews: PreviewProvider, TestablePr
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "audio.ogg",
duration: 300,
@@ -529,7 +529,7 @@ class TimelineInteractionHandler {
private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo {
switch item {
case let messageItem as EventBasedMessageTimelineItemProtocol:
return .init(type: .message(messageItem.contentType), isThread: messageItem.isThreaded)
return .init(type: .message(messageItem.contentType), isThread: messageItem.properties.isThreaded)
case let pollItem as PollRoomTimelineItem:
return .init(type: .poll(question: pollItem.poll.question), isThread: false)
default:
@@ -48,7 +48,7 @@ struct TimelineItemMenuActionProvider {
if item.canBeRepliedTo {
if let messageItem = item as? EventBasedMessageTimelineItemProtocol {
actions.append(.reply(isThread: messageItem.isThreaded))
actions.append(.reply(isThread: messageItem.properties.isThreaded))
} else {
actions.append(.reply(isThread: false))
}
@@ -24,7 +24,6 @@ struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: ""))
@@ -138,8 +138,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
} shouldStartAction: {
timelineItem.canBeRepliedTo
} action: {
let isThread = (timelineItem as? EventBasedMessageTimelineItemProtocol)?.isThreaded ?? false
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: .reply(isThread: isThread)))
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id,
action: .reply(isThread: timelineItem.properties.isThreaded)))
}
.contextMenu {
let provider = TimelineItemMenuActionProvider(timelineItem: timelineItem,
@@ -170,35 +170,33 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ViewBuilder
var contentWithReply: some View {
TimelineBubbleLayout(spacing: 8) {
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
if messageTimelineItem.isThreaded {
ThreadDecorator()
.padding(.leading, 4)
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
}
if timelineItem.properties.isThreaded {
ThreadDecorator()
.padding(.leading, 4)
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
}
if let replyDetails = timelineItem.properties.replyDetails {
// The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view.
if let replyDetails = messageTimelineItem.replyDetails {
// The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(8)
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
.onTapGesture {
context.send(viewAction: .focusOnEventID(replyDetails.eventID))
}
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
.hidden()
}
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(8)
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
.onTapGesture {
context.send(viewAction: .focusOnEventID(replyDetails.eventID))
}
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
.hidden()
}
content()
@@ -224,16 +222,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
private extension EventBasedTimelineItemProtocol {
var bubbleBackgroundColor: Color? {
let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming
switch self {
case let self as EventBasedMessageTimelineItemProtocol:
switch self {
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
// In case a reply detail or a thread decorator is present we render the color and the padding
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultColor : nil
default:
return defaultColor
}
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
// In case a reply detail or a thread decorator is present we render the color and the padding
return properties.replyDetails != nil || properties.isThreaded || hasMediaCaption ? defaultColor : nil
case is StickerRoomTimelineItem:
return nil
default:
@@ -251,30 +244,22 @@ private extension EventBasedTimelineItemProtocol {
return .zero
case is PollRoomTimelineItem:
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
case let self as EventBasedMessageTimelineItemProtocol:
switch self {
// In case a reply detail or a thread decorator is present we render the color and the padding
case is ImageRoomTimelineItem,
is VideoRoomTimelineItem:
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultInsets : .zero
case let locationTimelineItem as LocationRoomTimelineItem:
return locationTimelineItem.content.geoURI == nil ||
self.replyDetails != nil ||
self.isThreaded ? defaultInsets : .zero
default:
return defaultInsets
}
// In case a reply detail or a thread decorator is present we render the color and the padding
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
return properties.replyDetails != nil || properties.isThreaded || hasMediaCaption ? defaultInsets : .zero
case let locationTimelineItem as LocationRoomTimelineItem:
return locationTimelineItem.content.geoURI == nil ||
properties.replyDetails != nil ||
properties.isThreaded ? defaultInsets : .zero
default:
return defaultInsets
}
}
var contentCornerRadius: CGFloat {
guard let message = self as? EventBasedMessageTimelineItemProtocol else { return .zero }
switch message {
switch self {
case is ImageRoomTimelineItem, is VideoRoomTimelineItem, is LocationRoomTimelineItem:
return message.replyDetails != nil || message.isThreaded ? 8 : .zero
return properties.replyDetails != nil || properties.isThreaded ? 8 : .zero
default:
return .zero
}
@@ -386,25 +371,24 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))),
properties: .init(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short")))))),
groupStyle: .single))
let properties = RoomTimelineItemProperties(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Short message"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout."))))),
properties: properties),
groupStyle: .single))
}
.environmentObject(viewModel.context)
@@ -434,7 +418,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))),
@@ -445,7 +428,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
properties: RoomTimelineItemProperties(isEdited: true,
@@ -457,7 +439,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Short message"),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))),
@@ -468,7 +449,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Message goes Here"),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
@@ -479,7 +459,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "other.png",
imageInfo: .mockImage,
@@ -492,7 +471,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -500,7 +478,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil,
fileSize: nil,
contentType: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
properties: RoomTimelineItemProperties(isThreaded: true,
encryptionAuthenticity: .notGuaranteed(color: .gray))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
@@ -521,10 +500,9 @@ private struct MockTimelineContent: View {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: replyDetails),
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
@@ -532,7 +510,6 @@ private struct MockTimelineContent: View {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -540,14 +517,13 @@ private struct MockTimelineContent: View {
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "file.txt",
caption: "File",
@@ -555,50 +531,46 @@ private struct MockTimelineContent: View {
fileSize: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "image.jpg",
imageInfo: .mockImage,
thumbnailInfo: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
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"),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -606,7 +578,7 @@ private struct MockTimelineContent: View {
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails),
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
@@ -62,7 +62,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
@@ -85,7 +84,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
result.properties.deliveryStatus = .sending
@@ -105,7 +103,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
return result
@@ -116,7 +113,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test, content: .init(body: "house!"))
static let rtlString = TextRoomTimelineItem(id: .randomEvent,
@@ -124,7 +120,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test, content: .init(body: "באמת!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
@@ -132,7 +127,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "house! -- באמת‏! -- house!"))
@@ -141,7 +135,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "באמת‏! -- house! -- באמת!"))
@@ -150,7 +143,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "house! -- באמת!"))
@@ -159,7 +151,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "באמת‏! -- house!"))
@@ -108,7 +108,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
properties: .init(orderedReadReceipts: receipts))
}
@@ -49,7 +49,6 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -47,7 +47,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -132,7 +132,6 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -98,7 +98,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "image.jpg",
caption: caption,
@@ -98,7 +98,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description")))
@@ -107,7 +106,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
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")))
@@ -116,12 +114,12 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
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"),
replyDetails: .loaded(sender: .init(id: "Someone"),
eventID: "123",
eventContent: .message(.text(.init(body: "The thread content goes 'ere."))))))
properties: .init(replyDetails: .loaded(sender: .init(id: "Someone"),
eventID: "123",
eventContent: .message(.text(.init(body: "The thread content goes 'ere.")))),
isThreaded: true)))
}
}
@@ -59,7 +59,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -39,7 +39,6 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "1", displayName: "Bob"),
content: .init(body: "This is another message"))), groupStyle: .single))
@@ -51,7 +50,6 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Alice"),
content: .init(body: "This is a message"))), groupStyle: .single))
}
@@ -85,7 +85,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -99,7 +98,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: "", formattedBody: attributedString))
}
@@ -102,7 +102,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "video.mp4",
caption: caption,
@@ -67,7 +67,6 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "audio.ogg",
duration: 300,
@@ -24,27 +24,40 @@ struct RoomEventStringBuilder {
}
switch eventItemProxy.content {
case .unableToDecrypt(let encryptedMessage):
let errorMessage = switch encryptedMessage {
case .megolmV1AesSha2(_, .sentBeforeWeJoined): L10n.commonUnableToDecryptNoAccess
case .megolmV1AesSha2(_, .verificationViolation): L10n.commonUnableToDecryptVerificationViolation
case .megolmV1AesSha2(_, .unknownDevice), .megolmV1AesSha2(_, .unsignedDevice): L10n.commonUnableToDecryptInsecureDevice
default: L10n.commonWaitingForDecryptionKey
case .msgLike(let messageLikeContent):
switch messageLikeContent.kind {
case .message(let messageContent):
return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName)
case .sticker:
if messageEventStringBuilder.destination == .pinnedEvent {
var string = AttributedString(L10n.commonSticker)
string.bold()
return string
}
return prefix(L10n.commonSticker, with: displayName)
case .poll(let question, _, _, _, _, _, _):
if messageEventStringBuilder.destination == .pinnedEvent {
let questionPlaceholder = "{question}"
var finalString = AttributedString(L10n.commonPollSummary(questionPlaceholder))
finalString.bold()
let normalString = AttributedString(question)
finalString.replace(questionPlaceholder, with: normalString)
return finalString
}
return prefix(L10n.commonPollSummary(question), with: displayName)
case .redacted:
return prefix(L10n.commonMessageRemoved, with: displayName)
case .unableToDecrypt(let encryptedMessage):
let errorMessage = switch encryptedMessage {
case .megolmV1AesSha2(_, .sentBeforeWeJoined): L10n.commonUnableToDecryptNoAccess
case .megolmV1AesSha2(_, .verificationViolation): L10n.commonUnableToDecryptVerificationViolation
case .megolmV1AesSha2(_, .unknownDevice), .megolmV1AesSha2(_, .unsignedDevice): L10n.commonUnableToDecryptInsecureDevice
default: L10n.commonWaitingForDecryptionKey
}
return prefix(errorMessage, with: displayName)
}
return prefix(errorMessage, with: displayName)
case .redactedMessage:
return prefix(L10n.commonMessageRemoved, with: displayName)
case .sticker:
if messageEventStringBuilder.destination == .pinnedEvent {
var string = AttributedString(L10n.commonSticker)
string.bold()
return string
}
return prefix(L10n.commonSticker, with: displayName)
case .failedToParseMessageLike, .failedToParseState:
return prefix(L10n.commonUnsupportedEvent, with: displayName)
case .message(let messageContent):
return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName)
case .state(_, let state):
return stateEventStringBuilder
.buildString(for: state, sender: sender, isOutgoing: isOutgoing)
@@ -62,16 +75,6 @@ struct RoomEventStringBuilder {
member: sender.id,
memberIsYou: isOutgoing)
.map(AttributedString.init)
case .poll(let question, _, _, _, _, _, _):
if messageEventStringBuilder.destination == .pinnedEvent {
let questionPlaceholder = "{question}"
var finalString = AttributedString(L10n.commonPollSummary(questionPlaceholder))
finalString.bold()
let normalString = AttributedString(question)
finalString.replace(questionPlaceholder, with: normalString)
return finalString
}
return prefix(L10n.commonPollSummary(question), with: displayName)
case .callInvite:
return prefix(L10n.commonUnsupportedCall, with: displayName)
case .callNotify:
@@ -17,7 +17,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)),
@@ -27,7 +26,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗"),
properties: RoomTimelineItemProperties(reactions: [
@@ -39,7 +37,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/"),
properties: RoomTimelineItemProperties(reactions: [
@@ -59,7 +56,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
@@ -69,7 +65,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: .event(uniqueID: .init("RoomTimelineItemFixtures.default.5"),
@@ -78,7 +73,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Bob"),
content: .init(body: "New home office set up!"),
properties: RoomTimelineItemProperties(reactions: AggregatedReaction.mockReactions,
@@ -92,7 +86,6 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "",
formattedBody: AttributedStringBuilder(mentionBuilder: MentionBuilder())
@@ -276,7 +269,6 @@ private extension TextRoomTimelineItem {
isOutgoing: senderDisplayName == "Alice",
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: senderDisplayName),
content: .init(body: text))
}
@@ -295,7 +287,6 @@ private extension AudioRoomTimelineItem {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
content: .init(filename: "audio.mp3",
caption: caption,
@@ -314,7 +305,6 @@ private extension FileRoomTimelineItem {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
content: .init(filename: "file.pdf",
caption: caption,
@@ -332,7 +322,6 @@ private extension ImageRoomTimelineItem {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
content: .init(filename: "image.jpg",
caption: caption,
@@ -349,7 +338,6 @@ private extension VideoRoomTimelineItem {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
content: .init(filename: "video.mp4",
caption: caption,
@@ -366,7 +354,6 @@ private extension VoiceMessageRoomTimelineItem {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: isOutgoing ? "@alice:matrix.org" : "@bob:matrix.org"),
content: .init(filename: "message.ogg",
duration: 10,
@@ -493,7 +493,7 @@ class TimelineController: TimelineControllerProtocol {
return
}
switch timelineItem.replyDetails {
switch timelineItem.properties.replyDetails {
case .notLoaded:
activeTimeline.fetchDetails(for: eventID)
case .error:
@@ -9,6 +9,10 @@ import Foundation
/// Properties of a matrix event that are common between all timeline items.
struct RoomTimelineItemProperties: Hashable {
// Information on the item this one replying to
var replyDetails: TimelineItemReplyDetails?
// Whether it's part of a thread or not
var isThreaded = false
/// Whether the item has been edited.
var isEdited = false
/// The aggregated reactions that have been sent for this item.
@@ -123,8 +123,6 @@ class EventTimelineItemProxy {
avatarURL: nil)
}
}()
lazy var reactions = item.reactions
lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000))
@@ -20,9 +20,7 @@ enum EventBasedMessageTimelineItemContentType: Hashable {
}
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
var replyDetails: TimelineItemReplyDetails? { get }
var contentType: EventBasedMessageTimelineItemContentType { get }
var isThreaded: Bool { get }
}
extension EventBasedMessageTimelineItemProtocol {
@@ -13,14 +13,11 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
var shouldBoost = false
let sender: TimelineItemSender
let content: AudioRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
@@ -13,14 +13,11 @@ struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
let sender: TimelineItemSender
let content: EmoteRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -14,15 +14,12 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
var shouldBoost = false
let sender: TimelineItemSender
let content: FileRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -14,15 +14,12 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
var shouldBoost = false
let sender: TimelineItemSender
let content: ImageRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -9,12 +9,10 @@ import Foundation
struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
let sender: TimelineItemSender
@@ -24,8 +22,6 @@ struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatabl
content.body
}
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var contentType: EventBasedMessageTimelineItemContentType {
@@ -13,14 +13,11 @@ struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
let sender: TimelineItemSender
let content: NoticeRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -9,20 +9,16 @@ import UIKit
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
var shouldBoost = false
let sender: TimelineItemSender
let content: TextRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -14,15 +14,12 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
var shouldBoost = false
let sender: TimelineItemSender
let content: VideoRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
var body: String {
@@ -13,12 +13,10 @@ struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equa
let isOutgoing: Bool
let isEditable: Bool
let canBeRepliedTo: Bool
let isThreaded: Bool
let sender: TimelineItemSender
let content: AudioRoomTimelineItemContent
var replyDetails: TimelineItemReplyDetails?
var properties = RoomTimelineItemProperties()
@@ -28,18 +28,23 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
let isOutgoing = eventItemProxy.isOwn
switch eventItemProxy.content {
case .unableToDecrypt(let encryptedMessage):
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing)
case .redactedMessage:
return buildRedactedTimelineItem(eventItemProxy, isOutgoing)
case .sticker(let body, let imageInfo, let mediaSource):
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, mediaSource, isOutgoing)
case .msgLike(let messageLikeContent):
switch messageLikeContent.kind {
case .message(let messageContent):
return buildMessageTimelineItem(eventItemProxy, messageLikeContent, messageContent, isOutgoing)
case .sticker(let body, let imageInfo, let mediaSource):
return buildStickerTimelineItem(eventItemProxy, messageLikeContent, body, imageInfo, mediaSource, isOutgoing)
case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime, let edited):
return buildPollTimelineItem(eventItemProxy, messageLikeContent, question, kind, maxSelections, answers, votes, endTime, isOutgoing, edited)
case .redacted:
return buildRedactedTimelineItem(eventItemProxy, messageLikeContent, isOutgoing)
case .unableToDecrypt(let encryptedMessage):
return buildEncryptedTimelineItem(eventItemProxy, messageLikeContent, encryptedMessage, isOutgoing)
}
case .failedToParseMessageLike(let eventType, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .failedToParseState(let eventType, _, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .message(let messageContent):
return buildMessageTimelineItem(eventItemProxy, messageContent, isOutgoing)
case .state(_, let content):
if isDM, content == .roomCreate {
return nil
@@ -57,8 +62,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
avatarURLString: avatarUrl,
previousAvatarURLString: prevAvatarUrl,
isOutgoing: isOutgoing)
case .poll(question: let question, kind: let kind, maxSelections: let maxSelections, answers: let answers, votes: let votes, endTime: let endTime, let edited):
return buildPollTimelineItem(question, kind, maxSelections, answers, votes, endTime, eventItemProxy, isOutgoing, edited)
case .callInvite:
return buildCallInviteTimelineItem(for: eventItemProxy)
case .callNotify:
@@ -66,52 +69,234 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
}
// MARK: - Message Events
// MARK: - MsgLike Events
private func buildMessageTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ messageContent: MessageContent, _ isOutgoing: Bool) -> RoomTimelineItemProtocol? {
private func buildMessageTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol? {
switch messageContent.msgType {
case .text(content: let textMessageContent):
return buildTextTimelineItem(for: eventItemProxy, messageContent, textMessageContent, isOutgoing)
return buildTextTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, textMessageContent, isOutgoing)
case .image(content: let imageMessageContent):
return buildImageTimelineItem(for: eventItemProxy, messageContent, imageMessageContent, isOutgoing)
return buildImageTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, imageMessageContent, isOutgoing)
case .video(let videoMessageContent):
return buildVideoTimelineItem(for: eventItemProxy, messageContent, videoMessageContent, isOutgoing)
return buildVideoTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, videoMessageContent, isOutgoing)
case .file(let fileMessageContent):
return buildFileTimelineItem(for: eventItemProxy, messageContent, fileMessageContent, isOutgoing)
return buildFileTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, fileMessageContent, isOutgoing)
case .notice(content: let noticeMessageContent):
return buildNoticeTimelineItem(for: eventItemProxy, messageContent, noticeMessageContent, isOutgoing)
return buildNoticeTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, noticeMessageContent, isOutgoing)
case .emote(content: let emoteMessageContent):
return buildEmoteTimelineItem(for: eventItemProxy, messageContent, emoteMessageContent, isOutgoing)
return buildEmoteTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, emoteMessageContent, isOutgoing)
case .audio(let audioMessageContent):
if audioMessageContent.voice != nil {
return buildVoiceTimelineItem(for: eventItemProxy, messageContent, audioMessageContent, isOutgoing)
return buildVoiceTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, audioMessageContent, isOutgoing)
} else {
return buildAudioTimelineItem(for: eventItemProxy, messageContent, audioMessageContent, isOutgoing)
return buildAudioTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, audioMessageContent, isOutgoing)
}
case .location(let locationMessageContent):
return buildLocationTimelineItem(for: eventItemProxy, messageContent, locationMessageContent, isOutgoing)
return buildLocationTimelineItem(for: eventItemProxy, messageLikeContent, messageContent, locationMessageContent, isOutgoing)
case .other:
return nil
}
}
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ eventType: String,
_ error: String,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
body: L10n.commonUnsupportedEvent,
eventType: eventType,
error: error,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties())
private func buildTextTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ textMessageContent: TextMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
TextRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildTextTimelineItemContent(textMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ imageMessageContent: ImageMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
ImageRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildImageTimelineItemContent(imageMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ videoMessageContent: VideoMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
VideoRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildVideoTimelineItemContent(videoMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ audioMessageContent: AudioMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
AudioRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildAudioTimelineItemContent(audioMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildVoiceTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ audioMessageContent: AudioMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
VoiceMessageRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
content: buildAudioTimelineItemContent(audioMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ fileMessageContent: FileMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
FileRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildFileTimelineItemContent(fileMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ noticeMessageContent: NoticeMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
NoticeRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
content: buildNoticeTimelineItemContent(noticeMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ emoteMessageContent: EmoteMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
EmoteRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: emoteMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ messageContent: MessageContent,
_ locationMessageContent: LocationContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
LocationRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
content: buildLocationTimelineItemContent(locationMessageContent),
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: messageContent.isEdited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ body: String,
_ info: MatrixRustSDK.ImageInfo,
_ mediaSource: MediaSource,
@@ -127,13 +312,89 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
sender: eventItemProxy.sender,
imageInfo: imageInfo,
blurhash: info.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildPollTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ question: String,
_ pollKind: PollKind,
_ maxSelections: UInt64,
_ answers: [PollAnswer],
_ votes: [String: [String]],
_ endTime: UInt64?,
_ isOutgoing: Bool,
_ edited: Bool) -> RoomTimelineItemProtocol {
let allVotes = votes.reduce(0) { count, pair in
count + pair.value.count
}
let maxOptionVotes = votes.map(\.value.count).max()
let options = answers.map { answer in
let optionVotesCount = votes[answer.id]?.count
return Poll.Option(id: answer.id,
text: answer.text,
votes: optionVotesCount ?? 0,
allVotes: allVotes,
isSelected: votes[answer.id]?.contains(userID) ?? false,
isWinning: optionVotesCount.map { $0 == maxOptionVotes } ?? false)
}
let pollKind: Poll.Kind = switch pollKind {
case .disclosed:
.disclosed
case .undisclosed:
.undisclosed
}
let poll = Poll(question: question,
kind: pollKind,
maxSelections: Int(maxSelections),
options: options,
votes: votes,
endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) },
createdByAccountOwner: eventItemProxy.sender.id == userID)
return PollRoomTimelineItem(id: eventItemProxy.id,
poll: poll,
body: poll.question,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil,
isEdited: edited,
reactions: buildAggregatedReactions(messageLikeContent.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState)))
}
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
RedactedRoomTimelineItem(id: eventItemProxy.id,
body: L10n.commonMessageRemoved,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil))
}
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ messageLikeContent: MsgLikeContent,
_ encryptedMessage: EncryptedMessage,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
@@ -180,313 +441,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties())
}
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
RedactedRoomTimelineItem(id: eventItemProxy.id,
body: L10n.commonMessageRemoved,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties())
}
private func buildTextTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ textMessageContent: TextMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
TextRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildTextTimelineItemContent(textMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ imageMessageContent: ImageMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
ImageRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildImageTimelineItemContent(imageMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ videoMessageContent: VideoMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
VideoRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildVideoTimelineItemContent(videoMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ audioMessageContent: AudioMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
AudioRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildAudioTimelineItemContent(audioMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildVoiceTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ audioMessageContent: AudioMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
VoiceMessageRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
sender: eventItemProxy.sender,
content: buildAudioTimelineItemContent(audioMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ fileMessageContent: FileMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
FileRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
shouldBoost: eventItemProxy.shouldBoost,
sender: eventItemProxy.sender,
content: buildFileTimelineItemContent(fileMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ noticeMessageContent: NoticeMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
NoticeRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
sender: eventItemProxy.sender,
content: buildNoticeTimelineItemContent(noticeMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ emoteMessageContent: EmoteMessageContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
EmoteRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
sender: eventItemProxy.sender,
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: emoteMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
_ messageContent: MessageContent,
_ locationMessageContent: LocationContent,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
LocationRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
isThreaded: messageContent.threadRoot != nil,
sender: eventItemProxy.sender,
content: buildLocationTimelineItemContent(locationMessageContent),
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageContent.inReplyTo),
properties: RoomTimelineItemProperties(isEdited: messageContent.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildPollTimelineItem(_ question: String,
_ pollKind: PollKind,
_ maxSelections: UInt64,
_ answers: [PollAnswer],
_ votes: [String: [String]],
_ endTime: UInt64?,
_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ edited: Bool) -> RoomTimelineItemProtocol {
let allVotes = votes.reduce(0) { count, pair in
count + pair.value.count
}
let maxOptionVotes = votes.map(\.value.count).max()
let options = answers.map { answer in
let optionVotesCount = votes[answer.id]?.count
return Poll.Option(id: answer.id,
text: answer.text,
votes: optionVotesCount ?? 0,
allVotes: allVotes,
isSelected: votes[answer.id]?.contains(userID) ?? false,
isWinning: optionVotesCount.map { $0 == maxOptionVotes } ?? false)
}
let poll = Poll(question: question,
kind: .init(pollKind: pollKind),
maxSelections: Int(maxSelections),
options: options,
votes: votes,
endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) },
createdByAccountOwner: eventItemProxy.sender.id == userID)
return PollRoomTimelineItem(id: eventItemProxy.id,
poll: poll,
body: poll.question,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties(isEdited: edited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildCallInviteTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol {
CallInviteRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender)
}
private func buildCallNotificationTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol {
CallNotificationRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender)
}
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in
let senders = reaction.senders
.map { senderData in
ReactionSender(id: senderData.senderId, timestamp: Date(timeIntervalSince1970: TimeInterval(senderData.timestamp / 1000)))
}
.sorted { a, b in
// Sort reactions within an aggregation by timestamp descending.
// This puts the most recent at the top, useful in cases like the
// reaction summary view.
a.timestamp > b.timestamp
}
return AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: senders)
}
.sorted { a, b in
// Sort aggregated reactions by count and then timestamp ascending, using
// the most recent reaction in the aggregation(hence index 0).
// This appends new aggregations on the end of the reaction layout
// and the deterministic sort avoids reactions jumping around if the reactions timeline
// view reloads.
if a.count == b.count {
return a.senders[0].timestamp < b.senders[0].timestamp
}
return a.count > b.count
}
}
private func orderReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] {
receipts
.sorted { firstElement, secondElement in
// If there is no timestamp we order them as last
let firstTimestamp = firstElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
let secondTimestamp = secondElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
return firstTimestamp > secondTimestamp
}
.map { key, receipt in
ReadReceipt(userID: key, formattedTimestamp: receipt.dateTimestamp?.formattedMinimal())
}
}
private func authenticity(_ shieldState: ShieldState?) -> EncryptionAuthenticity? {
shieldState.flatMap(EncryptionAuthenticity.init)
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
isThreaded: messageLikeContent.threadRoot != nil))
}
// MARK: - Message events content
@@ -626,6 +582,86 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return finalString
}
// MARK: - EventBasedTimelineItem Properties
private func buildAggregatedReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
reactions.map { reaction in
let senders = reaction.senders
.map { senderData in
ReactionSender(id: senderData.senderId, timestamp: Date(timeIntervalSince1970: TimeInterval(senderData.timestamp / 1000)))
}
.sorted { a, b in
// Sort reactions within an aggregation by timestamp descending.
// This puts the most recent at the top, useful in cases like the
// reaction summary view.
a.timestamp > b.timestamp
}
return AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: senders)
}
.sorted { a, b in
// Sort aggregated reactions by count and then timestamp ascending, using
// the most recent reaction in the aggregation(hence index 0).
// This appends new aggregations on the end of the reaction layout
// and the deterministic sort avoids reactions jumping around if the reactions timeline
// view reloads.
if a.count == b.count {
return a.senders[0].timestamp < b.senders[0].timestamp
}
return a.count > b.count
}
}
private func buildOrderedReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] {
receipts
.sorted { firstElement, secondElement in
// If there is no timestamp we order them as last
let firstTimestamp = firstElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
let secondTimestamp = secondElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
return firstTimestamp > secondTimestamp
}
.map { key, receipt in
ReadReceipt(userID: key, formattedTimestamp: receipt.dateTimestamp?.formattedMinimal())
}
}
private func buildEncryptionAuthenticity(_ shieldState: ShieldState?) -> EncryptionAuthenticity? {
shieldState.flatMap(EncryptionAuthenticity.init)
}
// MARK: - Other Events
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ eventType: String,
_ error: String,
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
body: L10n.commonUnsupportedEvent,
eventType: eventType,
error: error,
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender,
properties: .init())
}
private func buildCallInviteTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol {
CallInviteRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender)
}
private func buildCallNotificationTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol {
CallNotificationRoomTimelineItem(id: eventItemProxy.id,
timestamp: eventItemProxy.timestamp,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
sender: eventItemProxy.sender)
}
// MARK: - State Events
private func buildStateTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -696,14 +732,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
let replyContent: TimelineEventContent
switch timelineItem {
case .message(let messageContent):
return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId(), messageType: messageContent.msgType), isThreaded: isThreaded)
case .poll(let question, _, _, _, _, _, _):
replyContent = .poll(question: question)
case .sticker(let body, _, _):
replyContent = .message(.text(.init(body: body)))
case .redactedMessage:
replyContent = .redacted
case .msgLike(let messageLikeContent):
switch messageLikeContent.kind {
case .message(let messageContent):
return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId(), messageType: messageContent.msgType), isThreaded: isThreaded)
case .poll(let question, _, _, _, _, _, _):
replyContent = .poll(question: question)
case .sticker(let body, _, _):
replyContent = .message(.text(.init(body: body)))
case .redacted:
replyContent = .redacted
default:
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
}
default:
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
}
@@ -756,26 +797,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
}
extension Poll.Kind {
init(pollKind: MatrixRustSDK.PollKind) {
switch pollKind {
case .disclosed:
self = .disclosed
case .undisclosed:
self = .undisclosed
}
}
}
private extension RepliedToEventDetails {
var isThreaded: Bool {
switch self {
case .ready(let content, _, _):
guard case let .message(content) = content else {
guard case let .msgLike(messageLikeContent) = content else {
return false
}
return content.threadRoot != nil
return messageLikeContent.threadRoot != nil
default:
return false
}
@@ -77,7 +77,8 @@ final class TimelineProxy: TimelineProxyProtocol {
func messageEventContent(for timelineItemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? {
guard let content = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content,
case let .message(messageContent) = content else {
case let .msgLike(messageLikeContent) = content,
case let .message(messageContent) = messageLikeContent.kind else {
return nil
}
-6
View File
@@ -121,7 +121,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
let noticeAttributedString = "NoticeAttributed"
@@ -130,7 +129,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
let emoteAttributedString = "EmoteAttributed"
@@ -139,7 +137,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
let imageMessage = ImageRoomTimelineItem(id: .randomEvent,
@@ -147,7 +144,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(filename: "ImageString",
caption: "ImageString",
@@ -158,7 +154,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(filename: "VideoString",
caption: "VideoString",
@@ -169,7 +164,6 @@ class LoggingTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "sender"),
content: .init(filename: "FileString",
caption: "FileString",
@@ -16,7 +16,6 @@ final class TextBasedRoomTimelineTests: XCTestCase {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString),
content: .init(body: "Test"))
XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + 1)
@@ -29,7 +28,6 @@ final class TextBasedRoomTimelineTests: XCTestCase {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString),
content: .init(body: "Test"))
XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + 1)
@@ -42,7 +40,6 @@ final class TextBasedRoomTimelineTests: XCTestCase {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString),
content: .init(body: "Test"))
timelineItem.properties.isEdited = true
@@ -57,7 +54,6 @@ final class TextBasedRoomTimelineTests: XCTestCase {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString),
content: .init(body: "Test"))
timelineItem.properties.isEdited = true
@@ -300,7 +300,6 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Sally Sanderson"),
content: .init(filename: "Amazing image.jpeg",
caption: "A caption goes right here.",
@@ -312,7 +311,6 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(filename: "Super video.mp4",
videoInfo: .mockVideo,
@@ -323,7 +321,6 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(filename: "Important file.pdf",
source: try? .init(url: .mockMXCFile, mimeType: "document/pdf"),
@@ -553,7 +553,6 @@ private extension TextRoomTimelineItem {
isOutgoing: sender == "bob",
isEditable: sender == "bob",
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "@\(sender):server.com", displayName: sender),
content: .init(body: text),
properties: RoomTimelineItemProperties(reactions: reactions, orderedReadReceipts: addReadReceipts))
@@ -573,7 +572,6 @@ private extension TextRoomTimelineItem {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: "Hello, World!"))
}
+1 -1
View File
@@ -59,7 +59,7 @@ packages:
# Element/Matrix dependencies
MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 25.04.08
exactVersion: 25.04.09
# path: ../matrix-rust-sdk
Compound:
url: https://github.com/element-hq/compound-ios