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) ||