Reply to a poll (#2087)

* Refactor TimelineItemReplyDetails for polls

* Fix reply for polls

* Update reply details factory

* Revert Poll Hashable conformance

* PR comments
This commit is contained in:
Alfonso Grillo
2023-11-15 16:57:34 +01:00
committed by GitHub
parent 0b9da83470
commit 9f1092b0b6
8 changed files with 179 additions and 150 deletions
@@ -32,7 +32,7 @@ struct MessageComposer: View {
let editCancellationAction: () -> Void let editCancellationAction: () -> Void
let onAppearAction: () -> Void let onAppearAction: () -> Void
@FocusState private var focused: Bool @FocusState private var focused: Bool
@State private var composerTranslation: CGFloat = 0 @State private var composerTranslation: CGFloat = 0
var body: some View { var body: some View {
@@ -40,7 +40,7 @@ struct MessageComposer: View {
if showResizeGrabber { if showResizeGrabber {
resizeGrabber resizeGrabber
} }
let borderRadius: CGFloat = 21 let borderRadius: CGFloat = 21
mainContent mainContent
.padding(.horizontal, 12.0) .padding(.horizontal, 12.0)
@@ -60,11 +60,11 @@ struct MessageComposer: View {
} }
.gesture(showResizeGrabber ? dragGesture : nil) .gesture(showResizeGrabber ? dragGesture : nil)
} }
// MARK: - Private // MARK: - Private
@State private var composerFrame = CGRect.zero @State private var composerFrame = CGRect.zero
private var mainContent: some View { private var mainContent: some View {
VStack(alignment: .leading, spacing: -6) { VStack(alignment: .leading, spacing: -6) {
header header
@@ -187,34 +187,34 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel.mock static let viewModel = RoomScreenViewModel.mock
static let replyTypes: [TimelineItemReplyDetails] = [ static let replyTypes: [TimelineItemReplyDetails] = [
.loaded(sender: .init(id: "Dave"), contentType: .audio(.init(body: "Audio: Ride the lightning", duration: 100, waveform: nil, source: nil, contentType: nil))), .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"), contentType: .emote(.init(body: "Emote: James thinks he's the phantom lord"))), .loaded(sender: .init(id: "James"), eventContent: .message(.emote(.init(body: "Emote: James thinks he's the phantom lord")))),
.loaded(sender: .init(id: "Robert"), contentType: .file(.init(body: "File: Crash course in brain surgery.pdf", source: nil, thumbnailSource: nil, contentType: nil))), .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"), contentType: .image(.init(body: "Image: Pushead", .loaded(sender: .init(id: "Cliff"), eventContent: .message(.image(.init(body: "Image: Pushead",
source: .init(url: .picturesDirectory, mimeType: nil), source: .init(url: .picturesDirectory, mimeType: nil),
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))), thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
.loaded(sender: .init(id: "Jason"), contentType: .notice(.init(body: "Notice: Too far gone?"))), .loaded(sender: .init(id: "Jason"), eventContent: .message(.notice(.init(body: "Notice: Too far gone?")))),
.loaded(sender: .init(id: "Kirk"), contentType: .text(.init(body: "Text: Where the wild things are"))), .loaded(sender: .init(id: "Kirk"), eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
.loaded(sender: .init(id: "Lars"), contentType: .video(.init(body: "Video: Through the never", .loaded(sender: .init(id: "Lars"), eventContent: .message(.video(.init(body: "Video: Through the never",
duration: 100, duration: 100,
source: nil, source: nil,
thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))), thumbnailSource: .init(url: .picturesDirectory, mimeType: nil))))),
.loading(eventID: "") .loading(eventID: "")
] ]
static func messageComposer(_ content: String = "", static func messageComposer(_ content: String = "",
sendingDisabled: Bool = false, sendingDisabled: Bool = false,
mode: RoomScreenComposerMode = .default) -> MessageComposer { mode: RoomScreenComposerMode = .default) -> MessageComposer {
let viewModel = WysiwygComposerViewModel(minHeight: 22, let viewModel = WysiwygComposerViewModel(minHeight: 22,
maxExpandedHeight: 250) maxExpandedHeight: 250)
viewModel.setMarkdownContent(content) viewModel.setMarkdownContent(content)
let composerView = WysiwygComposerView(placeholder: L10n.richTextEditorComposerPlaceholder, let composerView = WysiwygComposerView(placeholder: L10n.richTextEditorComposerPlaceholder,
viewModel: viewModel, viewModel: viewModel,
itemProviderHelper: nil, itemProviderHelper: nil,
keyCommandHandler: nil, keyCommandHandler: nil,
pasteHandler: nil) pasteHandler: nil)
return MessageComposer(composerView: composerView, return MessageComposer(composerView: composerView,
mode: mode, mode: mode,
showResizeGrabber: false, showResizeGrabber: false,
@@ -225,20 +225,21 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
editCancellationAction: { }, editCancellationAction: { },
onAppearAction: { viewModel.setup() }) onAppearAction: { viewModel.setup() })
} }
static var previews: some View { static var previews: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
messageComposer(sendingDisabled: true) messageComposer(sendingDisabled: true)
messageComposer("Some message", messageComposer("Some message",
mode: .edit(originalItemId: .random)) mode: .edit(originalItemId: .random))
messageComposer(mode: .reply(itemID: .random, messageComposer(mode: .reply(itemID: .random,
replyDetails: .loaded(sender: .init(id: "Kirk"), replyDetails: .loaded(sender: .init(id: "Kirk"),
contentType: .text(.init(body: "Text: Where the wild things are"))), isThread: false)) eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
isThread: false))
} }
.padding(.horizontal) .padding(.horizontal)
ScrollView { ScrollView {
VStack(spacing: 8) { VStack(spacing: 8) {
ForEach(replyTypes, id: \.self) { replyDetails in ForEach(replyTypes, id: \.self) { replyDetails in
@@ -232,8 +232,8 @@ class RoomScreenInteractionHandler {
} }
case .reply: case .reply:
let replyInfo = buildReplyInfo(for: eventTimelineItem) let replyInfo = buildReplyInfo(for: eventTimelineItem)
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: replyInfo.type) let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventContent: replyInfo.type)
actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread)))) actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails, isThread: replyInfo.isThread))))
case .forward(let itemID): case .forward(let itemID):
actionsSubject.send(.displayMessageForwarding(itemID: itemID)) actionsSubject.send(.displayMessageForwarding(itemID: itemID))
@@ -596,11 +596,14 @@ class RoomScreenInteractionHandler {
} }
private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo { private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo {
guard let messageItem = item as? EventBasedMessageTimelineItemProtocol else { switch item {
return .init(type: .text(.init(body: item.body)), isThread: false) case let messageItem as EventBasedMessageTimelineItemProtocol:
return .init(type: .message(messageItem.contentType), isThread: messageItem.isThreaded)
case let pollItem as PollRoomTimelineItem:
return .init(type: .poll(question: pollItem.poll.question), isThread: false)
default:
return .init(type: .message(.text(.init(body: item.body))), isThread: false)
} }
return .init(type: messageItem.contentType, isThread: messageItem.isThreaded)
} }
private func openSystemSettings() { private func openSystemSettings() {
@@ -611,7 +614,7 @@ class RoomScreenInteractionHandler {
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction { private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction {
var source: MediaSourceProxy? var source: MediaSourceProxy?
var body: String var body: String
switch timelineItem { switch timelineItem {
case let item as ImageRoomTimelineItem: case let item as ImageRoomTimelineItem:
source = item.content.source source = item.content.source
@@ -657,6 +660,6 @@ class RoomScreenInteractionHandler {
} }
private struct ReplyInfo { private struct ReplyInfo {
let type: EventBasedMessageTimelineItemContentType let type: TimelineEventContent
let isThread: Bool let isThread: Bool
} }
@@ -31,48 +31,56 @@ struct TimelineReplyView: View {
switch timelineItemReplyDetails { switch timelineItemReplyDetails {
case .loaded(let sender, let content): case .loaded(let sender, let content):
switch content { switch content {
case .audio(let content): case .message(let content):
switch content {
case .audio(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii))
case .emote(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .file(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: .init(kind: .iconAsset(Asset.Images.document), cornerRadii: iconCornerRadii))
case .image(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii))
case .notice(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .text(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .video(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) })
case .voice:
ReplyView(sender: sender,
plainBody: L10n.commonVoiceMessage,
formattedBody: nil,
icon: .init(kind: .icon(\.micOnOutline), cornerRadii: iconCornerRadii))
case .location:
ReplyView(sender: sender,
plainBody: L10n.commonSharedLocation,
formattedBody: nil,
icon: .init(kind: .iconAsset(Asset.Images.addLocation), cornerRadii: iconCornerRadii))
}
case .poll(let question):
ReplyView(sender: sender, ReplyView(sender: sender,
plainBody: content.body, plainBody: question,
formattedBody: nil, formattedBody: nil,
icon: .init(kind: .systemIcon("waveform"), cornerRadii: iconCornerRadii)) icon: .init(kind: .iconAsset(Asset.Images.timelinePoll), cornerRadii: iconCornerRadii))
case .emote(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .file(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: .init(kind: .iconAsset(Asset.Images.document), cornerRadii: iconCornerRadii))
case .image(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: .init(kind: .mediaSource(content.thumbnailSource ?? content.source), cornerRadii: iconCornerRadii))
case .notice(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .text(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
case .video(let content):
ReplyView(sender: sender,
plainBody: content.body,
formattedBody: nil,
icon: content.thumbnailSource.map { .init(kind: .mediaSource($0), cornerRadii: iconCornerRadii) })
case .voice:
ReplyView(sender: sender,
plainBody: L10n.commonVoiceMessage,
formattedBody: nil,
icon: .init(kind: .icon(\.micOnOutline), cornerRadii: iconCornerRadii))
case .location:
ReplyView(sender: sender,
plainBody: L10n.commonSharedLocation,
formattedBody: nil,
icon: .init(kind: .iconAsset(Asset.Images.addLocation), cornerRadii: iconCornerRadii))
} }
default: default:
LoadingReplyView() LoadingReplyView()
@@ -223,7 +231,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
static var previewItems: [TimelineReplyView] { static var previewItems: [TimelineReplyView] {
let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png") let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png")
return [ return [
TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")), TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")),
@@ -231,60 +239,60 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "This is a reply")))), eventContent: .message(.text(.init(body: "This is a reply"))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .emote(.init(body: "says hello")))), eventContent: .message(.emote(.init(body: "says hello"))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
contentType: .notice(.init(body: "Hello world")))), eventContent: .message(.notice(.init(body: "Hello world"))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .audio(.init(body: "Some audio", eventContent: .message(.audio(.init(body: "Some audio",
duration: 0, duration: 0,
waveform: nil, waveform: nil,
source: nil, source: nil,
contentType: nil)))), contentType: nil))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .file(.init(body: "Some file", eventContent: .message(.file(.init(body: "Some file",
source: nil, source: nil,
thumbnailSource: nil, thumbnailSource: nil,
contentType: nil)))), contentType: nil))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .image(.init(body: "Some image", eventContent: .message(.image(.init(body: "Some image",
source: imageSource, source: imageSource,
thumbnailSource: imageSource)))), thumbnailSource: imageSource))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .video(.init(body: "Some video", eventContent: .message(.video(.init(body: "Some video",
duration: 0, duration: 0,
source: nil, source: nil,
thumbnailSource: imageSource)))), thumbnailSource: imageSource))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .location(.init(body: "")))), eventContent: .message(.location(.init(body: ""))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .voice(.init(body: "Some voice message", eventContent: .message(.voice(.init(body: "Some voice message",
duration: 0, duration: 0,
waveform: nil, waveform: nil,
source: nil, source: nil,
contentType: nil)))), contentType: nil))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
contentType: .notice(.init(body: "", formattedBody: attributedStringWithMention)))), eventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithMention))))),
TimelineReplyView(placement: .timeline, TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),
contentType: .notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention)))) eventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention)))))
] ]
} }
@@ -421,7 +421,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
sender: .init(id: "whoever"), sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."), content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), groupStyle: .single)) eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
@@ -436,7 +437,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil, source: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
@@ -450,7 +451,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
thumbnailSource: nil, thumbnailSource: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
@@ -460,7 +461,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
sender: .init(id: ""), sender: .init(id: ""),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -473,7 +474,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
longitude: 12.496366), longitude: 12.496366),
description: "Location description description description description description description description description"), description: "Location description description description description description description description description"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -484,7 +485,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
content: .init(body: "Fallback geo uri description", content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil), geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
@@ -499,7 +500,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
source: nil, source: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), eventContent: .message(.text(.init(body: "Short"))))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 10, waveform: EstimatedWaveform.mockWaveform)) playerState: AudioPlayerState(id: .timelineItemIdentifier(.random), duration: 10, waveform: EstimatedWaveform.mockWaveform))
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
@@ -530,7 +531,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
sender: .init(id: "whoever"), sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."), content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), groupStyle: .single)) eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""), RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
@@ -541,7 +543,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
sender: .init(id: "whoever"), sender: .init(id: "whoever"),
content: .init(body: "Short message"), content: .init(body: "Short message"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))), groupStyle: .single)) 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) .environmentObject(viewModel.context)
} }
@@ -156,8 +156,9 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "whoever"), sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."), content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), groupStyle: .single)) eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))
AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), AudioRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
@@ -171,7 +172,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
source: nil, source: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), FileRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: false, isOutgoing: false,
@@ -184,7 +185,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
thumbnailSource: nil, thumbnailSource: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), ImageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
@@ -194,7 +195,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: ""), sender: .init(id: ""),
content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil), content: .init(body: "Some image", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"), thumbnailSource: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -207,7 +208,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
longitude: 12.496366), longitude: 12.496366),
description: "Location description description description description description description description description"), description: "Location description description description description description description description description"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
LocationRoomTimelineView(timelineItem: .init(id: .random, LocationRoomTimelineView(timelineItem: .init(id: .random,
timestamp: "Now", timestamp: "Now",
isOutgoing: false, isOutgoing: false,
@@ -218,7 +219,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
content: .init(body: "Fallback geo uri description", content: .init(body: "Fallback geo uri description",
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil), geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short"))))) eventContent: .message(.text(.init(body: "Short"))))))
VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""), VoiceMessageRoomTimelineView(timelineItem: .init(id: .init(timelineID: ""),
timestamp: "10:42", timestamp: "10:42",
isOutgoing: true, isOutgoing: true,
@@ -232,12 +233,12 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
source: nil, source: nil,
contentType: nil), contentType: nil),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
contentType: .text(.init(body: "Short")))), eventContent: .message(.text(.init(body: "Short"))))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.init(timelineID: "")), duration: 10, waveform: EstimatedWaveform.mockWaveform)) playerState: AudioPlayerState(id: .timelineItemIdentifier(.init(timelineID: "")), duration: 10, waveform: EstimatedWaveform.mockWaveform))
} }
.environmentObject(viewModel.context) .environmentObject(viewModel.context)
} }
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
@@ -136,6 +136,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
content: .init(body: "Fallback geo uri description", 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"), geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: "Location description description description description description description description description"),
replyDetails: .loaded(sender: .init(id: "Someone"), replyDetails: .loaded(sender: .init(id: "Someone"),
contentType: .text(.init(body: "The thread content goes 'ere."))))) eventContent: .message(.text(.init(body: "The thread content goes 'ere."))))))
} }
} }
@@ -19,6 +19,11 @@ import Foundation
enum TimelineItemReplyDetails: Hashable { enum TimelineItemReplyDetails: Hashable {
case notLoaded(eventID: String) case notLoaded(eventID: String)
case loading(eventID: String) case loading(eventID: String)
case loaded(sender: TimelineItemSender, contentType: EventBasedMessageTimelineItemContentType) case loaded(sender: TimelineItemSender, eventContent: TimelineEventContent)
case error(eventID: String, message: String) case error(eventID: String, message: String)
} }
enum TimelineEventContent: Hashable {
case message(EventBasedMessageTimelineItemContentType)
case poll(question: String)
}
@@ -628,47 +628,55 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
avatarURL: nil) avatarURL: nil)
} }
let replyContent: EventBasedMessageTimelineItemContentType let replyContent: TimelineEventContent
switch timelineItem.kind() { switch timelineItem.kind() {
case .message: case .message:
switch timelineItem.asMessage()?.msgtype() { return timelineItemReplyDetails(for: timelineItem.asMessage()?.msgtype(), sender: sender)
case .audio(let content):
if content.voice != nil {
replyContent = .voice(buildAudioTimelineItemContent(content))
} else {
replyContent = .audio(buildAudioTimelineItemContent(content))
}
case .emote(let content):
replyContent = .emote(buildEmoteTimelineItemContent(senderDisplayName: sender.displayName, senderID: sender.id, messageContent: content))
case .file(let content):
replyContent = .file(buildFileTimelineItemContent(content))
case .image(let content):
replyContent = .image(buildImageTimelineItemContent(content))
case .notice(let content):
replyContent = .notice(buildNoticeTimelineItemContent(content))
case .text(let content):
replyContent = .text(buildTextTimelineItemContent(content))
case .video(let content):
replyContent = .video(buildVideoTimelineItemContent(content))
case .location(let content):
replyContent = .location(buildLocationTimelineItemContent(content))
case .other, .none:
replyContent = .text(.init(body: L10n.commonUnsupportedEvent))
}
case .poll(let question, _, _, _, _, _): case .poll(let question, _, _, _, _, _):
replyContent = .text(.init(body: question)) replyContent = .poll(question: question)
case .sticker(let body, _, _): case .sticker(let body, _, _):
replyContent = .text(.init(body: body)) replyContent = .message(.text(.init(body: body)))
default: default:
replyContent = .text(.init(body: L10n.commonUnsupportedEvent)) replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
} }
return .loaded(sender: sender, contentType: replyContent) return .loaded(sender: sender, eventContent: replyContent)
case let .error(message): case let .error(message):
return .error(eventID: details.eventId, message: message) return .error(eventID: details.eventId, message: message)
} }
} }
private func timelineItemReplyDetails(for messageType: MessageType?, sender: TimelineItemSender) -> TimelineItemReplyDetails {
let replyContent: EventBasedMessageTimelineItemContentType
switch messageType {
case .audio(let content):
if content.voice != nil {
replyContent = .voice(buildAudioTimelineItemContent(content))
} else {
replyContent = .audio(buildAudioTimelineItemContent(content))
}
case .emote(let content):
replyContent = .emote(buildEmoteTimelineItemContent(senderDisplayName: sender.displayName, senderID: sender.id, messageContent: content))
case .file(let content):
replyContent = .file(buildFileTimelineItemContent(content))
case .image(let content):
replyContent = .image(buildImageTimelineItemContent(content))
case .notice(let content):
replyContent = .notice(buildNoticeTimelineItemContent(content))
case .text(let content):
replyContent = .text(buildTextTimelineItemContent(content))
case .video(let content):
replyContent = .video(buildVideoTimelineItemContent(content))
case .location(let content):
replyContent = .location(buildLocationTimelineItemContent(content))
case .other, .none:
replyContent = .text(.init(body: L10n.commonUnsupportedEvent))
}
return .loaded(sender: sender, eventContent: .message(replyContent))
}
} }
private extension LocationRoomTimelineItemContent.AssetType { private extension LocationRoomTimelineItemContent.AssetType {