- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings section, mail templates (Compose > More > Templates), a Subscriptions folder with bulk unsubscribe/delete, and tracker blocking with a Privacy toggle (on by default) - Own dialogs instead of browser popups; emoji picker in compose; compose windows survive a visit to Settings - Sentence completion learned from the user's sent mail (no AI) - Interface in English and Polish (one file per language, English as the fallback), picked from the browser on first run, then kept - Email translation on the server via LibreTranslate: Translate button and "Translate to...", messages in several languages translated part by part, names/addresses/emoji/file names/phone numbers kept, an animated line while translating - Antispam "Mark as spam" loads with the message, not when the menu opens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
import { useState } from "react";
|
|
|
|
import { useT } from "../../i18n";
|
|
import { ApiError } from "../../lib/api";
|
|
import type { MailTemplate } from "../../lib/types";
|
|
import { AppDialog, DialogButtons } from "../AppDialog";
|
|
|
|
/** Compose → More → Templates → Save as template: name it, save it. A name
|
|
* already taken turns the button into "Replace" rather than asking again. */
|
|
export function SaveTemplateDialog({
|
|
initialName,
|
|
existing,
|
|
onSave,
|
|
onClose,
|
|
}: {
|
|
initialName: string;
|
|
existing: MailTemplate[];
|
|
onSave: (name: string) => Promise<void>;
|
|
onClose: () => void;
|
|
}) {
|
|
const t = useT();
|
|
const [name, setName] = useState(initialName);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const trimmed = name.trim();
|
|
const taken = existing.some((x) => x.name.toLowerCase() === trimmed.toLowerCase());
|
|
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!trimmed || saving) return;
|
|
setSaving(true);
|
|
setError("");
|
|
try {
|
|
await onSave(trimmed);
|
|
onClose();
|
|
} catch (err) {
|
|
setError(err instanceof ApiError ? err.message : t("compose.templateSaveFailed"));
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AppDialog title={t("compose.saveTemplateTitle")} onClose={onClose}>
|
|
<form onSubmit={submit}>
|
|
<p className="mt-2 text-sm text-app-muted">{t("compose.saveTemplateHint")}</p>
|
|
<label className="mt-4 block text-sm">
|
|
<span className="text-app-muted">{t("compose.templateName")}</span>
|
|
<input
|
|
className="mt-1 w-full rounded-lg border border-app-border bg-app-bg px-3 py-1.5 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none"
|
|
value={name}
|
|
maxLength={80}
|
|
placeholder={t("compose.templateNamePlaceholder")}
|
|
onChange={(e) => setName(e.target.value)}
|
|
autoFocus
|
|
onFocus={(e) => e.currentTarget.select()}
|
|
/>
|
|
</label>
|
|
{taken && <p className="mt-2 text-xs text-amber-600 dark:text-amber-400">{t("compose.templateReplaceNote")}</p>}
|
|
{error && <p className="mt-2 text-xs text-red-500">{error}</p>}
|
|
<DialogButtons
|
|
submit
|
|
confirmLabel={saving ? t("compose.templateSaving") : taken ? t("compose.templateReplaceAction") : t("common.save")}
|
|
disabled={!trimmed || saving}
|
|
onCancel={onClose}
|
|
/>
|
|
</form>
|
|
</AppDialog>
|
|
);
|
|
}
|