Message editing (#298)

* Add translation for editing mode

* Add `is_editable` flag to timeline item

* Add edit api implementation

* Add edit context menu action

* Add changelog

* Update Rust package

* Add isEditable into video timeline item

* Fix cyclomatic complexity

* Fix video item thumbnail loading

* Fix not updating timeline layout
This commit is contained in:
ismailgulek
2022-11-10 14:41:38 +03:00
committed by GitHub
parent 34a5b53038
commit 05233dcf5b
36 changed files with 189 additions and 31 deletions

View File

@@ -15,6 +15,7 @@
"room_timeline_permalink_creation_failure" = "Failed creating the permalink";
"room_timeline_replying_to" = "Replying to %@";
"room_timeline_editing" = "Editing";
"session_verification_banner_title" = "Help keep your messages secure";
"session_verification_banner_message" = "Looks like youre using a new device. Verify its you.";

View File

@@ -24,6 +24,8 @@ extension ElementL10n {
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
/// Tablet
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
/// Editing
public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing")
/// Failed creating the permalink
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
/// Replying to %@

View File

@@ -22,6 +22,7 @@ enum RoomScreenViewModelAction { }
enum RoomScreenComposerMode: Equatable {
case `default`
case reply(id: String, displayName: String)
case edit(originalItemId: String)
}
enum RoomScreenViewAction {
@@ -32,6 +33,7 @@ enum RoomScreenViewAction {
case sendMessage
case sendReaction(key: String, eventID: String)
case cancelReply
case cancelEdit
}
struct RoomScreenViewState: BindableState {

View File

@@ -106,6 +106,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
MXLog.warning("React with \(key) failed. Not implemented.")
case .cancelReply:
state.composerMode = .default
case .cancelEdit:
state.composerMode = .default
}
}
@@ -139,6 +141,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch currentComposerState {
case .reply(let itemId, _):
await timelineController.sendReply(currentMessage, to: itemId)
case .edit(let originalItemId):
await timelineController.editMessage(currentMessage, of: originalItemId)
default:
await timelineController.sendMessage(currentMessage)
}
@@ -163,15 +167,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions {
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
timelineItem is EventBasedTimelineItemProtocol else {
let item = timelineItem as? EventBasedTimelineItemProtocol else {
return .init(actions: [])
}
var actions: [TimelineItemContextMenuAction] = [
.copy, .quote, .copyPermalink, .reply
]
if item.isEditable {
actions.append(.edit)
}
if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isOutgoing {
if item.isOutgoing {
actions.append(.redact)
}
@@ -187,6 +195,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch action {
case .copy:
UIPasteboard.general.string = item.text
case .edit:
state.bindings.composerFocused = true
state.bindings.composerText = item.text
state.composerMode = .edit(originalItemId: item.id)
case .quote:
state.bindings.composerFocused = true
state.bindings.composerText = "> \(item.text)"
@@ -208,10 +220,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription)
}
switch action {
case .reply:
break
default:
if action.switchToDefaultComposer {
state.composerMode = .default
}
}

View File

@@ -24,15 +24,14 @@ struct MessageComposer: View {
let sendAction: () -> Void
let replyCancellationAction: () -> Void
let editCancellationAction: () -> Void
var body: some View {
let rect = RoundedRectangle(cornerRadius: borderRadius)
VStack(alignment: .leading, spacing: 4.0) {
if case let .reply(_, displayName) = type {
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
}
header
HStack(alignment: .center) {
MessageComposerTextField(placeholder: "Send a message",
MessageComposerTextField(placeholder: ElementL10n.roomMessagePlaceholder,
text: $text,
focused: $focused,
maxHeight: 300)
@@ -60,12 +59,24 @@ struct MessageComposer: View {
.clipShape(rect)
.animation(.elementDefault, value: type)
}
@ViewBuilder
private var header: some View {
switch type {
case .reply(_, let displayName):
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
case .edit:
MessageComposerEditHeader(action: editCancellationAction)
case .default:
EmptyView()
}
}
private var borderRadius: CGFloat {
switch type {
case .default:
return 28.0
case .reply:
case .reply, .edit:
return 12.0
}
}
@@ -94,6 +105,28 @@ private struct MessageComposerReplyHeader: View {
}
}
private struct MessageComposerEditHeader: View {
let action: () -> Void
var body: some View {
HStack(alignment: .center) {
Label(ElementL10n.roomTimelineEditing, systemImage: "pencil")
.font(.element.caption2)
.foregroundColor(.element.secondaryContent)
.lineLimit(1)
Spacer()
Button {
action()
} label: {
Image(systemName: "x.circle")
.font(.element.callout)
.foregroundColor(.element.secondaryContent)
.padding(4.0)
}
}
}
}
struct MessageComposer_Previews: PreviewProvider {
static var previews: some View {
body.preferredColorScheme(.light)
@@ -108,7 +141,8 @@ struct MessageComposer_Previews: PreviewProvider {
sendingDisabled: true,
type: .default,
sendAction: { },
replyCancellationAction: { })
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("Some message"),
focused: .constant(false),
@@ -116,7 +150,16 @@ struct MessageComposer_Previews: PreviewProvider {
type: .reply(id: UUID().uuidString,
displayName: "John Doe"),
sendAction: { },
replyCancellationAction: { })
replyCancellationAction: { },
editCancellationAction: { })
MessageComposer(text: .constant("Some message"),
focused: .constant(false),
sendingDisabled: false,
type: .edit(originalItemId: UUID().uuidString),
sendAction: { },
replyCancellationAction: { },
editCancellationAction: { })
}
.tint(.element.accent)
}

View File

@@ -31,6 +31,8 @@ struct RoomScreen: View {
sendMessage()
} replyCancellationAction: {
context.send(viewAction: .cancelReply)
} editCancellationAction: {
context.send(viewAction: .cancelEdit)
}
.padding()
}

View File

@@ -64,6 +64,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
}
}

View File

@@ -86,6 +86,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
inGroupState: .single,
isOutgoing: isOutgoing,
isEditable: false,
senderId: senderId)
}
}

View File

@@ -68,6 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
source: nil,
image: UIImage(systemName: "photo")))
@@ -77,6 +78,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
source: nil,
image: nil))
@@ -86,6 +88,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
source: nil,
image: nil,

