ebcd8ec926
Forward: checkbox multi-select room picker + "Send to N rooms" batch send (Promise.allSettled). Full success auto-closes; partial failure keeps the dialog open with a "Forwarded to X/N — failed: …" summary and prunes the selection to only the failures (retry won't duplicate to already-sent rooms). Content builder extracted to a unit-tested forwardContent.ts (edit-forwarding, reply-strip, undecryptable-refused; 4 tests). Bookmarks: BookmarksPanel resolves each saved message's live event (useRoomEvent) so previews reflect edits and show a deleted indicator for redactions; the stored snapshot stays as the fallback while loading, on fetch failure, or after leaving the room. Stored bookmark shape unchanged. Gates: tsc/eslint/prettier clean, build OK, 665 tests. Reviewed (dup-resend on retry + Checkbox readOnly fixed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
|
import { getEditedEvent, trimReplyFromBody, trimReplyFromFormattedBody } from '../../../utils/room';
|
|
|
|
/**
|
|
* Build the content to forward:
|
|
* - undecryptable events are refused (would forward `m.bad.encrypted` junk)
|
|
* - edited messages forward the LATEST edit (`m.new_content`), not the
|
|
* original pre-edit body
|
|
* - reply fallbacks (`> <@user> …` quote + `<mx-reply>` block) are stripped
|
|
* along with the `m.relates_to` reply/thread relation, so the forwarded
|
|
* message stands alone in the target room
|
|
*/
|
|
export function buildForwardContent(
|
|
mx: MatrixClient,
|
|
mEvent: MatrixEvent,
|
|
): Record<string, unknown> | undefined {
|
|
if (mEvent.isDecryptionFailure()) return undefined;
|
|
|
|
let content = { ...mEvent.getContent() };
|
|
|
|
const eventId = mEvent.getId();
|
|
const room = mx.getRoom(mEvent.getRoomId());
|
|
if (eventId && room) {
|
|
const editedEvent = getEditedEvent(eventId, mEvent, room.getUnfilteredTimelineSet());
|
|
const newContent = editedEvent?.getContent()['m.new_content'];
|
|
if (newContent && typeof newContent === 'object') {
|
|
content = { ...(newContent as Record<string, unknown>) };
|
|
}
|
|
}
|
|
|
|
delete content['m.relates_to'];
|
|
if (typeof content.body === 'string') {
|
|
content.body = trimReplyFromBody(content.body);
|
|
}
|
|
if (typeof content.formatted_body === 'string') {
|
|
content.formatted_body = trimReplyFromFormattedBody(content.formatted_body);
|
|
}
|
|
return content;
|
|
}
|