Sender avatars, PDF password preview, contacts autosave, account-view + Trash/Spam auto-purge, UI polish
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
This commit is contained in:
@@ -9,6 +9,8 @@ import type { AccountOut, MailboxOut, MessageEnvelope } from "../../lib/types";
|
||||
import { PluginSlot } from "../../plugins/PluginSlot";
|
||||
import { Composer, type ComposerInit } from "../compose/Composer";
|
||||
import { Icon } from "../Icon";
|
||||
import { AvatarImage } from "../SenderAvatar";
|
||||
import { clearPdfPasswordCache } from "./AttachmentPreview";
|
||||
import { LabelChips } from "../mail/LabelChips";
|
||||
import { MessageBody } from "./MessageBody";
|
||||
|
||||
@@ -345,10 +347,11 @@ function MessageItem({
|
||||
const nameColor = accentColor ?? `hsl(${h} 55% 62%)`;
|
||||
const avatar = () => (
|
||||
<span
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
|
||||
className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full text-sm font-semibold text-white"
|
||||
style={{ backgroundColor: avatarBg }}
|
||||
>
|
||||
{initials(message)}
|
||||
<AvatarImage email={message.from_addr[0]?.email} />
|
||||
</span>
|
||||
);
|
||||
const senderName = (
|
||||
@@ -534,6 +537,8 @@ export function ThreadView({
|
||||
// --- labels: per-message storage + a thread union for display (chips + list) ---
|
||||
const messageIds = useMemo(() => messages.map((m) => m.id), [messages]);
|
||||
const threadId = messages[0]?.thread_id ?? null;
|
||||
// Forget remembered protected-PDF passwords when the open thread changes or closes.
|
||||
useEffect(() => () => clearPdfPasswordCache(), [threadId]);
|
||||
const allLabelsQuery = useQuery({ queryKey: ["labels-all"], queryFn: api.labelsAll });
|
||||
const msgLabelsQuery = useQuery({
|
||||
queryKey: ["message-labels", messageIds],
|
||||
|
||||
Reference in New Issue
Block a user