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

* Bump the RustSDK to v25.04.09

* Adopt new MsgLike based timeline item structure.

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

* Restructure the TimelineItemFactory

* Fix line length warning

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

* Move the `EventTimelineItem` mocks to the SDK mocks folder.
This commit is contained in:
Stefan Ceriu
2025-04-10 14:04:16 +03:00
committed by GitHub
parent aaea51e3f1
commit 83136af7c4
49 changed files with 515 additions and 597 deletions
@@ -48,7 +48,7 @@ struct TimelineItemMenuActionProvider {
if item.canBeRepliedTo {
if let messageItem = item as? EventBasedMessageTimelineItemProtocol {
actions.append(.reply(isThread: messageItem.isThreaded))
actions.append(.reply(isThread: messageItem.properties.isThreaded))
} else {
actions.append(.reply(isThread: false))
}
@@ -24,7 +24,6 @@ struct SwipeToReplyView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: ""),
content: .init(body: ""))
@@ -138,8 +138,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
} shouldStartAction: {
timelineItem.canBeRepliedTo
} action: {
let isThread = (timelineItem as? EventBasedMessageTimelineItemProtocol)?.isThreaded ?? false
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: .reply(isThread: isThread)))
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id,
action: .reply(isThread: timelineItem.properties.isThreaded)))
}
.contextMenu {
let provider = TimelineItemMenuActionProvider(timelineItem: timelineItem,
@@ -170,35 +170,33 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ViewBuilder
var contentWithReply: some View {
TimelineBubbleLayout(spacing: 8) {
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
if messageTimelineItem.isThreaded {
ThreadDecorator()
.padding(.leading, 4)
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
}
if timelineItem.properties.isThreaded {
ThreadDecorator()
.padding(.leading, 4)
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
}
if let replyDetails = timelineItem.properties.replyDetails {
// The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view.
if let replyDetails = messageTimelineItem.replyDetails {
// The rendered reply bubble with a greedy width. The custom layout prevents
// the infinite width from increasing the overall width of the view.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(8)
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
.onTapGesture {
context.send(viewAction: .focusOnEventID(replyDetails.eventID))
}
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
.hidden()
}
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.compound.bgCanvasDefault)
.cornerRadius(8)
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
.onTapGesture {
context.send(viewAction: .focusOnEventID(replyDetails.eventID))
}
// Add a fixed width reply bubble that is used for layout calculations but won't be rendered.
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
.fixedSize(horizontal: false, vertical: true)
.padding(4.0)
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
.hidden()
}
content()
@@ -224,16 +222,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
private extension EventBasedTimelineItemProtocol {
var bubbleBackgroundColor: Color? {
let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming
switch self {
case let self as EventBasedMessageTimelineItemProtocol:
switch self {
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
// In case a reply detail or a thread decorator is present we render the color and the padding
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultColor : nil
default:
return defaultColor
}
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
// In case a reply detail or a thread decorator is present we render the color and the padding
return properties.replyDetails != nil || properties.isThreaded || hasMediaCaption ? defaultColor : nil
case is StickerRoomTimelineItem:
return nil
default:
@@ -251,30 +244,22 @@ private extension EventBasedTimelineItemProtocol {
return .zero
case is PollRoomTimelineItem:
return .init(top: 12, leading: 12, bottom: 4, trailing: 12)
case let self as EventBasedMessageTimelineItemProtocol:
switch self {
// In case a reply detail or a thread decorator is present we render the color and the padding
case is ImageRoomTimelineItem,
is VideoRoomTimelineItem:
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultInsets : .zero
case let locationTimelineItem as LocationRoomTimelineItem:
return locationTimelineItem.content.geoURI == nil ||
self.replyDetails != nil ||
self.isThreaded ? defaultInsets : .zero
default:
return defaultInsets
}
// In case a reply detail or a thread decorator is present we render the color and the padding
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
return properties.replyDetails != nil || properties.isThreaded || hasMediaCaption ? defaultInsets : .zero
case let locationTimelineItem as LocationRoomTimelineItem:
return locationTimelineItem.content.geoURI == nil ||
properties.replyDetails != nil ||
properties.isThreaded ? defaultInsets : .zero
default:
return defaultInsets
}
}
var contentCornerRadius: CGFloat {
guard let message = self as? EventBasedMessageTimelineItemProtocol else { return .zero }
switch message {
switch self {
case is ImageRoomTimelineItem, is VideoRoomTimelineItem, is LocationRoomTimelineItem:
return message.replyDetails != nil || message.isThreaded ? 8 : .zero
return properties.replyDetails != nil || properties.isThreaded ? 8 : .zero
default:
return .zero
}
@@ -386,25 +371,24 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short"))))),
properties: .init(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "Short")))))),
groupStyle: .single))
let properties = RoomTimelineItemProperties(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .randomEvent,
timestamp: .mock,
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Short message"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventID: "123",
eventContent: .message(.text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout."))))),
properties: properties),
groupStyle: .single))
}
.environmentObject(viewModel.context)
@@ -434,7 +418,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unsignedDevice(color: .red))),
@@ -445,7 +428,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
properties: RoomTimelineItemProperties(isEdited: true,
@@ -457,7 +439,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Short message"),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .unknownDevice(color: .red))),
@@ -468,7 +449,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Message goes Here"),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
@@ -479,7 +459,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "other.png",
imageInfo: .mockImage,
@@ -492,7 +471,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -500,7 +478,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil,
fileSize: nil,
contentType: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
properties: RoomTimelineItemProperties(isThreaded: true,
encryptionAuthenticity: .notGuaranteed(color: .gray))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
@@ -521,10 +500,9 @@ private struct MockTimelineContent: View {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: replyDetails),
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
@@ -532,7 +510,6 @@ private struct MockTimelineContent: View {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -540,14 +517,13 @@ private struct MockTimelineContent: View {
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "file.txt",
caption: "File",
@@ -555,50 +531,46 @@ private struct MockTimelineContent: View {
fileSize: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: true,
isEditable: true,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "image.jpg",
imageInfo: .mockImage,
thumbnailInfo: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782,
longitude: 12.496366),
description: "Location description description description description description description description description"),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: replyDetails))
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
timestamp: .mock,
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: isThreaded,
sender: .init(id: ""),
content: .init(filename: "audio.ogg",
duration: 100,
@@ -606,7 +578,7 @@ private struct MockTimelineContent: View {
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails),
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
title: L10n.commonVoiceMessage,
duration: 10,
@@ -62,7 +62,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
@@ -85,7 +84,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
result.properties.deliveryStatus = .sending
@@ -105,7 +103,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "Test"))
return result
@@ -116,7 +113,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test, content: .init(body: "house!"))
static let rtlString = TextRoomTimelineItem(id: .randomEvent,
@@ -124,7 +120,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test, content: .init(body: "באמת!"))
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
@@ -132,7 +127,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "house! -- באמת‏! -- house!"))
@@ -141,7 +135,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "באמת‏! -- house! -- באמת!"))
@@ -150,7 +143,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "house! -- באמת!"))
@@ -159,7 +151,6 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .test,
content: .init(body: "באמת‏! -- house!"))
@@ -108,7 +108,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
properties: .init(orderedReadReceipts: receipts))
}
@@ -49,7 +49,6 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -47,7 +47,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -132,7 +132,6 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
@@ -98,7 +98,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "image.jpg",
caption: caption,
@@ -98,7 +98,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description")))
@@ -107,7 +106,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description")))
@@ -116,12 +114,12 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: true,
sender: .init(id: "Bob"),
content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"),
replyDetails: .loaded(sender: .init(id: "Someone"),
eventID: "123",
eventContent: .message(.text(.init(body: "The thread content goes 'ere."))))))
properties: .init(replyDetails: .loaded(sender: .init(id: "Someone"),
eventID: "123",
eventContent: .message(.text(.init(body: "The thread content goes 'ere.")))),
isThreaded: true)))
}
}
@@ -59,7 +59,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -39,7 +39,6 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "1", displayName: "Bob"),
content: .init(body: "This is another message"))), groupStyle: .single))
@@ -51,7 +50,6 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "", displayName: "Alice"),
content: .init(body: "This is a message"))), groupStyle: .single))
}
@@ -85,7 +85,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: text))
}
@@ -99,7 +98,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: isOutgoing,
isEditable: isOutgoing,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: senderId),
content: .init(body: "", formattedBody: attributedString))
}
@@ -102,7 +102,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "video.mp4",
caption: caption,
@@ -67,7 +67,6 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "audio.ogg",
duration: 300,