mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Add support for adding/editing/removing media captions. (#3547)
* Add support for editing media captions. * Add composer snapshots. * PR comment.
This commit is contained in:
@@ -284,9 +284,11 @@ extension FormatType {
|
||||
}
|
||||
|
||||
enum ComposerMode: Equatable {
|
||||
enum EditType { case `default`, addCaption, editCaption }
|
||||
|
||||
case `default`
|
||||
case reply(eventID: String, replyDetails: TimelineItemReplyDetails, isThread: Bool)
|
||||
case edit(originalEventOrTransactionID: EventOrTransactionId)
|
||||
case edit(originalEventOrTransactionID: EventOrTransactionId, type: EditType)
|
||||
case recordVoiceMessage(state: AudioRecorderState)
|
||||
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool)
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
case .newMessage:
|
||||
set(mode: .default)
|
||||
case .edit(let eventID):
|
||||
set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID)))
|
||||
set(mode: .edit(originalEventOrTransactionID: .eventId(eventId: eventID), type: .default))
|
||||
case .reply(let eventID):
|
||||
set(mode: .reply(eventID: eventID, replyDetails: .loading(eventID: eventID), isThread: false))
|
||||
replyLoadingTask = Task {
|
||||
@@ -323,7 +323,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
switch state.composerMode {
|
||||
case .default:
|
||||
type = .newMessage
|
||||
case .edit(.eventId(let originalEventID)):
|
||||
case .edit(.eventId(let originalEventID), .default):
|
||||
type = .edit(eventID: originalEventID)
|
||||
case .reply(let eventID, _, _):
|
||||
type = .reply(eventID: eventID)
|
||||
|
||||
@@ -82,8 +82,8 @@ struct MessageComposer: View {
|
||||
switch mode {
|
||||
case .reply(_, let replyDetails, _):
|
||||
MessageComposerReplyHeader(replyDetails: replyDetails, action: cancellationAction)
|
||||
case .edit:
|
||||
MessageComposerEditHeader(action: cancellationAction)
|
||||
case .edit(_, let editType):
|
||||
MessageComposerEditHeader(editType: editType, action: cancellationAction)
|
||||
case .recordVoiceMessage, .previewVoiceMessage, .default:
|
||||
EmptyView()
|
||||
}
|
||||
@@ -152,14 +152,20 @@ private struct MessageComposerReplyHeader: View {
|
||||
}
|
||||
|
||||
private struct MessageComposerEditHeader: View {
|
||||
let editType: ComposerMode.EditType
|
||||
let action: () -> Void
|
||||
|
||||
private var title: String {
|
||||
switch editType {
|
||||
case .default: L10n.commonEditing
|
||||
case .addCaption: L10n.commonAddingCaption
|
||||
case .editCaption: L10n.commonEditingCaption
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
Label(L10n.commonEditing,
|
||||
icon: \.editSolid,
|
||||
iconSize: .xSmall,
|
||||
relativeTo: .compound.bodySMSemibold)
|
||||
Label(title, icon: \.editSolid, iconSize: .xSmall, relativeTo: .compound.bodySMSemibold)
|
||||
.labelStyle(MessageComposerHeaderLabelStyle())
|
||||
Spacer()
|
||||
Button(action: action) {
|
||||
@@ -294,13 +300,20 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
||||
messageComposer()
|
||||
|
||||
messageComposer(.init(string: "Some message"),
|
||||
mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString)))
|
||||
mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .default))
|
||||
|
||||
messageComposer(mode: .reply(eventID: UUID().uuidString,
|
||||
replyDetails: .loaded(sender: .init(id: "Kirk"),
|
||||
eventID: "123",
|
||||
eventContent: .message(.text(.init(body: "Text: Where the wild things are")))),
|
||||
isThread: false))
|
||||
|
||||
Color.clear.frame(height: 20)
|
||||
|
||||
messageComposer(.init(string: "Some new caption"),
|
||||
mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .addCaption))
|
||||
messageComposer(.init(string: "Some updated caption"),
|
||||
mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .editCaption))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
@@ -100,10 +100,7 @@ class TimelineInteractionHandler {
|
||||
|
||||
switch action {
|
||||
case .copy:
|
||||
guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { return }
|
||||
UIPasteboard.general.string = messageTimelineItem.body
|
||||
case .edit:
|
||||
switch timelineItem {
|
||||
@@ -118,6 +115,19 @@ class TimelineInteractionHandler {
|
||||
default:
|
||||
MXLog.error("Cannot edit item with id: \(timelineItem.id)")
|
||||
}
|
||||
case .addCaption, .editCaption:
|
||||
switch timelineItem {
|
||||
case let messageTimelineItem as EventBasedMessageTimelineItemProtocol:
|
||||
processEditMessageEvent(messageTimelineItem)
|
||||
default:
|
||||
MXLog.error("Cannot add/edit caption on item with id: \(timelineItem.id)")
|
||||
}
|
||||
case .removeCaption:
|
||||
guard case let .event(_, eventOrTransactionID) = timelineItem.id else {
|
||||
MXLog.error("Failed removing caption, missing event ID")
|
||||
return
|
||||
}
|
||||
Task { await timelineController.removeCaption(eventOrTransactionID) }
|
||||
case .copyPermalink:
|
||||
guard let eventID = eventTimelineItem.id.eventID else {
|
||||
actionsSubject.send(.displayErrorToast(L10n.errorFailedCreatingThePermalink))
|
||||
@@ -133,17 +143,10 @@ class TimelineInteractionHandler {
|
||||
UIPasteboard.general.url = permalinkURL
|
||||
}
|
||||
case .redact:
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
Task {
|
||||
await timelineController.redact(eventOrTransactionID)
|
||||
}
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() }
|
||||
Task { await timelineController.redact(eventOrTransactionID) }
|
||||
case .reply:
|
||||
guard let eventID = eventTimelineItem.id.eventID else {
|
||||
return
|
||||
}
|
||||
guard let eventID = eventTimelineItem.id.eventID else { return }
|
||||
|
||||
let replyInfo = buildReplyInfo(for: eventTimelineItem)
|
||||
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, eventID: eventID, eventContent: replyInfo.type)
|
||||
@@ -156,21 +159,14 @@ class TimelineInteractionHandler {
|
||||
MXLog.info("Showing debug info for \(eventTimelineItem.id)")
|
||||
actionsSubject.send(.showDebugInfo(debugInfo))
|
||||
case .retryDecryption(let sessionID):
|
||||
Task {
|
||||
await timelineController.retryDecryption(for: sessionID)
|
||||
}
|
||||
Task { await timelineController.retryDecryption(for: sessionID) }
|
||||
case .report:
|
||||
actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id))
|
||||
case .react:
|
||||
displayEmojiPicker(for: itemID)
|
||||
case .toggleReaction(let key):
|
||||
Task {
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
await timelineController.toggleReaction(key, to: eventOrTransactionID)
|
||||
}
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() }
|
||||
Task { await timelineController.toggleReaction(key, to: eventOrTransactionID) }
|
||||
case .endPoll(let pollStartID):
|
||||
endPoll(pollStartID: pollStartID)
|
||||
case .pin:
|
||||
@@ -202,18 +198,35 @@ class TimelineInteractionHandler {
|
||||
|
||||
let text: String
|
||||
var htmlText: String?
|
||||
var editType = ComposerMode.EditType.default
|
||||
switch messageTimelineItem.contentType {
|
||||
case .text(let content):
|
||||
text = content.body
|
||||
htmlText = content.formattedBodyHTMLString
|
||||
case .emote(let content):
|
||||
text = "/me " + content.body
|
||||
case .audio(let content):
|
||||
text = content.caption ?? ""
|
||||
htmlText = content.formattedCaptionHTMLString
|
||||
editType = text.isEmpty ? .addCaption : .editCaption
|
||||
case .file(let content):
|
||||
text = content.caption ?? ""
|
||||
htmlText = content.formattedCaptionHTMLString
|
||||
editType = text.isEmpty ? .addCaption : .editCaption
|
||||
case .image(let content):
|
||||
text = content.caption ?? ""
|
||||
htmlText = content.formattedCaptionHTMLString
|
||||
editType = text.isEmpty ? .addCaption : .editCaption
|
||||
case .video(let content):
|
||||
text = content.caption ?? ""
|
||||
htmlText = content.formattedCaptionHTMLString
|
||||
editType = text.isEmpty ? .addCaption : .editCaption
|
||||
default:
|
||||
text = messageTimelineItem.body
|
||||
}
|
||||
|
||||
// Always update the mode first and then the text so that the composer has time to save the text draft
|
||||
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalEventOrTransactionID: eventOrTransactionID))))
|
||||
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalEventOrTransactionID: eventOrTransactionID, type: editType))))
|
||||
actionsSubject.send(.composer(action: .setText(plainText: text, htmlText: htmlText)))
|
||||
}
|
||||
|
||||
|
||||
@@ -606,11 +606,17 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
html: html,
|
||||
inReplyToEventID: eventID,
|
||||
intentionalMentions: intentionalMentions)
|
||||
case .edit(let originalEventOrTransactionID):
|
||||
case .edit(let originalEventOrTransactionID, .default):
|
||||
await timelineController.edit(originalEventOrTransactionID,
|
||||
message: message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions)
|
||||
case .edit(let originalEventOrTransactionID, .addCaption),
|
||||
.edit(let originalEventOrTransactionID, .editCaption):
|
||||
await timelineController.editCaption(originalEventOrTransactionID,
|
||||
message: message,
|
||||
html: html,
|
||||
intentionalMentions: intentionalMentions)
|
||||
case .default:
|
||||
switch slashCommand(message: message) {
|
||||
case .join:
|
||||
|
||||
@@ -57,6 +57,9 @@ struct TimelineItemMenuReaction: Hashable {
|
||||
enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
case copy
|
||||
case edit
|
||||
case addCaption
|
||||
case editCaption
|
||||
case removeCaption
|
||||
case copyPermalink
|
||||
case redact
|
||||
case reply(isThread: Bool)
|
||||
@@ -76,7 +79,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
/// Whether the item should cancel a reply/edit occurring in the composer.
|
||||
var switchToDefaultComposer: Bool {
|
||||
switch self {
|
||||
case .reply, .edit:
|
||||
case .reply, .edit, .addCaption, .editCaption:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
@@ -106,7 +109,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
/// Whether or not the action is destructive.
|
||||
var isDestructive: Bool {
|
||||
switch self {
|
||||
case .redact, .report:
|
||||
case .redact, .report, .removeCaption:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -130,6 +133,12 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
Label(L10n.actionCopy, icon: \.copy)
|
||||
case .edit:
|
||||
Label(L10n.actionEdit, icon: \.edit)
|
||||
case .addCaption:
|
||||
Label(L10n.actionAddCaption, icon: \.edit)
|
||||
case .editCaption:
|
||||
Label(L10n.actionEditCaption, icon: \.edit)
|
||||
case .removeCaption:
|
||||
Label(L10n.actionRemoveCaption, icon: \.delete)
|
||||
case .copyPermalink:
|
||||
Label(L10n.actionCopyLinkToMessage, icon: \.link)
|
||||
case .reply(let isThread):
|
||||
|
||||
+9
-1
@@ -65,7 +65,15 @@ struct TimelineItemMenuActionProvider {
|
||||
}
|
||||
|
||||
if item.isEditable {
|
||||
actions.append(.edit)
|
||||
if let messageItem = item as? EventBasedMessageTimelineItemProtocol, messageItem.supportsMediaCaption {
|
||||
if messageItem.hasMediaCaption {
|
||||
actions.append(contentsOf: [.editCaption, .removeCaption])
|
||||
} else {
|
||||
actions.append(.addCaption)
|
||||
}
|
||||
} else if !(item is VoiceMessageRoomTimelineItem) {
|
||||
actions.append(.edit)
|
||||
}
|
||||
}
|
||||
|
||||
if canCurrentUserPin, let eventID = item.id.eventID {
|
||||
|
||||
Reference in New Issue
Block a user