diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index 5839cf6c1..9ab0f017b 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -683,6 +683,10 @@ The indicator is hidden once the server confirms the event (when the internal st Context menu → **Forward** allows forwarding a message to any room the user is a member of. +### Copy Message Text + +Context menu → **Copy Text** copies a message's plain-text body to the clipboard (reply fallback stripped via `trimReplyFromBody`), complementing the existing **Copy Link** (permalink) action. It renders only when the event has a usable text body, so media without a caption doesn't show an empty action. + ### Draft Persistence - Composer drafts are stored in `localStorage` keyed by `roomId` diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index fca7d7449..78d4bf458 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -56,6 +56,7 @@ import { getMemberAvatarMxc, getMemberName, sendStateEvent, + trimReplyFromBody, } from '../../../utils/room'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { messageAriaLabel } from '../../../utils/a11y'; @@ -411,6 +412,43 @@ export const MessageCopyLinkItem = as< ); }); +// Copies the message's plain-text body (reply fallback stripped) to the +// clipboard. Renders nothing for events without a usable text body (e.g. media +// without a caption), so the caller can list it unconditionally. +export const MessageCopyTextItem = as< + 'button', + { + mEvent: MatrixEvent; + onClose?: () => void; + } +>(({ mEvent, onClose, ...props }, ref) => { + const content = mEvent.getContent(); + const rawBody = typeof content.body === 'string' ? content.body : ''; + const body = trimReplyFromBody(rawBody).trim(); + + if (!body) return null; + + const handleCopy = () => { + copyToClipboard(body); + onClose?.(); + }; + + return ( + } + radii="300" + onClick={handleCopy} + {...props} + ref={ref} + > + + Copy Text + + + ); +}); + export const MessagePinItem = as< 'button', { @@ -1284,6 +1322,7 @@ export const Message = React.memo( onClose={closeMenu} /> )} + {canPinEvent && ( @@ -1513,6 +1552,7 @@ export const Event = React.memo( onClose={closeMenu} /> )} + {((!mEvent.isRedacted() && canDelete && !stateEvent) ||