collapse cross-account dupes, Lato UI font, mail-list selection & density tuning
Backend: - Collapse identical cross-account deliveries in a thread: new messages.delivery_id (migration 0011) preserves the real Message-ID even when message_id_hdr is synthesized (§7.1); get_thread groups by it and reports also_in_account_ids; the list count counts distinct deliveries. Backfill script for existing rows. Frontend: - Bundle Lato (SIL OFL) as the UI font; unread mail-list rows use Black weight. - Mail-list selection: hover-avatar checkbox, tri-state "select all", bulk Archive/Pin/Delete (Restore/Delete in Archive/Trash/Spam) with a count in the reading pane; unread blue dot; "also delivered to" note in the thread. - Spacing/size tuning: sender-column width, icon gaps, avatar size, bar heights, and a round of -1px font sizes across the sidebar, mail list, and reading pane. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -188,7 +188,7 @@ function ActionsMenu({
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{t("mail.actions")}
|
||||
<span className="text-[9px]">▾</span>
|
||||
<span className="text-[11px]">▾</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 z-20 mt-1 w-52 overflow-hidden rounded-md border border-app-border bg-app-elevated py-1 shadow-lg">
|
||||
@@ -215,6 +215,7 @@ function MessageItem({
|
||||
zIndex,
|
||||
showReplyBar,
|
||||
canReply,
|
||||
alsoIn,
|
||||
onToggle,
|
||||
onReply,
|
||||
onToggleRead,
|
||||
@@ -231,6 +232,7 @@ function MessageItem({
|
||||
zIndex: number;
|
||||
showReplyBar: boolean;
|
||||
canReply: boolean;
|
||||
alsoIn: string[];
|
||||
onToggle: () => void;
|
||||
onReply: (kind: string) => void;
|
||||
onToggleRead: () => void;
|
||||
@@ -286,9 +288,9 @@ function MessageItem({
|
||||
onClick={onToggle}
|
||||
>
|
||||
{avatar()}
|
||||
<span className="min-w-0 flex-1 truncate text-sm">
|
||||
<span className="min-w-0 flex-1 truncate text-[15px]">
|
||||
{senderName}
|
||||
<span className="ml-2 text-app-faint">{message.snippet}</span>
|
||||
<span className="ml-2 text-xs text-app-faint">{message.snippet}</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-app-faint">{fmtRelative(message.date)}</span>
|
||||
</button>
|
||||
@@ -302,7 +304,7 @@ function MessageItem({
|
||||
className="flex w-full items-baseline justify-between gap-2 text-left"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className="min-w-0 truncate text-sm">
|
||||
<span className="min-w-0 truncate text-[15px]">
|
||||
{senderName}
|
||||
{message.from_addr[0]?.email && (
|
||||
<span className="text-app-faint"> <{message.from_addr[0].email}></span>
|
||||
@@ -320,7 +322,7 @@ function MessageItem({
|
||||
<span className="truncate">
|
||||
{t("mail.to")} {recipientSummary(message.to_addr)}
|
||||
</span>
|
||||
<span className="text-[9px] text-app-faint">{showDetails ? "▲" : "▼"}</span>
|
||||
<span className="text-[11px] text-app-faint">{showDetails ? "▲" : "▼"}</span>
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-3 text-xs">
|
||||
{location && <span className="font-semibold text-app-fg">{location}</span>}
|
||||
@@ -355,6 +357,11 @@ function MessageItem({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{alsoIn.length > 0 && (
|
||||
<div className="mt-0.5 truncate text-xs text-app-faint">
|
||||
{t("mail.alsoIn")} {alsoIn.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{showDetails && (
|
||||
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-t border-app-border pt-2 text-xs text-app-muted">
|
||||
<dt className="text-app-faint">{t("mail.from")}</dt>
|
||||
@@ -514,8 +521,8 @@ export function ThreadView({
|
||||
|
||||
return (
|
||||
<div data-testid="thread-view" className="flex h-full flex-col overflow-y-auto bg-app-bg">
|
||||
<div className="sticky top-0 z-10 flex h-[44px] shrink-0 items-center justify-between gap-2 border-b border-app-border bg-app-surface px-4">
|
||||
<h2 className="truncate text-base font-semibold text-app-fg">{subject || "(no subject)"}</h2>
|
||||
<div className="sticky top-0 z-10 flex h-[52px] shrink-0 items-center justify-between gap-2 border-b border-app-border bg-app-surface px-4">
|
||||
<h2 className="truncate text-[17px] font-semibold text-app-fg">{subject || "(no subject)"}</h2>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
className="rounded p-1.5 text-app-muted hover:bg-app-hover hover:text-app-fg"
|
||||
@@ -558,6 +565,9 @@ export function ThreadView({
|
||||
}`;
|
||||
// Reply/Reply-all/Forward make no sense on a trashed message.
|
||||
const canReply = specialUse.get(message.mailbox_id) !== "trash";
|
||||
const alsoIn = (message.also_in_account_ids ?? [])
|
||||
.map((id) => accounts?.find((a) => a.id === id)?.email)
|
||||
.filter((e): e is string => Boolean(e));
|
||||
return (
|
||||
<Fragment key={message.id}>
|
||||
<MessageItem
|
||||
@@ -570,6 +580,7 @@ export function ThreadView({
|
||||
zIndex={z}
|
||||
showReplyBar={isNewest && composing?.anchorId !== message.id}
|
||||
canReply={canReply}
|
||||
alsoIn={alsoIn}
|
||||
onToggle={() => toggle(message.id)}
|
||||
onReply={(kind) => openReply(message, kind)}
|
||||
onToggleRead={() => toggleRead(message)}
|
||||
|
||||
Reference in New Issue
Block a user