m.replace events no longer appear as their own "* text" line — the original row gets m.new_content and an (edited) marker. Media rows carry a [Image]/[Video]/[Audio]/[File] prefix and the JSON export includes mediaUrl; the UI notes attachments aren't included. Unit-tested. Fixes #50 Fixes #81 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
// Pure helpers for ExportRoomHistory.tsx, kept SDK/DOM-free so they're easy to
|
|
// unit test in isolation.
|
|
|
|
export type RawExportRecord = {
|
|
eventId: string;
|
|
ts: number;
|
|
sender: string;
|
|
body: string;
|
|
msgtype: string;
|
|
// Set when this record IS an m.replace edit event, to the event id it targets.
|
|
editsEventId?: string;
|
|
// The m.new_content.body carried by an m.replace edit event.
|
|
newBody?: string;
|
|
};
|
|
|
|
export type ResolvedExportRecord = {
|
|
eventId: string;
|
|
ts: number;
|
|
sender: string;
|
|
body: string;
|
|
msgtype: string;
|
|
edited: boolean;
|
|
};
|
|
|
|
// Human-readable label for media msgtypes, used to prefix txt/html export rows
|
|
// so a media message isn't indistinguishable from a plain-text one.
|
|
export const MEDIA_TYPE_LABELS: Partial<Record<string, string>> = {
|
|
'm.image': 'Image',
|
|
'm.video': 'Video',
|
|
'm.audio': 'Audio',
|
|
'm.file': 'File',
|
|
};
|
|
|
|
/**
|
|
* Resolves m.replace (edit) events against the original message they target:
|
|
* the edit event is dropped from the output (it should not appear as its own
|
|
* row), and the original message's body is replaced with the edit's
|
|
* `m.new_content.body`, marked `edited: true`.
|
|
*
|
|
* Order-independent: edits may appear before or after their target in the
|
|
* input (e.g. across separate back-pagination batches).
|
|
*/
|
|
export function resolveMessageEdits(records: RawExportRecord[]): ResolvedExportRecord[] {
|
|
const edits = new Map<string, string>();
|
|
for (const record of records) {
|
|
if (record.editsEventId && record.newBody) {
|
|
edits.set(record.editsEventId, record.newBody);
|
|
}
|
|
}
|
|
|
|
const resolved: ResolvedExportRecord[] = [];
|
|
for (const record of records) {
|
|
// Edit events never become their own row.
|
|
if (record.editsEventId) continue;
|
|
const editedBody = edits.get(record.eventId);
|
|
resolved.push({
|
|
eventId: record.eventId,
|
|
ts: record.ts,
|
|
sender: record.sender,
|
|
body: editedBody ?? record.body,
|
|
msgtype: record.msgtype,
|
|
edited: editedBody !== undefined,
|
|
});
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
// Prefixes a media message's body with a "[Image]"-style marker for txt/html
|
|
// export output, and appends an "(edited)" suffix when applicable. Plain-text
|
|
// messages are returned unchanged (aside from the edited suffix).
|
|
export function formatExportBody(record: ResolvedExportRecord): string {
|
|
const label = MEDIA_TYPE_LABELS[record.msgtype];
|
|
const mediaPrefix = label ? `[${label}] ` : '';
|
|
const editedSuffix = record.edited ? ' (edited)' : '';
|
|
return `${mediaPrefix}${record.body}${editedSuffix}`;
|
|
}
|