mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 00:14:23 +00:00
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
// Open composers that plugins may work on (api.composer.*).
|
|
//
|
|
// Each composer registers a handle under a random id while it is open. A
|
|
// plugin learns the id only from the composer itself: the click context of
|
|
// a composer action (Attach menu, toolbar, Options, Send menu) or the props
|
|
// of a composer slot. So a plugin edits the message the user pointed it
|
|
// at, never one it found by itself.
|
|
|
|
import { generateUUID } from '@/lib/utils';
|
|
import type { ComposerSnapshot, ComposerAttachmentData } from '@/lib/plugin-types';
|
|
|
|
export interface ComposerContent {
|
|
html: string;
|
|
text: string;
|
|
}
|
|
|
|
export interface ComposerAttachment {
|
|
blobId: string;
|
|
name: string;
|
|
type: string;
|
|
size: number;
|
|
}
|
|
|
|
export type ComposerMetadata = {
|
|
subject: string;
|
|
to: string[];
|
|
cc?: string[];
|
|
bcc?: string[];
|
|
pending?: boolean;
|
|
attachments: ComposerSnapshot['attachments'];
|
|
accountKey?: string;
|
|
identityId?: string;
|
|
};
|
|
|
|
export interface ComposerHandle {
|
|
/** Read this composer's metadata; account and identity stay host-side. */
|
|
readMetadata?(): ComposerMetadata;
|
|
/** Read a file currently attached to this composer. */
|
|
readAttachment?(id: string): Promise<ComposerAttachmentData>;
|
|
/** The user's own text, without the signature and the quote. */
|
|
readBody(): ComposerContent;
|
|
readSelection(): ComposerContent & { empty: boolean };
|
|
/** Replace the user's own text. */
|
|
replaceBody(html: string, text: string): boolean;
|
|
/** Replace the selection (or insert at the caret), or with `below` add after it. */
|
|
replaceSelection(html: string, text: string, opts: { below: boolean }): boolean;
|
|
/** Attach a blob the plugin uploaded, through the composer's usual checks. */
|
|
attach(file: ComposerAttachment): Promise<boolean>;
|
|
}
|
|
|
|
const handles = new Map<string, ComposerHandle>();
|
|
const snapshots = new Map<string, { fingerprint: string; revision: string }>();
|
|
|
|
/** Bind an opaque revision to the current body, recipients, files and account. */
|
|
export function captureComposer(id: string): ComposerSnapshot {
|
|
const handle = handles.get(id);
|
|
if (!handle) throw new Error('composer is not open any more');
|
|
const body = handle.readBody();
|
|
const metadata = handle.readMetadata?.() ?? { subject: '', to: [], attachments: [] };
|
|
const fingerprint = JSON.stringify({ body, metadata });
|
|
let current = snapshots.get(id);
|
|
if (!current || current.fingerprint !== fingerprint) {
|
|
current = { fingerprint, revision: generateUUID() };
|
|
snapshots.set(id, current);
|
|
}
|
|
return {
|
|
revision: current.revision,
|
|
subject: metadata.subject,
|
|
text: body.text,
|
|
to: [...metadata.to],
|
|
cc: [...(metadata.cc ?? [])],
|
|
bcc: [...(metadata.bcc ?? [])],
|
|
attachments: metadata.attachments.map(file => ({ ...file })),
|
|
pending: metadata.pending ?? false,
|
|
};
|
|
}
|
|
|
|
/** Register an open composer, optionally using a preallocated capability id. */
|
|
export function registerComposer(handle: ComposerHandle, id = generateUUID()): { id: string; unregister: () => void } {
|
|
handles.set(id, handle);
|
|
return { id, unregister: () => { handles.delete(id); snapshots.delete(id); } };
|
|
}
|
|
|
|
export function composerHandle(id: unknown): ComposerHandle | null {
|
|
return typeof id === 'string' ? handles.get(id) ?? null : null;
|
|
}
|
|
|
|
/** Test helper. */
|
|
export function resetComposers(): void {
|
|
handles.clear();
|
|
snapshots.clear();
|
|
}
|