View File

@@ -64,6 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
}
}

View File

@@ -52,6 +52,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: senderId)
}
}

View File

@@ -73,6 +73,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
inGroupState: .single,
isOutgoing: isOutgoing,
isEditable: isOutgoing,
senderId: senderId)
}
}

View File

@@ -78,6 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
duration: 21,
source: nil,
@@ -89,6 +90,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
duration: 22,
source: nil,
@@ -100,6 +102,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
timestamp: "Now",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "Bob",
duration: 23,
source: nil,

View File

@@ -23,6 +23,7 @@ struct TimelineItemContextMenuActions {
enum TimelineItemContextMenuAction: Identifiable, Hashable {
case copy
case edit
case quote
case copyPermalink
case redact
@@ -30,6 +31,15 @@ enum TimelineItemContextMenuAction: Identifiable, Hashable {
case viewSource
var id: Self { self }
var switchToDefaultComposer: Bool {
switch self {
case .reply, .edit:
return false
default:
return true
}
}
}
public struct TimelineItemContextMenu: View {
@@ -53,6 +63,10 @@ public struct TimelineItemContextMenu: View {
Button { callback(item) } label: {
Label(ElementL10n.actionCopy, systemImage: "doc.on.doc")
}
case .edit:
Button { callback(item) } label: {
Label(ElementL10n.edit, systemImage: "pencil")
}
case .quote:
Button { callback(item) } label: {
Label(ElementL10n.actionQuote, systemImage: "quote.bubble")

View File

@@ -111,17 +111,17 @@ struct TimelineItemList: View {
.onReceive(scrollToBottomPublisher) {
scrollToBottom(animated: true)
}
.onChange(of: context.viewState.items.count) { _ in
guard !context.viewState.items.isEmpty,
context.viewState.items.count != timelineItems.count else {
return
.onChange(of: context.viewState.items) { items in
defer {
// update the items anyway
timelineItems = items
}
// Pin to the bottom if empty
if timelineItems.isEmpty {
if let lastItem = context.viewState.items.last {
if let lastItem = items.last {
let pinnedItem = PinnedItem(id: lastItem.id, anchor: .bottom, animated: false)
timelineItems = context.viewState.items
timelineItems = items
self.pinnedItem = pinnedItem
}
@@ -131,9 +131,9 @@ struct TimelineItemList: View {
// Pin to the new bottom if visible
if let currentLastItem = timelineItems.last,
visibleItemIdentifiers.contains(currentLastItem.id),
let newLastItem = context.viewState.items.last {
let newLastItem = items.last {
let pinnedItem = PinnedItem(id: newLastItem.id, anchor: .bottom, animated: false)
timelineItems = context.viewState.items
timelineItems = items
self.pinnedItem = pinnedItem
return
@@ -143,20 +143,12 @@ struct TimelineItemList: View {
if let currentFirstItem = timelineItems.first,
visibleItemIdentifiers.contains(currentFirstItem.id) {
let pinnedItem = PinnedItem(id: currentFirstItem.id, anchor: .top, animated: false)
timelineItems = context.viewState.items
timelineItems = items
self.pinnedItem = pinnedItem
return
}
// Otherwise just update the items
timelineItems = context.viewState.items
}
.onChange(of: context.viewState.items, perform: { items in
if timelineItems != items {
timelineItems = items
}
})
.background(GeometryReader { geo in
Color.clear.preference(key: ViewFramePreferenceKey.self, value: [geo.frame(in: .global)])
})

View File

@@ -65,6 +65,10 @@ struct MockRoomProxy: RoomProxyProtocol {
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingMessage)
}
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingMessage)
}
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
.failure(.failedRedactingEvent)

View File

@@ -188,6 +188,24 @@ class RoomProxy: RoomProxyProtocol {
}
}
}
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> {
sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true)
defer {
sendMessageBgTask?.stop()
}
let transactionId = genTransactionId()
return await Task.dispatch(on: .global()) {
do {
try self.room.edit(newMsg: newMessage, originalEventId: originalEventId, txnId: transactionId)
return .success(())
} catch {
return .failure(.failedSendingMessage)
}
}
}
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
let transactionID = genTransactionId()

View File

@@ -59,6 +59,8 @@ protocol RoomProxyProtocol {
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError>
func sendMessage(_ message: String, inReplyToEventId: String?) async -> Result<Void, RoomProxyError>
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
}

View File

@@ -30,6 +30,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "10:10 AM",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "",
senderDisplayName: "Jacob",
properties: RoomTimelineItemProperties(isEdited: true)),
@@ -38,6 +39,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "10:11 AM",
inGroupState: .beginning,
isOutgoing: false,
isEditable: false,
senderId: "",
senderDisplayName: "Helena",
properties: RoomTimelineItemProperties(reactions: [
@@ -48,6 +50,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "10:11 AM",
inGroupState: .end,
isOutgoing: false,
isEditable: false,
senderId: "",
senderDisplayName: "Helena",
properties: RoomTimelineItemProperties(reactions: [
@@ -61,6 +64,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "5 PM",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "",
senderDisplayName: "Helena"),
TextRoomTimelineItem(id: UUID().uuidString,
@@ -68,6 +72,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "5 PM",
inGroupState: .beginning,
isOutgoing: true,
isEditable: true,
senderId: "",
senderDisplayName: "Bob"),
TextRoomTimelineItem(id: UUID().uuidString,
@@ -75,6 +80,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "5 PM",
inGroupState: .end,
isOutgoing: true,
isEditable: true,
senderId: "",
senderDisplayName: "Bob",
properties: RoomTimelineItemProperties(reactions: [
@@ -91,6 +97,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
timestamp: "5 PM",
inGroupState: .single,
isOutgoing: false,
isEditable: false,
senderId: "",
senderDisplayName: "Helena")
]
@@ -106,6 +113,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func sendMessage(_ message: String) async { }
func sendReply(_ message: String, to itemId: String) async { }
func editMessage(_ newMessage: String, of itemId: String) async { }
func redact(_ eventID: String) async { }

View File

@@ -30,6 +30,10 @@ struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedSendingMessage)
}
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedSendingMessage)
}
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedRedactingItem)

View File

@@ -121,6 +121,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
break
}
}
func editMessage(_ newMessage: String, of itemId: String) async {
switch await timelineProvider.editMessage(newMessage, originalItemId: itemId) {
default:
break
}
}
func redact(_ eventID: String) async {
switch await timelineProvider.redact(eventID) {
@@ -273,7 +280,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
switch await mediaProvider.loadImageFromSource(source) {
case .success(let image):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? ImageRoomTimelineItem else {
var item = timelineItems[index] as? VideoRoomTimelineItem else {
return
}

View File

@@ -45,6 +45,8 @@ protocol RoomTimelineControllerProtocol {
func sendReply(_ message: String, to itemId: String) async
func editMessage(_ newMessage: String, of itemId: String) async
func redact(_ eventID: String) async
func debugDescriptionFor(_ itemId: String) -> String

View File

@@ -84,6 +84,15 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
return .failure(.failedSendingMessage)
}
}
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError> {
switch await roomProxy.editMessage(newMessage, originalEventId: originalItemId) {
case .success:
return .success(())
case .failure:
return .failure(.failedSendingMessage)
}
}
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
switch await roomProxy.redact(eventID) {

View File

@@ -33,6 +33,8 @@ protocol RoomTimelineProviderProtocol {
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError>
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError>
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError>
}

View File

@@ -45,6 +45,10 @@ struct MessageTimelineItem<Content: MessageContentProtocol> {
var isEdited: Bool {
item.content().asMessage()?.isEdited() == true
}
var isEditable: Bool {
item.isEditable()
}
var inReplyTo: String? {
item.content().asMessage()?.inReplyTo()

View File

@@ -78,6 +78,10 @@ struct EventTimelineItemProxy: CustomDebugStringConvertible {
var isOwn: Bool {
item.isOwn()
}
var isEditable: Bool {
item.isEditable()
}
var sender: String {
item.sender()

View File

@@ -52,6 +52,7 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
var shouldShowSenderDetails: Bool { get }
var inGroupState: TimelineItemInGroupState { get }
var isOutgoing: Bool { get }
var isEditable: Bool { get }
var senderId: String { get }
var senderDisplayName: String? { get set }

View File

@@ -24,6 +24,7 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -29,6 +29,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -23,6 +23,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -24,6 +24,7 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -23,6 +23,7 @@ struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, E
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -24,6 +24,7 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -23,6 +23,7 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let timestamp: String
let inGroupState: TimelineItemInGroupState
let isOutgoing: Bool
let isEditable: Bool
let senderId: String
var senderDisplayName: String?

View File

@@ -98,6 +98,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
senderId: eventItemProxy.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -114,6 +115,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
senderId: eventItemProxy.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -134,6 +136,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
senderId: eventItemProxy.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -155,6 +158,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: message.isEditable,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -178,6 +182,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: message.isEditable,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -207,6 +212,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: message.isEditable,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -236,6 +242,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: message.isEditable,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,
@@ -257,6 +264,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
inGroupState: inGroupState,
isOutgoing: isOutgoing,
isEditable: message.isEditable,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage,

1
changelog.d/252.feature Normal file
View File

@@ -0,0 +1 @@
Timeline: Implement message editing via context menu.