diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index f1268adbb..ffad66961 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -31,7 +31,7 @@ enum RoomScreenViewModelAction { enum RoomScreenComposerMode: Equatable { case `default` - case reply(id: String, displayName: String) + case reply(itemID: String, replyDetails: TimelineItemReplyDetails) case edit(originalItemId: String) var isEdit: Bool { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index f0f66def1..fd73c79ae 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -339,7 +339,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } case .reply: state.bindings.composerFocused = true - state.composerMode = .reply(id: eventTimelineItem.id, displayName: eventTimelineItem.sender.displayName ?? eventTimelineItem.sender.id) + + let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: buildReplyContent(for: eventTimelineItem)) + + state.composerMode = .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails) case .viewSource: let debugInfo = timelineController.debugInfo(for: eventTimelineItem.id) MXLog.info(debugInfo) @@ -410,6 +413,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } } + + private func buildReplyContent(for item: EventBasedTimelineItemProtocol) -> EventBasedMessageTimelineItemContentType { + guard let messageItem = item as? EventBasedMessageTimelineItemProtocol else { + return .text(.init(body: item.body)) + } + + return messageItem.contentType + } } // MARK: - Mocks diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift index 1e1c773ed..a6fe39e98 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift @@ -20,7 +20,7 @@ struct MessageComposer: View { @Binding var text: String @Binding var focused: Bool let sendingDisabled: Bool - let type: RoomScreenComposerMode + let mode: RoomScreenComposerMode let sendAction: EnterKeyHandler let pasteAction: PasteHandler @@ -75,14 +75,14 @@ struct MessageComposer: View { } // Explicitly disable all animations to fix weirdness with the header immediately // appearing whilst the text field and keyboard are still animating up to it. - .animation(.noAnimation, value: type) + .animation(.noAnimation, value: mode) } - + @ViewBuilder private var header: some View { - switch type { - case .reply(_, let displayName): - MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction) + switch mode { + case .reply(_, let replyDetails): + MessageComposerReplyHeader(replyDetails: replyDetails, action: replyCancellationAction) case .edit: MessageComposerEditHeader(action: editCancellationAction) case .default: @@ -94,22 +94,22 @@ struct MessageComposer: View { // ZStack with opacity so the button size is consistent. ZStack { Image(systemName: "checkmark") - .opacity(type.isEdit ? 1 : 0) + .opacity(mode.isEdit ? 1 : 0) .fontWeight(.medium) .accessibilityLabel(L10n.actionConfirm) - .accessibilityHidden(!type.isEdit) + .accessibilityHidden(!mode.isEdit) Image(asset: Asset.Images.timelineComposerSendMessage) .resizable() .frame(width: sendButtonIconSize, height: sendButtonIconSize) .padding(EdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 6)) - .opacity(type.isEdit ? 0 : 1) + .opacity(mode.isEdit ? 0 : 1) .accessibilityLabel(L10n.actionSend) - .accessibilityHidden(type.isEdit) + .accessibilityHidden(mode.isEdit) } } private var borderRadius: CGFloat { - switch type { + switch mode { case .default: return isMultiline ? 20 : 28 case .reply, .edit: @@ -119,27 +119,31 @@ struct MessageComposer: View { } private struct MessageComposerReplyHeader: View { - let displayName: String + let replyDetails: TimelineItemReplyDetails let action: () -> Void var body: some View { - HStack(alignment: .center) { - Label(L10n.commonReplyingTo(displayName), systemImage: "arrowshape.turn.up.left") - .labelStyle(MessageComposerHeaderLabelStyle()) - Spacer() - Button(action: action) { - Image(systemName: "xmark") - .font(.caption2.weight(.medium)) - .foregroundColor(.element.secondaryContent) - .padding(12.0) + TimelineReplyView(placement: .composer, timelineItemReplyDetails: replyDetails) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(4.0) + .background(.white) + .cornerRadius(13.0) + .padding([.trailing, .vertical], 8.0) + .padding([.leading], -4.0) + .overlay(alignment: .topTrailing) { + Button(action: action) { + Image(systemName: "xmark") + .font(.caption2.weight(.medium)) + .foregroundColor(.element.tertiaryContent) + .padding(16.0) + } } - } } } private struct MessageComposerEditHeader: View { let action: () -> Void - + var body: some View { HStack(alignment: .center) { Label(L10n.commonEditing, systemImage: "pencil.line") @@ -148,7 +152,7 @@ private struct MessageComposerEditHeader: View { Button(action: action) { Image(systemName: "xmark") .font(.caption2.weight(.medium)) - .foregroundColor(.element.secondaryContent) + .foregroundColor(.element.tertiaryContent) .padding(12.0) } } @@ -161,19 +165,21 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle { configuration.icon configuration.title } - .font(.compound.bodyXS) + .font(.compound.bodySM) .foregroundColor(.element.secondaryContent) .lineLimit(1) } } struct MessageComposer_Previews: PreviewProvider { + static let viewModel = RoomScreenViewModel.mock + static var previews: some View { VStack { MessageComposer(text: .constant(""), focused: .constant(false), sendingDisabled: true, - type: .default, + mode: .default, sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, @@ -182,7 +188,7 @@ struct MessageComposer_Previews: PreviewProvider { MessageComposer(text: .constant("This is a short message."), focused: .constant(false), sendingDisabled: false, - type: .default, + mode: .default, sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, @@ -191,7 +197,7 @@ struct MessageComposer_Previews: PreviewProvider { MessageComposer(text: .constant("This is a very long message that will wrap to 2 lines on an iPhone 14."), focused: .constant(false), sendingDisabled: false, - type: .default, + mode: .default, sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, @@ -200,7 +206,7 @@ struct MessageComposer_Previews: PreviewProvider { MessageComposer(text: .constant("This is an even longer message that will wrap to 3 lines on an iPhone 14, just to see the difference it makes."), focused: .constant(false), sendingDisabled: false, - type: .default, + mode: .default, sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, @@ -209,22 +215,47 @@ struct MessageComposer_Previews: PreviewProvider { MessageComposer(text: .constant("Some message"), focused: .constant(false), sendingDisabled: false, - type: .reply(id: UUID().uuidString, - displayName: "John Doe"), - sendAction: { }, - pasteAction: { _ in }, - replyCancellationAction: { }, - editCancellationAction: { }) - - MessageComposer(text: .constant("Some message"), - focused: .constant(false), - sendingDisabled: false, - type: .edit(originalItemId: UUID().uuidString), + mode: .edit(originalItemId: UUID().uuidString), sendAction: { }, pasteAction: { _ in }, replyCancellationAction: { }, editCancellationAction: { }) } .padding(.horizontal) + + ScrollView { + VStack { + let replyTypes: [TimelineItemReplyDetails] = [ + .loaded(sender: .init(id: "Dave"), contentType: .audio(.init(body: "Audio: Ride the lightning", duration: 100, 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: "Robert"), contentType: .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", + source: .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: "Kirk"), contentType: .text(.init(body: "Text: Where the wild things are"))), + .loaded(sender: .init(id: "Lars"), contentType: .video(.init(body: "Video: Through the never", + duration: 100, + source: nil, + thumbnailSource: .init(url: .picturesDirectory, mimeType: nil)))), + .loading(eventID: "") + ] + + ForEach(replyTypes, id: \.self) { replyDetails in + MessageComposer(text: .constant(""), + focused: .constant(false), + sendingDisabled: false, + mode: .reply(itemID: UUID().uuidString, + replyDetails: replyDetails), + sendAction: { }, + pasteAction: { _ in }, + replyCancellationAction: { }, + editCancellationAction: { }) + } + } + } + .padding(.horizontal) + .environmentObject(viewModel.context) + .previewDisplayName("Replying") } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index cfceb3fde..25315fd64 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -16,7 +16,13 @@ import SwiftUI +enum TimelineReplyViewPlacement { + case timeline + case composer +} + struct TimelineReplyView: View { + let placement: TimelineReplyViewPlacement let timelineItemReplyDetails: TimelineItemReplyDetails? var body: some View { @@ -25,19 +31,37 @@ struct TimelineReplyView: View { case .loaded(let sender, let content): switch content { case .audio(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, systemIconName: "waveform") + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: nil, + icon: .init(systemIconName: "waveform", cornerRadii: iconCornerRadii)) case .emote(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: content.formattedBody) case .file(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, systemIconName: "doc.text.fill") + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: nil, + icon: .init(systemIconName: "doc.text.fill", cornerRadii: iconCornerRadii)) case .image(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, mediaSource: content.thumbnailSource ?? content.source) + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: nil, + icon: .init(mediaSource: content.thumbnailSource ?? content.source, cornerRadii: iconCornerRadii)) case .notice(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: content.formattedBody) case .text(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: content.formattedBody) + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: content.formattedBody) case .video(let content): - ReplyView(sender: sender, plainBody: content.body, formattedBody: nil, mediaSource: content.thumbnailSource) + ReplyView(sender: sender, + plainBody: content.body, + formattedBody: nil, + icon: .init(mediaSource: content.thumbnailSource, cornerRadii: iconCornerRadii)) } default: LoadingReplyView() @@ -45,6 +69,15 @@ struct TimelineReplyView: View { } } + private var iconCornerRadii: Double { + switch placement { + case .composer: + return 9.0 + case .timeline: + return 4.0 + } + } + private struct LoadingReplyView: View { var body: some View { ReplyView(sender: .init(id: "@alice:matrix.org"), plainBody: "Hello world", formattedBody: nil) @@ -53,6 +86,12 @@ struct TimelineReplyView: View { } private struct ReplyView: View { + struct Icon { + var mediaSource: MediaSourceProxy? + var systemIconName: String? + let cornerRadii: Double + } + @EnvironmentObject private var context: RoomScreenViewModel.Context @ScaledMetric private var imageContainerSize = 36.0 @@ -60,16 +99,19 @@ struct TimelineReplyView: View { let plainBody: String let formattedBody: AttributedString? - var systemIconName: String? - var mediaSource: MediaSourceProxy? + var icon: Icon? var body: some View { HStack { - icon + iconView .frame(width: imageContainerSize, height: imageContainerSize) .foregroundColor(.element.primaryContent) .background(Color.compound.bgSubtlePrimary) - .cornerRadius(9.0, corners: .allCorners) + .cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners) + + if icon?.mediaSource == nil, icon?.systemIconName == nil { + Spacer().frame(width: 4.0) + } VStack(alignment: .leading) { Text(sender.displayName ?? sender.id) @@ -86,8 +128,8 @@ struct TimelineReplyView: View { } @ViewBuilder - private var icon: some View { - if let mediaSource { + private var iconView: some View { + if let mediaSource = icon?.mediaSource { LoadableImage(mediaSource: mediaSource, size: .init(width: imageContainerSize, height: imageContainerSize), @@ -98,11 +140,11 @@ struct TimelineReplyView: View { .aspectRatio(contentMode: .fill) } - if let systemIconName { + if let systemIconName = icon?.systemIconName { Image(systemName: systemIconName) .resizable() .aspectRatio(contentMode: .fit) - .padding(4.0) + .padding(8.0) } } } @@ -113,30 +155,50 @@ struct TimelineReplyView_Previews: PreviewProvider { static var previews: some View { VStack(alignment: .leading) { - TimelineReplyView(timelineItemReplyDetails: .notLoaded(eventID: "")) + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .notLoaded(eventID: "")) - TimelineReplyView(timelineItemReplyDetails: .loading(eventID: "")) + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: .loading(eventID: "")) - TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - content: .text(.init(body: "This is a reply")))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .text(.init(body: "This is a reply")))) - TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - content: .emote(.init(body: "says hello")))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .emote(.init(body: "says hello")))) - TimelineReplyView(timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bot"), - content: .notice(.init(body: "Hello world")))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bot"), + contentType: .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(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .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)))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .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(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .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)))) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), + contentType: .video(.init(body: "Some video", + duration: 0, + source: nil, + thumbnailSource: imageSource)))) } .environmentObject(viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 3e587a057..53c0671ac 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -28,6 +28,7 @@ struct RoomScreen: View { HStack(spacing: 4.0) { RoomAttachmentPicker(context: context) messageComposer + .environmentObject(context) } .padding([.horizontal, .bottom]) .padding(.top, 8) @@ -69,7 +70,7 @@ struct RoomScreen: View { MessageComposer(text: $context.composerText, focused: $context.composerFocused, sendingDisabled: context.viewState.sendButtonDisabled, - type: context.viewState.composerMode) { + mode: context.viewState.composerMode) { sendMessage() } pasteAction: { provider in context.send(viewAction: .handlePasteOrDrop(provider: provider)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 4d109ae2b..1f0e90d30 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -162,7 +162,7 @@ struct TimelineItemBubbledStylerView: View { 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(timelineItemReplyDetails: replyDetails) + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) .foregroundColor(.compound.textPlaceholder) .fixedSize(horizontal: false, vertical: true) .padding(4.0) @@ -172,7 +172,7 @@ struct TimelineItemBubbledStylerView: View { .layoutPriority(TimelineBubbleLayout.Priority.visibleQuote) // Add a fixed width reply bubble that is used for layout calculations but won't be rendered. - TimelineReplyView(timelineItemReplyDetails: replyDetails) + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) .fixedSize(horizontal: false, vertical: true) .padding(4.0) .layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote) @@ -264,7 +264,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { sender: .init(id: "whoever"), content: .init(body: "A long message that should be on multiple lines."), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - content: .text(.init(body: "Short")))), + contentType: .text(.init(body: "Short")))), .single) RoomTimelineViewProvider.text(TextRoomTimelineItem(id: "", @@ -274,7 +274,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { sender: .init(id: "whoever"), content: .init(body: "Short message"), replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"), - content: .text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))), + contentType: .text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout.")))), .single) } .environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index b27dd85a9..b2e2e2372 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -52,7 +52,8 @@ struct TimelineItemPlainStylerView: View { Rectangle() .foregroundColor(.global.melon) .frame(width: 4.0) - TimelineReplyView(timelineItemReplyDetails: replyDetails) + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: replyDetails) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/TimelineItemReplyDetails.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/TimelineItemReplyDetails.swift index cb5dbf838..9a8d8b9c2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/TimelineItemReplyDetails.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/TimelineItemReplyDetails.swift @@ -16,19 +16,9 @@ import Foundation -enum TimelineItemReplyContent: Hashable { - case audio(AudioRoomTimelineItemContent) - case emote(EmoteRoomTimelineItemContent) - case file(FileRoomTimelineItemContent) - case image(ImageRoomTimelineItemContent) - case notice(NoticeRoomTimelineItemContent) - case text(TextRoomTimelineItemContent) - case video(VideoRoomTimelineItemContent) -} - enum TimelineItemReplyDetails: Hashable { case notLoaded(eventID: String) case loading(eventID: String) - case loaded(sender: TimelineItemSender, content: TimelineItemReplyContent) + case loaded(sender: TimelineItemSender, contentType: EventBasedMessageTimelineItemContentType) case error(eventID: String, message: String) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index 1028b92c4..2d91d74b4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -16,7 +16,17 @@ import Foundation -protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { - var body: String { get } - var replyDetails: TimelineItemReplyDetails? { get } +enum EventBasedMessageTimelineItemContentType: Hashable { + case audio(AudioRoomTimelineItemContent) + case emote(EmoteRoomTimelineItemContent) + case file(FileRoomTimelineItemContent) + case image(ImageRoomTimelineItemContent) + case notice(NoticeRoomTimelineItemContent) + case text(TextRoomTimelineItemContent) + case video(VideoRoomTimelineItemContent) +} + +protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { + var replyDetails: TimelineItemReplyDetails? { get } + var contentType: EventBasedMessageTimelineItemContentType { get } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 756bd064a..77d49da27 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -24,6 +24,8 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC var sender: TimelineItemSender { get } + var body: String { get } + var properties: RoomTimelineItemProperties { get } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift index fee7b72de..2a2cbf1f5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift @@ -32,4 +32,8 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .audio(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift index 3febb2bd3..5a898f3ed 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift @@ -33,4 +33,8 @@ struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .emote(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift index 7daa4bfc5..b79015766 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift @@ -34,4 +34,8 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .file(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift index cb0c26ca8..ba595da4f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift @@ -34,4 +34,8 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .image(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift index 2cde6ecab..d2db4f535 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift @@ -33,4 +33,8 @@ struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .notice(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift index ffb8b15cf..8be020437 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift @@ -34,4 +34,8 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable { var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .text(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index d54be6dea..33f3b4a4a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -34,4 +34,8 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl var body: String { content.body } + + var contentType: EventBasedMessageTimelineItemContentType { + .video(content) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 27a31b6e0..da032370e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -443,7 +443,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { avatarURL: nil) } - let replyContent: TimelineItemReplyContent + let replyContent: EventBasedMessageTimelineItemContentType switch message.msgtype() { case .audio(let content): replyContent = .audio(buildAudioTimelineItemContent(content)) @@ -463,7 +463,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return nil } - return .loaded(sender: sender, content: replyContent) + return .loaded(sender: sender, contentType: replyContent) case let .error(message): return .error(eventID: details.eventId, message: message) } diff --git a/changelog.d/976.feature b/changelog.d/976.feature new file mode 100644 index 000000000..adc7f680a --- /dev/null +++ b/changelog.d/976.feature @@ -0,0 +1 @@ +Update reply composer mode UI to include message being replied to \ No newline at end of file