element-hq/element-x-ios/issues/2636 - Expose paths for focusing replied-to timeline items by tapping on an in-reply-to message bubble
This commit is contained in:
committed by
Stefan Ceriu
parent
ae76bf8eca
commit
3214106909
@@ -199,18 +199,32 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static let replyTypes: [TimelineItemReplyDetails] = [
|
||||
.loaded(sender: .init(id: "Dave"), eventContent: .message(.audio(.init(body: "Audio: Ride the lightning", duration: 100, waveform: nil, source: nil, contentType: nil)))),
|
||||
.loaded(sender: .init(id: "James"), eventContent: .message(.emote(.init(body: "Emote: James thinks he's the phantom lord")))),
|
||||
.loaded(sender: .init(id: "Robert"), eventContent: .message(.file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil)))),
|
||||
.loaded(sender: .init(id: "Cliff"), eventContent: .message(.image(.init(body: "Image: Pushead",
|
||||
source: .init(url: .picturesDirectory, mimeType: nil),
|
||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
|
||||
.loaded(sender: .init(id: "Jason"), eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))),
|
||||
.loaded(sender: .init(id: "Kirk"), eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
|
||||
.loaded(sender: .init(id: "Lars"), eventContent: .message(.video(.init(body: "Video: Through the never",
|
||||
duration: 100,
|
||||
source: nil,
|
||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
|
||||
.loaded(sender: .init(id: "Dave"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.audio(.init(body: "Audio: Ride the lightning", duration: 100, waveform: nil, source: nil, contentType: nil)))),
|
||||
.loaded(sender: .init(id: "James"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.emote(.init(body: "Emote: James thinks he's the phantom lord")))),
|
||||
.loaded(sender: .init(id: "Robert"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil)))),
|
||||
.loaded(sender: .init(id: "Cliff"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.image(.init(body: "Image: Pushead",
|
||||
source: .init(url: .picturesDirectory, mimeType: nil),
|
||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
|
||||
.loaded(sender: .init(id: "Jason"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))),
|
||||
.loaded(sender: .init(id: "Kirk"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
|
||||
.loaded(sender: .init(id: "Lars"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.video(.init(body: "Video: Through the never",
|
||||
duration: 100,
|
||||
source: nil,
|
||||
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
|
||||
.loading(eventID: "")
|
||||
]
|
||||
|
||||
@@ -247,6 +261,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
messageComposer(mode: .reply(itemID: .random,
|
||||
replyDetails: .loaded(sender: .init(id: "Kirk"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
|
||||
isThread: false))
|
||||
}
|
||||
|
||||
@@ -230,8 +230,12 @@ class RoomScreenInteractionHandler {
|
||||
}
|
||||
}
|
||||
case .reply:
|
||||
guard let eventID = eventTimelineItem.id.eventID else {
|
||||
return
|
||||
}
|
||||
|
||||
let replyInfo = buildReplyInfo(for: eventTimelineItem)
|
||||
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventContent: replyInfo.type)
|
||||
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type)
|
||||
|
||||
actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread))))
|
||||
case .forward(let itemID):
|
||||
|
||||
@@ -76,8 +76,10 @@ enum RoomScreenViewAudioAction {
|
||||
|
||||
enum RoomScreenViewAction {
|
||||
case displayRoomDetails
|
||||
|
||||
case itemAppeared(itemID: TimelineItemIdentifier)
|
||||
case itemDisappeared(itemID: TimelineItemIdentifier)
|
||||
|
||||
case itemTapped(itemID: TimelineItemIdentifier)
|
||||
case toggleReaction(key: String, itemID: TimelineItemIdentifier)
|
||||
case sendReadReceiptIfNeeded(TimelineItemIdentifier)
|
||||
@@ -104,6 +106,8 @@ enum RoomScreenViewAction {
|
||||
case audio(RoomScreenViewAudioAction)
|
||||
|
||||
case presentCall
|
||||
|
||||
case focusOnEventID(String)
|
||||
}
|
||||
|
||||
enum RoomScreenComposerAction {
|
||||
|
||||
@@ -172,6 +172,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
actionsSubject.send(.displayCallScreen)
|
||||
case .showReadReceipts(itemID: let itemID):
|
||||
showReadReceipts(for: itemID)
|
||||
case .focusOnEventID(let eventID):
|
||||
// TODO: .. something
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ struct TimelineReplyView: View {
|
||||
var body: some View {
|
||||
if let timelineItemReplyDetails {
|
||||
switch timelineItemReplyDetails {
|
||||
case .loaded(let sender, let content):
|
||||
case .loaded(let sender, _, let content):
|
||||
switch content {
|
||||
case .message(let content):
|
||||
switch content {
|
||||
@@ -131,38 +131,6 @@ struct TimelineReplyView: View {
|
||||
|
||||
var icon: Icon?
|
||||
|
||||
var isTextOnly: Bool {
|
||||
icon == nil
|
||||
}
|
||||
|
||||
/// The string shown as the message preview.
|
||||
///
|
||||
/// This converts the formatted body to a plain string to remove formatting
|
||||
/// and render with a consistent font size. This conversion is done to avoid
|
||||
/// showing markdown characters in the preview for messages with formatting.
|
||||
var messagePreview: String {
|
||||
guard let formattedBody,
|
||||
let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else {
|
||||
return plainBody
|
||||
}
|
||||
|
||||
let range = NSRange(location: 0, length: attributedString.length)
|
||||
attributedString.enumerateAttributes(in: range) { attributes, range, _ in
|
||||
if let userID = attributes[.MatrixUserID] as? String {
|
||||
if let displayName = context.viewState.members[userID]?.displayName {
|
||||
attributedString.replaceCharacters(in: range, with: "@\(displayName)")
|
||||
} else {
|
||||
attributedString.replaceCharacters(in: range, with: userID)
|
||||
}
|
||||
}
|
||||
|
||||
if attributes[.MatrixAllUsersMention] as? Bool == true {
|
||||
attributedString.replaceCharacters(in: range, with: PillConstants.atRoom)
|
||||
}
|
||||
}
|
||||
return attributedString.string
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
iconView
|
||||
@@ -183,7 +151,7 @@ struct TimelineReplyView: View {
|
||||
.tint(.compound.textLinkExternal)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.padding(.leading, isTextOnly ? 8 : 0)
|
||||
.padding(.leading, icon == nil ? 8 : 0)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
@@ -216,6 +184,34 @@ struct TimelineReplyView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The string shown as the message preview.
|
||||
///
|
||||
/// This converts the formatted body to a plain string to remove formatting
|
||||
/// and render with a consistent font size. This conversion is done to avoid
|
||||
/// showing markdown characters in the preview for messages with formatting.
|
||||
private var messagePreview: String {
|
||||
guard let formattedBody,
|
||||
let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else {
|
||||
return plainBody
|
||||
}
|
||||
|
||||
let range = NSRange(location: 0, length: attributedString.length)
|
||||
attributedString.enumerateAttributes(in: range) { attributes, range, _ in
|
||||
if let userID = attributes[.MatrixUserID] as? String {
|
||||
if let displayName = context.viewState.members[userID]?.displayName {
|
||||
attributedString.replaceCharacters(in: range, with: "@\(displayName)")
|
||||
} else {
|
||||
attributedString.replaceCharacters(in: range, with: userID)
|
||||
}
|
||||
}
|
||||
|
||||
if attributes[.MatrixAllUsersMention] as? Bool == true {
|
||||
attributedString.replaceCharacters(in: range, with: PillConstants.atRoom)
|
||||
}
|
||||
}
|
||||
return attributedString.string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,18 +240,22 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "This is a reply"))))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.emote(.init(body: "says hello"))))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.notice(.init(body: "Hello world"))))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.audio(.init(body: "Some audio",
|
||||
duration: 0,
|
||||
waveform: nil,
|
||||
@@ -264,6 +264,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.file(.init(body: "Some file",
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
@@ -271,22 +272,26 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.image(.init(body: "Some image",
|
||||
source: imageSource,
|
||||
thumbnailSource: imageSource))))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.video(.init(body: "Some video",
|
||||
duration: 0,
|
||||
source: nil,
|
||||
thumbnailSource: imageSource))))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.location(.init(body: ""))))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.voice(.init(body: "Some voice message",
|
||||
duration: 0,
|
||||
waveform: nil,
|
||||
@@ -294,16 +299,20 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
|
||||
contentType: nil))))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithMention))))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention))))),
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
eventID: "123",
|
||||
eventContent: .poll(question: "Do you like polls?"))),
|
||||
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
|
||||
eventID: "123",
|
||||
eventContent: .redacted))
|
||||
]
|
||||
}
|
||||
|
||||
@@ -220,9 +220,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
.padding(.leading, 4)
|
||||
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -230,6 +232,9 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
.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)
|
||||
@@ -418,6 +423,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
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"))))),
|
||||
groupStyle: .single))
|
||||
|
||||
@@ -434,6 +440,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
|
||||
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
@@ -448,6 +455,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
thumbnailSource: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
@@ -458,6 +466,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
@@ -471,6 +480,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
longitude: 12.496366),
|
||||
description: "Location description description description description description description description description"),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
@@ -482,6 +492,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
content: .init(body: "Fallback geo uri description",
|
||||
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
|
||||
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
@@ -497,6 +508,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))),
|
||||
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 10, waveform: EstimatedWaveform.mockWaveform))
|
||||
}
|
||||
@@ -528,6 +540,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
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"))))),
|
||||
groupStyle: .single))
|
||||
|
||||
@@ -540,6 +553,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
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."))))),
|
||||
groupStyle: .single))
|
||||
}
|
||||
|
||||
@@ -52,7 +52,11 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
Rectangle()
|
||||
.foregroundColor(.compound.iconTertiary)
|
||||
.frame(width: 4.0)
|
||||
|
||||
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails)
|
||||
.onTapGesture {
|
||||
context.send(viewAction: .focusOnEventID(replyDetails.eventID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,101 +146,6 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
// These akwats include a reply
|
||||
static var threads: some View {
|
||||
ScrollView {
|
||||
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: "whoever"),
|
||||
content: .init(body: "A long message that should be on multiple lines."),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))),
|
||||
groupStyle: .single))
|
||||
|
||||
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "audio.ogg",
|
||||
duration: 100,
|
||||
waveform: nil,
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "File",
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
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: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
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: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "audio.ogg",
|
||||
duration: 100,
|
||||
waveform: EstimatedWaveform.mockWaveform,
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventContent: .message(.text(.init(body: "Short"))))),
|
||||
playerState: AudioPlayerState(id: .timelineItemIdentifier(.init(timelineID: "")), duration: 10, waveform: EstimatedWaveform.mockWaveform))
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
||||
@@ -253,5 +162,142 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
|
||||
.padding()
|
||||
.environment(\.timelineStyle, .plain)
|
||||
.previewDisplayName("Threads")
|
||||
|
||||
replies
|
||||
.environment(\.timelineStyle, .plain)
|
||||
.previewDisplayName("Replies")
|
||||
}
|
||||
|
||||
// These akwats include a reply
|
||||
static var threads: some View {
|
||||
ScrollView {
|
||||
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
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"))))),
|
||||
groupStyle: .single))
|
||||
|
||||
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "audio.ogg",
|
||||
duration: 100,
|
||||
waveform: nil,
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "File",
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: true,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
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: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .random,
|
||||
timestamp: "Now",
|
||||
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: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))))
|
||||
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
isThreaded: true,
|
||||
sender: .init(id: ""),
|
||||
content: .init(body: "audio.ogg",
|
||||
duration: 100,
|
||||
waveform: EstimatedWaveform.mockWaveform,
|
||||
source: nil,
|
||||
contentType: nil),
|
||||
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Short"))))),
|
||||
playerState: AudioPlayerState(id: .timelineItemIdentifier(.init(timelineID: "")), duration: 10, waveform: EstimatedWaveform.mockWaveform))
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
static var replies: some View {
|
||||
ScrollView {
|
||||
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
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"))))),
|
||||
groupStyle: .single))
|
||||
|
||||
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
|
||||
timestamp: "10:42",
|
||||
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."))))),
|
||||
groupStyle: .single))
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
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."))))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,15 @@ import Foundation
|
||||
enum TimelineItemReplyDetails: Hashable {
|
||||
case notLoaded(eventID: String)
|
||||
case loading(eventID: String)
|
||||
case loaded(sender: TimelineItemSender, eventContent: TimelineEventContent)
|
||||
case loaded(sender: TimelineItemSender, eventID: String, eventContent: TimelineEventContent)
|
||||
case error(eventID: String, message: String)
|
||||
|
||||
var eventID: String {
|
||||
switch self {
|
||||
case .notLoaded(let eventID), .loading(let eventID), .loaded(_, let eventID, _), .error(let eventID, _):
|
||||
return eventID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TimelineEventContent: Hashable {
|
||||
|
||||
@@ -640,7 +640,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
switch timelineItem.kind() {
|
||||
case .message:
|
||||
return timelineItemReplyDetails(for: timelineItem.asMessage()?.msgtype(), sender: sender)
|
||||
return timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype())
|
||||
case .poll(let question, _, _, _, _, _, _):
|
||||
replyContent = .poll(question: question)
|
||||
case .sticker(let body, _, _):
|
||||
@@ -651,13 +651,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
|
||||
return .loaded(sender: sender, eventContent: replyContent)
|
||||
return .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent)
|
||||
case let .error(message):
|
||||
return .error(eventID: details.eventId, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
private func timelineItemReplyDetails(for messageType: MessageType?, sender: TimelineItemSender) -> TimelineItemReplyDetails {
|
||||
private func timelineItemReplyDetails(sender: TimelineItemSender, eventID: String, messageType: MessageType?) -> TimelineItemReplyDetails {
|
||||
let replyContent: EventBasedMessageTimelineItemContentType
|
||||
|
||||
switch messageType {
|
||||
@@ -685,7 +685,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
replyContent = .text(.init(body: L10n.commonUnsupportedEvent))
|
||||
}
|
||||
|
||||
return .loaded(sender: sender, eventContent: .message(replyContent))
|
||||
return .loaded(sender: sender,
|
||||
eventID: eventID,
|
||||
eventContent: .message(replyContent))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user