fix(composer): scheduling keeps formatting, mentions and the reply relation

The schedule modal took only a plain body and rebuilt {body, msgtype},
dropping formatted_body, m.mentions and m.relates_to. It now receives
the full IContent; an unedited body is sent verbatim, an edited body
drops the now-stale formatted_body but keeps mentions and the reply/
thread relation. Reschedule from the tray preserves them too.
Unit-tested (mergeScheduledBody).

Fixes #36

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-15 21:32:06 -04:00
co-authored by Claude Opus 5
parent 62595ef066
commit bec248b228
3 changed files with 82 additions and 6 deletions
+29 -5
View File
@@ -31,8 +31,13 @@ import {
interface ScheduleMessageModalProps {
roomId: string;
/** Pre-fill the message body from the composer. Pass null/undefined to open blank. */
initialBody?: string;
/**
* Pre-fill from the composer (or the message being rescheduled). The textarea
* edits only `body`; the rest of the content (mentions, reply/thread relation,
* formatting) rides along — see `mergeScheduledBody`. Pass null/undefined to
* open blank.
*/
initialContent?: IContent | null;
/** Pre-fill the date/time pickers (Unix ms) — used when editing/rescheduling. */
initialSendAt?: number;
/** Header title; defaults to "Schedule Message". */
@@ -43,6 +48,23 @@ interface ScheduleMessageModalProps {
onClose: () => void;
}
/**
* [Gitea #36] Build the content to schedule from the pre-filled composer content
* and the (possibly edited) textarea body. Previously the modal rebuilt a bare
* `{ body, msgtype }`, silently dropping `formatted_body`, `m.mentions` and the
* reply/thread relation. Now: an unedited body sends the content verbatim; an
* edited body replaces `body` and drops `format`/`formatted_body` (they would be
* stale) but keeps mentions and the relation.
*/
export function mergeScheduledBody(initial: IContent | null | undefined, body: string): IContent {
const trimmed = body.trim();
if (!initial) return { body: trimmed, msgtype: 'm.text' };
const initialBody = typeof initial.body === 'string' ? initial.body : '';
if (trimmed === initialBody.trim()) return initial;
const { format: _format, formatted_body: _formattedBody, ...rest } = initial;
return { ...rest, msgtype: initial.msgtype ?? 'm.text', body: trimmed };
}
function formatRelativeTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
@@ -55,7 +77,7 @@ function formatRelativeTime(ms: number): string {
export function ScheduleMessageModal({
roomId,
initialBody,
initialContent,
initialSendAt,
title = 'Schedule Message',
submitLabel = 'Schedule',
@@ -64,7 +86,9 @@ export function ScheduleMessageModal({
}: ScheduleMessageModalProps) {
const modalStyle = useModalStyle(400);
const mx = useMatrixClient();
const [messageText, setMessageText] = useState(initialBody ?? '');
const [messageText, setMessageText] = useState(
typeof initialContent?.body === 'string' ? initialContent.body : '',
);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -128,7 +152,7 @@ export function ScheduleMessageModal({
return;
}
const content: IContent = { body: messageText.trim(), msgtype: 'm.text' };
const content = mergeScheduledBody(initialContent, messageText);
setError(null);
setSubmitting(true);
try {