Sender avatars (privacy-safe, server-fetched, cached): - New GET /api/avatar resolves Gravatar (d=404) then the sender domain's favicon under the SSRF guard, Redis-cached (hit+miss); gated by SENDER_AVATARS_ENABLED. GuardedResponse gains raw bytes. Shown in the mail list, reading pane, People panel, and Contacts (overlay that falls back to initials on a miss). PDF preview: - Password-protected PDFs now prompt in-place and retry (pdf.js PasswordException), with a clear error for other load failures instead of a blank overlay. The password is remembered per attachment until the thread is left; clicking the field no longer dismisses the overlay. Contacts: - Autosave reply recipients: message.sent now carries to/cc (name+email); the contacts plugin subscribes and adds any non-contact recipients to the Autosaved group. Contact avatars show the gravatar/favicon via /api/avatar. Mail views / retention: - Clicking an account shows all its mail except Trash/Spam (was inbox-only). - Daily cron autopurge_trash_spam permanently deletes Trash/Spam older than TRASH_RETENTION_DAYS (default 30; 0 disables) — server EXPUNGE + local rows. UI polish: - Ultimo signet logo in the Mail/Calendar/Contacts sidebar header and collapsed rail; consistent 11px top margin and vertically-aligned header row across states (fixed-size flex icon buttons); SVG favicon on the browser tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
19 lines
613 B
TypeScript
19 lines
613 B
TypeScript
import { useT } from "../i18n";
|
|
import { useTheme } from "../lib/theme";
|
|
|
|
// Quick light/dark flip (the full light/dark/system selector lives in Settings).
|
|
export function ThemeToggle() {
|
|
const t = useT();
|
|
const { resolved, setTheme } = useTheme();
|
|
return (
|
|
<button
|
|
className="inline-flex h-7 w-7 items-center justify-center rounded text-app-muted hover:bg-app-hover hover:text-app-fg"
|
|
title={t("theme.toggle")}
|
|
aria-label={t("theme.toggle")}
|
|
onClick={() => setTheme(resolved === "dark" ? "light" : "dark")}
|
|
>
|
|
{resolved === "dark" ? "☀" : "☾"}
|
|
</button>
|
|
);
|
|
}
|