Fill placeholders when a template is inserted
Templates were a fixed subject and body, so anything that changed per message -- who it is going to, today's date -- had to be typed over afterwards. Eight names are recognised: recipientName, recipientFirstName, recipientEmail, myName, myEmail, subject, date and time. Dates and times go through datetime.ts rather than toLocaleDateString, so a template follows the date order and clock the app was already told to use. Filling happens on insert rather than on send. What a placeholder came to is then visible in the composer and can be edited, instead of the message changing between writing it and sending it. Two things are deliberately left alone. A placeholder that cannot be answered yet -- a recipient's name on a draft nobody has addressed -- stays in the body as written, because substituting an empty string produces "Hi ,", which is wrong rather than visibly unfinished; leaving the name says which word is still missing and can be typed over. And a name that is not a placeholder is left as written too, since a body that quietly ate an unrecognised token would be worse than one that shows it. Values are escaped on the way into HTML: a display name comes from a contact card or a typed address and is not trusted markup.
This commit is contained in:
@@ -11,6 +11,7 @@ import { ensureScheduledMailbox, useScheduled } from "./scheduled";
|
||||
import { formatScheduleTime, holdUntil } from "@/lib/schedule";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
import { settings } from "./settings";
|
||||
import { fillPlaceholders, type PlaceholderContext } from "@/lib/templatePlaceholders";
|
||||
|
||||
export interface ComposeAttachment {
|
||||
id: string;
|
||||
@@ -575,8 +576,17 @@ export const useCompose = create<ComposeState>((set, get) => ({
|
||||
insertTemplate(key, html, subject) {
|
||||
const d = get().drafts.find((x) => x.key === key);
|
||||
if (!d) return;
|
||||
const patch: Partial<Draft> = { html: `<div>${sanitizeEditorHtml(html)}</div>${d.html}`, text: `${htmlToText(html)}\n${d.text}` };
|
||||
if (subject && !d.subject) patch.subject = subject;
|
||||
// Placeholders are filled against the draft as it stands right now, which
|
||||
// is why this happens on insert rather than on send: what the template is
|
||||
// filled with is visible and editable afterwards, instead of changing
|
||||
// under the message between writing it and sending it.
|
||||
const ident = d.identityId ? useMail.getState().identities.find((i) => i.id === d.identityId) : undefined;
|
||||
const ctx: PlaceholderContext = { to: d.to, from: ident ? { name: ident.name, email: ident.email } : null, subject: d.subject };
|
||||
// The body is filled once as HTML and the plain-text side derived from the
|
||||
// result, so the two cannot disagree about what a placeholder came to.
|
||||
const filled = fillPlaceholders(html, ctx, { html: true });
|
||||
const patch: Partial<Draft> = { html: `<div>${sanitizeEditorHtml(filled)}</div>${d.html}`, text: `${htmlToText(filled)}\n${d.text}` };
|
||||
if (subject && !d.subject) patch.subject = fillPlaceholders(subject, ctx, { html: false });
|
||||
get().update(key, patch);
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user