mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -15,6 +15,7 @@
|
|||||||
"room_timeline_permalink_creation_failure" = "Failed creating the permalink";
|
"room_timeline_permalink_creation_failure" = "Failed creating the permalink";
|
||||||
|
|
||||||
"room_timeline_replying_to" = "Replying to %@";
|
"room_timeline_replying_to" = "Replying to %@";
|
||||||
|
"room_timeline_editing" = "Editing";
|
||||||
|
|
||||||
"session_verification_banner_title" = "Help keep your messages secure";
|
"session_verification_banner_title" = "Help keep your messages secure";
|
||||||
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ extension ElementL10n {
|
|||||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||||
/// Tablet
|
/// Tablet
|
||||||
public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device")
|
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
|
/// Failed creating the permalink
|
||||||
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
|
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
|
||||||
/// Replying to %@
|
/// Replying to %@
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum RoomScreenViewModelAction { }
|
|||||||
enum RoomScreenComposerMode: Equatable {
|
enum RoomScreenComposerMode: Equatable {
|
||||||
case `default`
|
case `default`
|
||||||
case reply(id: String, displayName: String)
|
case reply(id: String, displayName: String)
|
||||||
|
case edit(originalItemId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenViewAction {
|
enum RoomScreenViewAction {
|
||||||
@@ -32,6 +33,7 @@ enum RoomScreenViewAction {
|
|||||||
case sendMessage
|
case sendMessage
|
||||||
case sendReaction(key: String, eventID: String)
|
case sendReaction(key: String, eventID: String)
|
||||||
case cancelReply
|
case cancelReply
|
||||||
|
case cancelEdit
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewState: BindableState {
|
struct RoomScreenViewState: BindableState {
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
MXLog.warning("React with \(key) failed. Not implemented.")
|
MXLog.warning("React with \(key) failed. Not implemented.")
|
||||||
case .cancelReply:
|
case .cancelReply:
|
||||||
state.composerMode = .default
|
state.composerMode = .default
|
||||||
|
case .cancelEdit:
|
||||||
|
state.composerMode = .default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +141,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
switch currentComposerState {
|
switch currentComposerState {
|
||||||
case .reply(let itemId, _):
|
case .reply(let itemId, _):
|
||||||
await timelineController.sendReply(currentMessage, to: itemId)
|
await timelineController.sendReply(currentMessage, to: itemId)
|
||||||
|
case .edit(let originalItemId):
|
||||||
|
await timelineController.editMessage(currentMessage, of: originalItemId)
|
||||||
default:
|
default:
|
||||||
await timelineController.sendMessage(currentMessage)
|
await timelineController.sendMessage(currentMessage)
|
||||||
}
|
}
|
||||||
@@ -163,7 +167,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions {
|
private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions {
|
||||||
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
||||||
timelineItem is EventBasedTimelineItemProtocol else {
|
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||||
return .init(actions: [])
|
return .init(actions: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +175,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
.copy, .quote, .copyPermalink, .reply
|
.copy, .quote, .copyPermalink, .reply
|
||||||
]
|
]
|
||||||
|
|
||||||
if let item = timelineItem as? EventBasedTimelineItemProtocol, item.isOutgoing {
|
if item.isEditable {
|
||||||
|
actions.append(.edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.isOutgoing {
|
||||||
actions.append(.redact)
|
actions.append(.redact)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +195,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
switch action {
|
switch action {
|
||||||
case .copy:
|
case .copy:
|
||||||
UIPasteboard.general.string = item.text
|
UIPasteboard.general.string = item.text
|
||||||
|
case .edit:
|
||||||
|
state.bindings.composerFocused = true
|
||||||
|
state.bindings.composerText = item.text
|
||||||
|
state.composerMode = .edit(originalItemId: item.id)
|
||||||
case .quote:
|
case .quote:
|
||||||
state.bindings.composerFocused = true
|
state.bindings.composerFocused = true
|
||||||
state.bindings.composerText = "> \(item.text)"
|
state.bindings.composerText = "> \(item.text)"
|
||||||
@@ -208,10 +220,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription)
|
state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch action {
|
if action.switchToDefaultComposer {
|
||||||
case .reply:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
state.composerMode = .default
|
state.composerMode = .default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,14 @@ struct MessageComposer: View {
|
|||||||
|
|
||||||
let sendAction: () -> Void
|
let sendAction: () -> Void
|
||||||
let replyCancellationAction: () -> Void
|
let replyCancellationAction: () -> Void
|
||||||
|
let editCancellationAction: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let rect = RoundedRectangle(cornerRadius: borderRadius)
|
let rect = RoundedRectangle(cornerRadius: borderRadius)
|
||||||
VStack(alignment: .leading, spacing: 4.0) {
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
if case let .reply(_, displayName) = type {
|
header
|
||||||
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
|
|
||||||
}
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
MessageComposerTextField(placeholder: "Send a message",
|
MessageComposerTextField(placeholder: ElementL10n.roomMessagePlaceholder,
|
||||||
text: $text,
|
text: $text,
|
||||||
focused: $focused,
|
focused: $focused,
|
||||||
maxHeight: 300)
|
maxHeight: 300)
|
||||||
@@ -61,11 +60,23 @@ struct MessageComposer: View {
|
|||||||
.animation(.elementDefault, value: type)
|
.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 {
|
private var borderRadius: CGFloat {
|
||||||
switch type {
|
switch type {
|
||||||
case .default:
|
case .default:
|
||||||
return 28.0
|
return 28.0
|
||||||
case .reply:
|
case .reply, .edit:
|
||||||
return 12.0
|
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 {
|
struct MessageComposer_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
body.preferredColorScheme(.light)
|
body.preferredColorScheme(.light)
|
||||||
@@ -108,7 +141,8 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: true,
|
sendingDisabled: true,
|
||||||
type: .default,
|
type: .default,
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
replyCancellationAction: { })
|
replyCancellationAction: { },
|
||||||
|
editCancellationAction: { })
|
||||||
|
|
||||||
MessageComposer(text: .constant("Some message"),
|
MessageComposer(text: .constant("Some message"),
|
||||||
focused: .constant(false),
|
focused: .constant(false),
|
||||||
@@ -116,7 +150,16 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
type: .reply(id: UUID().uuidString,
|
type: .reply(id: UUID().uuidString,
|
||||||
displayName: "John Doe"),
|
displayName: "John Doe"),
|
||||||
sendAction: { },
|
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)
|
.tint(.element.accent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ struct RoomScreen: View {
|
|||||||
sendMessage()
|
sendMessage()
|
||||||
} replyCancellationAction: {
|
} replyCancellationAction: {
|
||||||
context.send(viewAction: .cancelReply)
|
context.send(viewAction: .cancelReply)
|
||||||
|
} editCancellationAction: {
|
||||||
|
context.send(viewAction: .cancelEdit)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: senderId)
|
senderId: senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: false,
|
||||||
senderId: senderId)
|
senderId: senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
source: nil,
|
source: nil,
|
||||||
image: UIImage(systemName: "photo")))
|
image: UIImage(systemName: "photo")))
|
||||||
@@ -77,6 +78,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
source: nil,
|
source: nil,
|
||||||
image: nil))
|
image: nil))
|
||||||
@@ -86,6 +88,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
source: nil,
|
source: nil,
|
||||||
image: nil,
|
image: nil,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: senderId)
|
senderId: senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: senderId)
|
senderId: senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: isOutgoing,
|
||||||
senderId: senderId)
|
senderId: senderId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
duration: 21,
|
duration: 21,
|
||||||
source: nil,
|
source: nil,
|
||||||
@@ -89,6 +90,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
duration: 22,
|
duration: 22,
|
||||||
source: nil,
|
source: nil,
|
||||||
@@ -100,6 +102,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "Bob",
|
senderId: "Bob",
|
||||||
duration: 23,
|
duration: 23,
|
||||||
source: nil,
|
source: nil,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct TimelineItemContextMenuActions {
|
|||||||
|
|
||||||
enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
||||||
case copy
|
case copy
|
||||||
|
case edit
|
||||||
case quote
|
case quote
|
||||||
case copyPermalink
|
case copyPermalink
|
||||||
case redact
|
case redact
|
||||||
@@ -30,6 +31,15 @@ enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
|||||||
case viewSource
|
case viewSource
|
||||||
|
|
||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
|
|
||||||
|
var switchToDefaultComposer: Bool {
|
||||||
|
switch self {
|
||||||
|
case .reply, .edit:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct TimelineItemContextMenu: View {
|
public struct TimelineItemContextMenu: View {
|
||||||
@@ -53,6 +63,10 @@ public struct TimelineItemContextMenu: View {
|
|||||||
Button { callback(item) } label: {
|
Button { callback(item) } label: {
|
||||||
Label(ElementL10n.actionCopy, systemImage: "doc.on.doc")
|
Label(ElementL10n.actionCopy, systemImage: "doc.on.doc")
|
||||||
}
|
}
|
||||||
|
case .edit:
|
||||||
|
Button { callback(item) } label: {
|
||||||
|
Label(ElementL10n.edit, systemImage: "pencil")
|
||||||
|
}
|
||||||
case .quote:
|
case .quote:
|
||||||
Button { callback(item) } label: {
|
Button { callback(item) } label: {
|
||||||
Label(ElementL10n.actionQuote, systemImage: "quote.bubble")
|
Label(ElementL10n.actionQuote, systemImage: "quote.bubble")
|
||||||
|
|||||||
@@ -111,17 +111,17 @@ struct TimelineItemList: View {
|
|||||||
.onReceive(scrollToBottomPublisher) {
|
.onReceive(scrollToBottomPublisher) {
|
||||||
scrollToBottom(animated: true)
|
scrollToBottom(animated: true)
|
||||||
}
|
}
|
||||||
.onChange(of: context.viewState.items.count) { _ in
|
.onChange(of: context.viewState.items) { items in
|
||||||
guard !context.viewState.items.isEmpty,
|
defer {
|
||||||
context.viewState.items.count != timelineItems.count else {
|
// update the items anyway
|
||||||
return
|
timelineItems = items
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pin to the bottom if empty
|
// Pin to the bottom if empty
|
||||||
if timelineItems.isEmpty {
|
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)
|
let pinnedItem = PinnedItem(id: lastItem.id, anchor: .bottom, animated: false)
|
||||||
timelineItems = context.viewState.items
|
timelineItems = items
|
||||||
self.pinnedItem = pinnedItem
|
self.pinnedItem = pinnedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +131,9 @@ struct TimelineItemList: View {
|
|||||||
// Pin to the new bottom if visible
|
// Pin to the new bottom if visible
|
||||||
if let currentLastItem = timelineItems.last,
|
if let currentLastItem = timelineItems.last,
|
||||||
visibleItemIdentifiers.contains(currentLastItem.id),
|
visibleItemIdentifiers.contains(currentLastItem.id),
|
||||||
let newLastItem = context.viewState.items.last {
|
let newLastItem = items.last {
|
||||||
let pinnedItem = PinnedItem(id: newLastItem.id, anchor: .bottom, animated: false)
|
let pinnedItem = PinnedItem(id: newLastItem.id, anchor: .bottom, animated: false)
|
||||||
timelineItems = context.viewState.items
|
timelineItems = items
|
||||||
self.pinnedItem = pinnedItem
|
self.pinnedItem = pinnedItem
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -143,20 +143,12 @@ struct TimelineItemList: View {
|
|||||||
if let currentFirstItem = timelineItems.first,
|
if let currentFirstItem = timelineItems.first,
|
||||||
visibleItemIdentifiers.contains(currentFirstItem.id) {
|
visibleItemIdentifiers.contains(currentFirstItem.id) {
|
||||||
let pinnedItem = PinnedItem(id: currentFirstItem.id, anchor: .top, animated: false)
|
let pinnedItem = PinnedItem(id: currentFirstItem.id, anchor: .top, animated: false)
|
||||||
timelineItems = context.viewState.items
|
timelineItems = items
|
||||||
self.pinnedItem = pinnedItem
|
self.pinnedItem = pinnedItem
|
||||||
|
|
||||||
return
|
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
|
.background(GeometryReader { geo in
|
||||||
Color.clear.preference(key: ViewFramePreferenceKey.self, value: [geo.frame(in: .global)])
|
Color.clear.preference(key: ViewFramePreferenceKey.self, value: [geo.frame(in: .global)])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ struct MockRoomProxy: RoomProxyProtocol {
|
|||||||
.failure(.failedSendingMessage)
|
.failure(.failedSendingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
.failure(.failedSendingMessage)
|
||||||
|
}
|
||||||
|
|
||||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
.failure(.failedRedactingEvent)
|
.failure(.failedRedactingEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,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> {
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
let transactionID = genTransactionId()
|
let transactionID = genTransactionId()
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
func sendMessage(_ message: String, inReplyToEventId: String?) 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>
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "10:10 AM",
|
timestamp: "10:10 AM",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Jacob",
|
senderDisplayName: "Jacob",
|
||||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||||
@@ -38,6 +39,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
inGroupState: .beginning,
|
inGroupState: .beginning,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Helena",
|
senderDisplayName: "Helena",
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
@@ -48,6 +50,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
inGroupState: .end,
|
inGroupState: .end,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Helena",
|
senderDisplayName: "Helena",
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
@@ -61,6 +64,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Helena"),
|
senderDisplayName: "Helena"),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
@@ -68,6 +72,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
inGroupState: .beginning,
|
inGroupState: .beginning,
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Bob"),
|
senderDisplayName: "Bob"),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
@@ -75,6 +80,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
inGroupState: .end,
|
inGroupState: .end,
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
|
isEditable: true,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Bob",
|
senderDisplayName: "Bob",
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
@@ -91,6 +97,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
inGroupState: .single,
|
inGroupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
senderId: "",
|
senderId: "",
|
||||||
senderDisplayName: "Helena")
|
senderDisplayName: "Helena")
|
||||||
]
|
]
|
||||||
@@ -107,6 +114,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
|
|
||||||
func sendReply(_ message: String, to itemId: String) async { }
|
func sendReply(_ message: String, to itemId: String) async { }
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, of itemId: String) async { }
|
||||||
|
|
||||||
func redact(_ eventID: String) async { }
|
func redact(_ eventID: String) async { }
|
||||||
|
|
||||||
func debugDescriptionFor(_ itemId: String) -> String {
|
func debugDescriptionFor(_ itemId: String) -> String {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
.failure(.failedSendingMessage)
|
.failure(.failedSendingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||||
|
.failure(.failedSendingMessage)
|
||||||
|
}
|
||||||
|
|
||||||
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
|
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||||
.failure(.failedRedactingItem)
|
.failure(.failedRedactingItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, of itemId: String) async {
|
||||||
|
switch await timelineProvider.editMessage(newMessage, originalItemId: itemId) {
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func redact(_ eventID: String) async {
|
func redact(_ eventID: String) async {
|
||||||
switch await timelineProvider.redact(eventID) {
|
switch await timelineProvider.redact(eventID) {
|
||||||
default:
|
default:
|
||||||
@@ -273,7 +280,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
switch await mediaProvider.loadImageFromSource(source) {
|
switch await mediaProvider.loadImageFromSource(source) {
|
||||||
case .success(let image):
|
case .success(let image):
|
||||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ protocol RoomTimelineControllerProtocol {
|
|||||||
|
|
||||||
func sendReply(_ message: String, to itemId: String) async
|
func sendReply(_ message: String, to itemId: String) async
|
||||||
|
|
||||||
|
func editMessage(_ newMessage: String, of itemId: String) async
|
||||||
|
|
||||||
func redact(_ eventID: String) async
|
func redact(_ eventID: String) async
|
||||||
|
|
||||||
func debugDescriptionFor(_ itemId: String) -> String
|
func debugDescriptionFor(_ itemId: String) -> String
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||||
switch await roomProxy.redact(eventID) {
|
switch await roomProxy.redact(eventID) {
|
||||||
case .success:
|
case .success:
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ protocol RoomTimelineProviderProtocol {
|
|||||||
|
|
||||||
func sendMessage(_ message: String, inReplyToItemId: String?) 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>
|
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ struct MessageTimelineItem<Content: MessageContentProtocol> {
|
|||||||
item.content().asMessage()?.isEdited() == true
|
item.content().asMessage()?.isEdited() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEditable: Bool {
|
||||||
|
item.isEditable()
|
||||||
|
}
|
||||||
|
|
||||||
var inReplyTo: String? {
|
var inReplyTo: String? {
|
||||||
item.content().asMessage()?.inReplyTo()
|
item.content().asMessage()?.inReplyTo()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ struct EventTimelineItemProxy: CustomDebugStringConvertible {
|
|||||||
item.isOwn()
|
item.isOwn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEditable: Bool {
|
||||||
|
item.isEditable()
|
||||||
|
}
|
||||||
|
|
||||||
var sender: String {
|
var sender: String {
|
||||||
item.sender()
|
item.sender()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
|||||||
var shouldShowSenderDetails: Bool { get }
|
var shouldShowSenderDetails: Bool { get }
|
||||||
var inGroupState: TimelineItemInGroupState { get }
|
var inGroupState: TimelineItemInGroupState { get }
|
||||||
var isOutgoing: Bool { get }
|
var isOutgoing: Bool { get }
|
||||||
|
var isEditable: Bool { get }
|
||||||
|
|
||||||
var senderId: String { get }
|
var senderId: String { get }
|
||||||
var senderDisplayName: String? { get set }
|
var senderDisplayName: String? { get set }
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, E
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
|||||||
let timestamp: String
|
let timestamp: String
|
||||||
let inGroupState: TimelineItemInGroupState
|
let inGroupState: TimelineItemInGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
let isEditable: Bool
|
||||||
|
|
||||||
let senderId: String
|
let senderId: String
|
||||||
var senderDisplayName: String?
|
var senderDisplayName: String?
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: eventItemProxy.isEditable,
|
||||||
senderId: eventItemProxy.sender,
|
senderId: eventItemProxy.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -114,6 +115,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: eventItemProxy.isEditable,
|
||||||
senderId: eventItemProxy.sender,
|
senderId: eventItemProxy.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -134,6 +136,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: eventItemProxy.isEditable,
|
||||||
senderId: eventItemProxy.sender,
|
senderId: eventItemProxy.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -155,6 +158,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: message.isEditable,
|
||||||
senderId: message.sender,
|
senderId: message.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -178,6 +182,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: message.isEditable,
|
||||||
senderId: message.sender,
|
senderId: message.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -207,6 +212,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: message.isEditable,
|
||||||
senderId: message.sender,
|
senderId: message.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -236,6 +242,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: message.isEditable,
|
||||||
senderId: message.sender,
|
senderId: message.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
@@ -257,6 +264,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||||
inGroupState: inGroupState,
|
inGroupState: inGroupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
isEditable: message.isEditable,
|
||||||
senderId: message.sender,
|
senderId: message.sender,
|
||||||
senderDisplayName: displayName,
|
senderDisplayName: displayName,
|
||||||
senderAvatar: avatarImage,
|
senderAvatar: avatarImage,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Timeline: Implement message editing via context menu.
|
||||||
Reference in New Issue
Block a user