diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1143cb399..e397f3c4b 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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 = ""; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; + 0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = ""; }; 0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = ""; }; 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = ""; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = ""; }; @@ -1491,7 +1492,6 @@ 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURITests.swift; sourceTree = ""; }; 1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = ""; }; - 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = ""; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetView.swift; sourceTree = ""; }; 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = ""; }; @@ -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" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e8ca155d3..ed9e95e34 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { diff --git a/ElementX/Sources/Mocks/EventTimelineItem.swift b/ElementX/Sources/Mocks/SDK/EventTimelineItem.swift similarity index 79% rename from ElementX/Sources/Mocks/EventTimelineItem.swift rename to ElementX/Sources/Mocks/SDK/EventTimelineItem.swift index d7e7ed801..5ce6b2d9c 100644 --- a/ElementX/Sources/Mocks/EventTimelineItem.swift +++ b/ElementX/Sources/Mocks/SDK/EventTimelineItem.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift index 801e9132a..fb784ea24 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift @@ -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.", diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift index 766ade40f..1cfe2d947 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift @@ -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), diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift index 6e3f36ccb..f6263ea05 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift @@ -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), diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/AudioFileEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/AudioFileEventsTimelineView.swift index bd28e9c31..ee8f71683 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/AudioFileEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/AudioFileEventsTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/FileMediaEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/FileMediaEventsTimelineView.swift index a8998bf5e..466b20ec5 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/FileMediaEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/FileMediaEventsTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift index 47472a2bb..6208bbc55 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VideoMediaEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VideoMediaEventsTimelineView.swift index aba34d9c6..80cc1a8cb 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VideoMediaEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VideoMediaEventsTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VoiceMessageMediaEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VoiceMessageMediaEventsTimelineView.swift index 5be5faadd..1e6f30abf 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VoiceMessageMediaEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/VoiceMessageMediaEventsTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 818d941e7..5c7bfbb34 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -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: diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 99519ccbd..8c779bbc0 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift index 56ec58f82..33b8bf686 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift @@ -24,7 +24,6 @@ struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview { isOutgoing: true, isEditable: true, canBeRepliedTo: true, - isThreaded: false, sender: .init(id: ""), content: .init(body: "")) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 86b570836..033f6f994 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -138,8 +138,8 @@ struct TimelineItemBubbledStylerView: 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: 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: 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, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index 64379cc79..7c2822830 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -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!")) diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index ae8be5954..05e803b81 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift index c538a5b80..481ecc2d2 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift index 5292df404..a58104560 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift index 5726ae9fa..e2a0ad9ea 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift index af14fd8bc..6e93ef848 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift index 1a9717d12..465bea9ae 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -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))) } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift index 7090ee3ed..a3dc6ee54 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift index 3d13d5b93..927364454 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift index 746071d95..b4ce33668 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift @@ -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)) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift index 88ddcea90..8955b9251 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift index ea39a87a5..461d5ec5b 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift @@ -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, diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index 50043c554..58a5ef043 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -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: diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index 39dddc90d..e258d2851 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -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, diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 60909442c..756991c92 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -493,7 +493,7 @@ class TimelineController: TimelineControllerProtocol { return } - switch timelineItem.replyDetails { + switch timelineItem.properties.replyDetails { case .notLoaded: activeTimeline.fetchDetails(for: eventID) case .error: diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift index 7bbda546c..5b81f55c6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift @@ -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. diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index a568aa252..7657e6e11 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -123,8 +123,6 @@ class EventTimelineItemProxy { avatarURL: nil) } }() - - lazy var reactions = item.reactions lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000)) diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index 4e7cb2332..f8ffb80af 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -20,9 +20,7 @@ enum EventBasedMessageTimelineItemContentType: Hashable { } protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { - var replyDetails: TimelineItemReplyDetails? { get } var contentType: EventBasedMessageTimelineItemContentType { get } - var isThreaded: Bool { get } } extension EventBasedMessageTimelineItemProtocol { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift index 037f41515..e54e11631 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift @@ -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() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift index 496cf9049..c4af9a8ad 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift index 289d15646..78c749e4c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift index 0afcdf44d..fc6317ff6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift index 052b3432a..2c0b2e77b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift index 28de96674..6c423cd85 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift index ecc020a7c..b6dc99aad 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index b7044c110..ebf448b79 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift index 900f61908..11b7fe546 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift @@ -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() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index ac44c0456..7a13d68bb 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -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 } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 536cd73ed..fa3cc8471 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -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 } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 88b454750..ebbe75a14 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -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", diff --git a/UnitTests/Sources/TextBasedRoomTimelineTests.swift b/UnitTests/Sources/TextBasedRoomTimelineTests.swift index 9449d956c..ab132bbfa 100644 --- a/UnitTests/Sources/TextBasedRoomTimelineTests.swift +++ b/UnitTests/Sources/TextBasedRoomTimelineTests.swift @@ -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 diff --git a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift index 35b54abae..0958198a7 100644 --- a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift +++ b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift @@ -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"), diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 1f7cc2617..4920e6256 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -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!")) } diff --git a/project.yml b/project.yml index b17230dcd..947e76ce7 100644 --- a/project.yml +++ b/project.yml @@ -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