mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Composer mode UI updates (#984)
* Fixes #975 - Update editing message UI * Fixes #976 - Update reply composer mode UI to include message being replied to * Use different icon corner radii in the timeline reply view depending on its placement
This commit is contained in:
parent
87075eecfd
commit
83e3d828cd
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -162,7 +162,7 @@ struct TimelineItemBubbledStylerView<Content: View>: 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<Content: View>: 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)
|
||||
|
@ -52,7 +52,8 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
Rectangle()
|
||||
.foregroundColor(.global.melon)
|
||||
.frame(width: 4.0)
|
||||
TimelineReplyView(timelineItemReplyDetails: replyDetails)
|
||||
TimelineReplyView(placement: .timeline,
|
||||
timelineItemReplyDetails: replyDetails)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
|
||||
|
||||
var sender: TimelineItemSender { get }
|
||||
|
||||
var body: String { get }
|
||||
|
||||
var properties: RoomTimelineItemProperties { get }
|
||||
}
|
||||
|
||||
|
@ -32,4 +32,8 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.audio(content)
|
||||
}
|
||||
}
|
||||
|
@ -33,4 +33,8 @@ struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.emote(content)
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,8 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.file(content)
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,8 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.image(content)
|
||||
}
|
||||
}
|
||||
|
@ -33,4 +33,8 @@ struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.notice(content)
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,8 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.text(content)
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,8 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var contentType: EventBasedMessageTimelineItemContentType {
|
||||
.video(content)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
1
changelog.d/976.feature
Normal file
1
changelog.d/976.feature
Normal file
@ -0,0 +1 @@
|
||||
Update reply composer mode UI to include message being replied to
|
Loading…
x
Reference in New Issue
Block a user