mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ struct RoomScreen: View {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
} editCancellationAction: {
|
||||
context.send(viewAction: .cancelEdit)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
timestamp: timestamp,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
||||
timestamp: timestamp,
|
||||
inGroupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -64,6 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
timestamp: timestamp,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
||||
timestamp: timestamp,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
timestamp: timestamp,
|
||||
inGroupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: isOutgoing,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user