fix(captions): harden caption editing after review
Address findings from 2 review agents (core edit path was verified correct — media preservation incl. encrypted, threading, no-op guard): - Require a filename (utils/room.ts): canEditCaption now also checks the MSC2530 `filename` exists. Fixes media from clients that omit filename, where the editor prefilled the filename as a caption and clearing it wrote an empty body. Such media simply isn't caption-editable (matches renderCaption never showing a caption for it). - Carry m.mentions (MessageEditor): a caption edit now unions typed @-mentions with prior mentions like the text-edit path, so mentioning someone in a caption edit notifies them. - Double caption: revert to the editor replacing the content while editing (as text edits do) instead of rendering the media + its caption above an editor prefilled with the same caption — removes the confusing duplicate. - Removed-caption "(edited)" marker (RenderMessageContent): when a media message is edited but has no caption (e.g. the caption was removed), render the standalone "(edited)" affordance so Edit History stays reachable (previously it lived only inside the caption and vanished). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Text style={{ marginTop: config.space.S200 }} size="T200">
|
||||
<MessageEditedContent onEditHistoryClick={onEditHistoryClick} />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -904,22 +904,17 @@ export const Message = React.memo(
|
||||
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
||||
{reply}
|
||||
{edit && onEditId ? (
|
||||
<>
|
||||
{/* Editing a media caption: keep the media visible for context; the
|
||||
editor below edits only the caption. */}
|
||||
{canEditCaption(mx, mEvent) && children}
|
||||
<MessageEditor
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
width: '100vw',
|
||||
}}
|
||||
roomId={room.roomId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
imagePackRooms={imagePackRooms}
|
||||
onCancel={() => onEditId()}
|
||||
/>
|
||||
</>
|
||||
<MessageEditor
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
width: '100vw',
|
||||
}}
|
||||
roomId={room.roomId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
imagePackRooms={imagePackRooms}
|
||||
onCancel={() => onEditId()}
|
||||
/>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user