Productivity features, UI translations, local email translation
- 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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user