diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 1058c72968..a9efc2209c 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -22,6 +22,7 @@ "a11y_voice_message_record" = "Record voice message."; "a11y_voice_message_stop_recording" = "Stop recording"; "action_accept" = "Accept"; +"action_add_caption" = "Add caption"; "action_add_to_timeline" = "Add to timeline"; "action_back" = "Back"; "action_call" = "Call"; @@ -47,6 +48,7 @@ "action_discard" = "Discard"; "action_done" = "Done"; "action_edit" = "Edit"; +"action_edit_caption" = "Edit caption"; "action_edit_poll" = "Edit poll"; "action_enable" = "Enable"; "action_end_poll" = "End poll"; @@ -81,6 +83,7 @@ "action_react" = "React"; "action_reject" = "Reject"; "action_remove" = "Remove"; +"action_remove_caption" = "Remove caption"; "action_reply" = "Reply"; "action_reply_in_thread" = "Reply in thread"; "action_report_bug" = "Report bug"; @@ -119,6 +122,7 @@ "banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "About"; "common_acceptable_use_policy" = "Acceptable use policy"; +"common_adding_caption" = "Adding caption"; "common_advanced_settings" = "Advanced settings"; "common_analytics" = "Analytics"; "common_appearance" = "Appearance"; @@ -137,6 +141,7 @@ "common_direct_chat" = "Direct chat"; "common_edited_suffix" = "(edited)"; "common_editing" = "Editing"; +"common_editing_caption" = "Editing caption"; "common_emote" = "* %1$@ %2$@"; "common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryption enabled"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 5eab6c3017..1d45cfdbb8 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -76,6 +76,8 @@ internal enum L10n { internal static var a11yVoiceMessageStopRecording: String { return L10n.tr("Localizable", "a11y_voice_message_stop_recording") } /// Accept internal static var actionAccept: String { return L10n.tr("Localizable", "action_accept") } + /// Add caption + internal static var actionAddCaption: String { return L10n.tr("Localizable", "action_add_caption") } /// Add to timeline internal static var actionAddToTimeline: String { return L10n.tr("Localizable", "action_add_to_timeline") } /// Back @@ -126,6 +128,8 @@ internal enum L10n { internal static var actionDone: String { return L10n.tr("Localizable", "action_done") } /// Edit internal static var actionEdit: String { return L10n.tr("Localizable", "action_edit") } + /// Edit caption + internal static var actionEditCaption: String { return L10n.tr("Localizable", "action_edit_caption") } /// Edit poll internal static var actionEditPoll: String { return L10n.tr("Localizable", "action_edit_poll") } /// Enable @@ -198,6 +202,8 @@ internal enum L10n { internal static var actionReject: String { return L10n.tr("Localizable", "action_reject") } /// Remove internal static var actionRemove: String { return L10n.tr("Localizable", "action_remove") } + /// Remove caption + internal static var actionRemoveCaption: String { return L10n.tr("Localizable", "action_remove_caption") } /// Reply internal static var actionReply: String { return L10n.tr("Localizable", "action_reply") } /// Reply in thread @@ -276,6 +282,8 @@ internal enum L10n { internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } /// Acceptable use policy internal static var commonAcceptableUsePolicy: String { return L10n.tr("Localizable", "common_acceptable_use_policy") } + /// Adding caption + internal static var commonAddingCaption: String { return L10n.tr("Localizable", "common_adding_caption") } /// Advanced settings internal static var commonAdvancedSettings: String { return L10n.tr("Localizable", "common_advanced_settings") } /// Analytics @@ -312,6 +320,8 @@ internal enum L10n { internal static var commonEditedSuffix: String { return L10n.tr("Localizable", "common_edited_suffix") } /// Editing internal static var commonEditing: String { return L10n.tr("Localizable", "common_editing") } + /// Editing caption + internal static var commonEditingCaption: String { return L10n.tr("Localizable", "common_editing_caption") } /// * %1$@ %2$@ internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index a8def51e01..45e2fd5d50 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -14133,8 +14133,8 @@ class TimelineProxyMock: TimelineProxyProtocol { var editNewContentCalled: Bool { return editNewContentCallsCount > 0 } - var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)? - var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation)] = [] + var editNewContentReceivedArguments: (eventOrTransactionID: EventOrTransactionId, newContent: EditedContent)? + var editNewContentReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, newContent: EditedContent)] = [] var editNewContentUnderlyingReturnValue: Result! var editNewContentReturnValue: Result! { @@ -14160,9 +14160,9 @@ class TimelineProxyMock: TimelineProxyProtocol { } } } - var editNewContentClosure: ((EventOrTransactionId, RoomMessageEventContentWithoutRelation) async -> Result)? + var editNewContentClosure: ((EventOrTransactionId, EditedContent) async -> Result)? - func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: EditedContent) async -> Result { editNewContentCallsCount += 1 editNewContentReceivedArguments = (eventOrTransactionID: eventOrTransactionID, newContent: newContent) DispatchQueue.main.async { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 3459fb1c81..bfc6767a2b 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -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) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 06623b8c7a..c8e77830c2 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -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) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 89f8903d07..0662cc0076 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -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) diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index e21ee3b4a7..18e6ef0ee8 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -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))) } diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 94db414a07..76fb206eb9 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -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: diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index b6ce6d036d..38f8246225 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -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): diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 9fcc47575a..df74221431 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -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 { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 0ec25915e7..b8cf4387e2 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -91,6 +91,13 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { html: String?, intentionalMentions: IntentionalMentions) async { } + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async { } + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async { } + func redact(_ eventOrTransactionID: EventOrTransactionId) async { } func pin(eventID: String) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index edcf2ef221..a9abcd63ff 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -238,7 +238,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { html: html, intentionalMentions: intentionalMentions.toRustMentions()) - switch await activeTimeline.edit(eventOrTransactionID, newContent: messageContent) { + switch await activeTimeline.edit(eventOrTransactionID, newContent: .roomMessage(content: messageContent)) { case .success: MXLog.info("Finished editing message by event") case let .failure(error): @@ -246,6 +246,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async { + // We're waiting on an API for including mentions: https://github.com/matrix-org/matrix-rust-sdk/issues/4302 + MXLog.info("Editing timeline item caption: \(eventOrTransactionID) in \(roomID)") + + // When formattedCaption is nil, caption will be parsed as markdown and generate the HTML for us. + let newContent = createCaptionEdit(caption: message, formattedCaption: html.map { .init(format: .html, body: $0) }) + switch await activeTimeline.edit(eventOrTransactionID, newContent: newContent) { + case .success: + MXLog.info("Finished editing caption") + case let .failure(error): + MXLog.error("Failed editing caption with error: \(error)") + } + } + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async { + // Set a `nil` caption to remove it from the event. + let newContent = createCaptionEdit(caption: nil, formattedCaption: nil) + switch await activeTimeline.edit(eventOrTransactionID, newContent: newContent) { + case .success: + MXLog.info("Finished removing caption.") + case let .failure(error): + MXLog.error("Failed removing caption with error: \(error)") + } + } + func redact(_ eventOrTransactionID: EventOrTransactionId) async { MXLog.info("Send redaction in \(roomID)") diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 0d13a2994a..e32356e25a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -56,6 +56,13 @@ protocol RoomTimelineControllerProtocol { html: String?, intentionalMentions: IntentionalMentions) async + func editCaption(_ eventOrTransactionID: EventOrTransactionId, + message: String, + html: String?, + intentionalMentions: IntentionalMentions) async + + func removeCaption(_ eventOrTransactionID: EventOrTransactionId) async + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async func redact(_ eventOrTransactionID: EventOrTransactionId) async diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift index 6b4ae9f5ea..a0832abf5c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedMessageTimelineItemProtocol.swift @@ -26,6 +26,15 @@ protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol { } extension EventBasedMessageTimelineItemProtocol { + var supportsMediaCaption: Bool { + switch contentType { + case .audio, .file, .image, .video: + true + case .emote, .notice, .text, .location, .voice: + false + } + } + var hasMediaCaption: Bool { switch contentType { case .audio(let content): diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 0a7a1b1dca..0c2422de17 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -164,9 +164,9 @@ final class TimelineProxy: TimelineProxyProtocol { } } - func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: RoomMessageEventContentWithoutRelation) async -> Result { + func edit(_ eventOrTransactionID: EventOrTransactionId, newContent: EditedContent) async -> Result { do { - try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: .roomMessage(content: newContent)) + try await timeline.edit(eventOrTransactionId: eventOrTransactionID, newContent: newContent) MXLog.info("Finished editing timeline item: \(eventOrTransactionID)") diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index d0d5285fb2..3bff76ecba 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -38,7 +38,7 @@ protocol TimelineProxyProtocol { func paginateForwards(requestSize: UInt16) async -> Result func edit(_ eventOrTransactionID: EventOrTransactionId, - newContent: RoomMessageEventContentWithoutRelation) async -> Result + newContent: EditedContent) async -> Result func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-en-GB.1.png index aee9e9b7d3..20eb21a0c4 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a5e7e651f73e10b1b7c5fad0a3eb631d9f50f5f42286f1faaed852316dd287d -size 98836 +oid sha256:389426d51309b08493728679fe367ca3469d0f7962423c10ea3b2b599685599c +size 125743 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-pseudo.1.png index 61a955e37b..3f2e93a144 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4914f7dc335cc0333584c2ad34034ce5a28e6fb3bb52d05f27734ed2dae5e7a6 -size 99876 +oid sha256:037ea34c80269e1a2761710c061aad688fbdf32d41b667c01b83ef027f9ea319 +size 129460 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-en-GB.1.png index a28707e06c..f33e3e05a1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9951096cb1ddc3050415353192108a4489908d8355b30189845a7da5fa2b5d83 -size 55103 +oid sha256:a55833e52c9f9bee6ba1b5162f69f3f650ae7a1965ff89f03533e7b8b40cabd4 +size 76082 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-pseudo.1.png index bda11829e5..4619c54083 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageComposer-iPhone-16-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:745116e5588711197b37e391b2f2ca6359c9a4cb8b3dd718ec55dd29c33d72a1 -size 57224 +oid sha256:a934bf0353d41916807d166a1b49b8cb9732a2fd791605a0c3ffdbac387fab8a +size 80923 diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 1c9327fd98..f1a1123282 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -31,14 +31,14 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerFocus() { - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default))) XCTAssertTrue(viewModel.state.bindings.composerFocused) viewModel.process(timelineAction: .removeFocus) XCTAssertFalse(viewModel.state.bindings.composerFocused) } func testComposerMode() { - let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default) viewModel.process(timelineAction: .setMode(mode: mode)) XCTAssertEqual(viewModel.state.composerMode, mode) viewModel.process(timelineAction: .clear) @@ -46,7 +46,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerModeIsPublished() { - let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock")) + let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventId(eventId: "mock"), type: .default) let expectation = expectation(description: "Composer mode is published") let cancellable = viewModel .context @@ -226,7 +226,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), type: .default))) viewModel.context.plainComposerText = .init(string: "Hello world!") viewModel.saveDraft() @@ -385,7 +385,7 @@ class ComposerToolbarViewModelTests: XCTestCase { await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) - XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"))) + XCTAssertEqual(viewModel.state.composerMode, .edit(originalEventOrTransactionID: .eventId(eventId: "testID"), type: .default)) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world!")) } @@ -473,7 +473,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testSaveVolatileDraftWhenEditing() { viewModel.context.composerFormattingEnabled = false viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString)))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventId(eventId: UUID().uuidString), type: .default))) let draft = draftServiceMock.saveVolatileDraftReceivedDraft XCTAssertNotNil(draft)