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'
);
};