diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index 2a578161f..37ea79fda 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -717,7 +717,7 @@ KaTeX-rendered math in messages, two paths: Images and videos can be sent with a caption. The caption and media are sent as a single event (caption = the event `body` when it differs from `filename`). -- **Edit caption**: an image/video you sent shows an **Edit caption** action (quick-actions pencil + message menu). It opens the message editor seeded with the current caption; saving sends an `m.replace` whose `m.new_content` preserves the media (`url`/`info`/encrypted `file`/`filename`) and only changes `body`/`formatted_body`. An empty caption removes it (`body` falls back to `filename`). Gated by `canEditCaption` (`utils/room.ts`) to your own image/video messages; the media stays visible above the editor, and caption edits appear in Edit History (diffed by the word-diff). No re-upload — encrypted media keeps its original file/key. +- **Edit caption**: an image/video you sent shows an **Edit caption** action (quick-actions pencil + message menu). It opens the message editor seeded with the current caption; saving sends an `m.replace` whose `m.new_content` preserves the media (`url`/`info`/encrypted `file`/`filename`) and only changes `body`/`formatted_body`. An empty caption removes it (`body` falls back to `filename`). Gated by `canEditCaption` (`utils/room.ts`) to your own image/video messages that carry an MSC2530 `filename`. Caption edits carry `m.mentions` (an @-mention in a caption notifies) and appear in Edit History (diffed by the word-diff) — even after a caption is removed, the "(edited)" marker remains so history stays reachable. No re-upload — encrypted media keeps its original file/key. ### Location Sharing diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 903bedf75..c476df573 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { MsgType } from 'matrix-js-sdk'; import { HTMLReactParserOptions } from 'html-react-parser'; import { Opts } from 'linkifyjs'; -import { config } from 'folds'; +import { config, Text } from 'folds'; import { AudioContent, DownloadFile, @@ -11,6 +11,7 @@ import { MAudio, MBadEncrypted, MEmote, + MessageEditedContent, MFile, MImage, MLocation, @@ -119,6 +120,15 @@ export function RenderMessageContent({ /> ); } + // No caption, but the media was edited (e.g. a caption was removed): keep the + // "(edited)" affordance so Edit History stays reachable. + if (edited) { + return ( + + + + ); + } return null; }; diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 06a09a0a3..3ce5645cb 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -904,22 +904,17 @@ export const Message = React.memo( {reply} {edit && onEditId ? ( - <> - {/* Editing a media caption: keep the media visible for context; the - editor below edits only the caption. */} - {canEditCaption(mx, mEvent) && children} - onEditId()} - /> - + onEditId()} + /> ) : ( children )} diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx index 065f8dec9..dcff3a1cd 100644 --- a/src/app/features/room/message/MessageEditor.tsx +++ b/src/app/features/room/message/MessageEditor.tsx @@ -162,7 +162,7 @@ export const MessageEditor = as<'div', MessageEditorProps>( delete mediaContent.formatted_body; } - // No-op guard: nothing changed. + // No-op guard: caption text/markup unchanged. if ( mediaContent.body === orig.body && mediaContent.formatted_body === orig.formatted_body @@ -170,6 +170,16 @@ export const MessageEditor = as<'div', MessageEditorProps>( return undefined; } + // Carry mentions typed into the caption (union with prior mentions), so + // an @-mention in a caption edit notifies — mirrors the text path. + const [, , prevMentions] = getPrevBodyAndFormattedBody(); + const mentionData = getMentions(mx, roomId, editor); + prevMentions?.user_ids?.forEach((id) => mentionData.users.add(id)); + mediaContent['m.mentions'] = getMentionContent( + Array.from(mentionData.users), + mentionData.room, + ); + const content: IContent = { ...mediaContent, 'm.new_content': mediaContent, diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index f687fe5cb..aeb91a245 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -520,7 +520,11 @@ export const canEditCaption = (mx: MatrixClient, mEvent: MatrixEvent) => { mEvent.getSender() === mx.getUserId() && (!relationType || relationType === RelationType.Thread) && mEvent.getType() === MessageEvent.RoomMessage && - (content.msgtype === MsgType.Image || content.msgtype === MsgType.Video) + (content.msgtype === MsgType.Image || content.msgtype === MsgType.Video) && + // Require the MSC2530 filename so a caption has a well-defined empty state + // (body === filename) — media from clients that omit filename isn't caption- + // editable (and renderCaption never shows a caption for it anyway). + typeof content.filename === 'string' ); };