From 7ec618b7ff6d771a96d927211d22b59ae7db83cf Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 15 May 2023 17:04:37 +0300 Subject: [PATCH] Added custom reply views for all message types as well as a loading state --- .../View/Replies/TimelineReplyView.swift | 114 +++++++++++++----- .../View/Style/TimelineBubbleLayout.swift | 2 +- .../Style/TimelineItemPlainStylerView.swift | 2 +- 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index 9447add2d..46917962b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -25,58 +25,116 @@ struct TimelineReplyView: View { case .loaded(let sender, let content): switch content { case .audio(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: nil) + ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, systemIconName: "waveform") case .emote(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) case .file(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: nil) + ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, systemIconName: "doc.text.fill") case .image(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: nil) + ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, mediaSource: content.thumbnailSource ?? content.source) case .notice(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) case .text(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) case .video(let content): - TimelineTextReplyView(sender: sender, plainBody: content.body, formattedBody: nil) + ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, mediaSource: content.thumbnailSource) } default: - Text("Missing in-reply-to details") - .font(.compound.bodyMD) - .foregroundColor(.element.secondaryContent) - .padding() + LoadingReplyView() } } } - private struct TimelineTextReplyView: View { + private struct LoadingReplyView: View { + var body: some View { + ReplyView(sender: .init(id: "@alice:matrix.org"), plainBody: "Hello world", formattedBody: nil) + .redacted(reason: .placeholder) + } + } + + private struct ReplyView: View { + @EnvironmentObject private var context: RoomScreenViewModel.Context + @ScaledMetric private var imageContainerSize = 36.0 + let sender: TimelineItemSender let plainBody: String let formattedBody: AttributedString? + var systemIconName: String? + var mediaSource: MediaSourceProxy? + var body: some View { - VStack(alignment: .leading) { - Text(sender.displayName ?? sender.id) - .font(.compound.bodySMSemibold) - .foregroundColor(.compound.textPrimary) + HStack { + icon + .frame(width: imageContainerSize, height: imageContainerSize) + .foregroundColor(.element.primaryContent) + .background(Color.compound.bgSubtlePrimary) + .cornerRadius(9.0, corners: .allCorners) - Text(formattedBody ?? AttributedString(plainBody)) - .font(.compound.bodyMD) - .foregroundColor(.compound.textPlaceholder) - .tint(.element.links) - .lineLimit(2) + VStack(alignment: .leading) { + Text(sender.displayName ?? sender.id) + .font(.compound.bodySMSemibold) + .foregroundColor(.compound.textPrimary) + + Text(formattedBody ?? AttributedString(plainBody)) + .font(.compound.bodyMD) + .foregroundColor(.compound.textPlaceholder) + .tint(.element.links) + .lineLimit(2) + } + } + } + + @ViewBuilder + private var icon: some View { + if let mediaSource { + LoadableImage(mediaSource: mediaSource, size: .init(width: imageContainerSize, height: imageContainerSize), imageProvider: context.imageProvider) { + Image(systemName: "photo") + .padding(4.0) + } + .aspectRatio(contentMode: .fill) + } + + if let systemIconName { + Image(systemName: systemIconName) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(4.0) } } } } struct TimelineReplyView_Previews: PreviewProvider { + static let viewModel = RoomScreenViewModel.mock + static var previews: some View { - TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - content: .text(.init(body: "This is a reply")))) - .background(Color.element.background) - .cornerRadius(8) - .padding(8) - .background(Color.element.bubblesYou) - .cornerRadius(12) + VStack(alignment: .leading) { + TimelineReplyView(timelineItemReplyDetails: .notLoaded(eventID: "")) + + TimelineReplyView(timelineItemReplyDetails: .loading(eventID: "")) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + content: .text(.init(body: "This is a reply")))) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + content: .emote(.init(body: "says hello")))) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bot"), + content: .notice(.init(body: "Hello world")))) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + content: .audio(.init(body: "Some audio", duration: 0, source: nil, contentType: nil)))) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + content: .file(.init(body: "Some file", source: nil, thumbnailSource: nil, contentType: nil)))) + + let imageSource = MediaSourceProxy(url: .init(staticString: "https://mock.com"), mimeType: "image/png") + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), content: .image(.init(body: "Some image", source: imageSource, thumbnailSource: imageSource)))) + + TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), content: .video(.init(body: "Some video", duration: 0, source: nil, thumbnailSource: imageSource)))) + } + .environmentObject(viewModel.context) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift index 61f3214fe..c891bb48e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift @@ -76,6 +76,6 @@ extension View { func timelineQuoteBubbleFormatting() -> some View { foregroundColor(.compound.textPlaceholder) .fixedSize(horizontal: false, vertical: true) - .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) + .padding(4.0) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index 97c5cd855..7a709139a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -50,7 +50,7 @@ struct TimelineItemPlainStylerView: View { let replyDetails = messageTimelineItem.replyDetails { HStack(spacing: 4.0) { Rectangle() - .foregroundColor(.element.accent) + .foregroundColor(.global.melon) .frame(width: 4.0) TimelineReplyView(timelineItemReplyDetails: replyDetails) }