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:
Stefan Ceriu 2023-05-31 14:51:51 +03:00 committed by GitHub
parent 87075eecfd
commit 83e3d828cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 230 additions and 93 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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))

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 }
}

View File

@ -24,6 +24,8 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
var sender: TimelineItemSender { get }
var body: String { get }
var properties: RoomTimelineItemProperties { get }
}

View File

@ -32,4 +32,8 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.audio(content)
}
}

View File

@ -33,4 +33,8 @@ struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.emote(content)
}
}

View File

@ -34,4 +34,8 @@ struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.file(content)
}
}

View File

@ -34,4 +34,8 @@ struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.image(content)
}
}

View File

@ -33,4 +33,8 @@ struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.notice(content)
}
}

View File

@ -34,4 +34,8 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.text(content)
}
}

View File

@ -34,4 +34,8 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiabl
var body: String {
content.body
}
var contentType: EventBasedMessageTimelineItemContentType {
.video(content)
}
}

View File

@ -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
View File

@ -0,0 +1 @@
Update reply composer mode UI to include message being replied to