frontend: redesign the thread reading panel

Rework the conversation view to match the new dark-theme mockups:
- Subject bar gains a print button and an expand/collapse-all toggle (default
  action expands all; collapsing leaves only the newest message open).
- Collapsed messages are compact single-line cards (40px avatar, colored sender
  name, snippet, relative date) that overlap into a stack; alignment follows the
  sort order (top edge for oldest-first, bottom edge for newest-first) and the
  always-open newest card overlaps its neighbour in both orders.
- Expanded messages show sender+address (whole header row toggles collapse), a
  detail disclosure (From/To/Cc/Subject/Date/Size), the message location
  (Inbox/Sent), a read/unread circle (per-message flag toggle), a quick
  Reply/Reply-all, and an Actions menu (reply/forward/print/delete). The
  Reply/Reply all/Forward bar now sits inside the newest card above a divider.
- Message body: the quoted original is hidden by default with a "•••" toggle
  injected after the "On … wrote:" attribution; the iframe now collapses-then-
  measures so it shrinks back when the quote is hidden.
- Avatars use the sender's IMAP-account colour when the address matches one of
  our accounts, else a stable per-sender hue; two initials for multi-word names.
- Brighter dark-theme border token; new print/expand-all/collapse-all icons.

Backend: add size_bytes to the message envelope (shown in message details).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 18:58:01 +02:00
co-authored by Claude Opus 4.8
parent 293945480b
commit 1a83e0fb9b
13 changed files with 442 additions and 62 deletions
+3
View File
@@ -69,6 +69,7 @@ export function MailLayout() {
// Which special-use folder the reading pane is showing (drives the toolbar).
const currentFolder = view.startsWith("folder:") ? view.slice("folder:".length) : "inbox";
const accountsQuery = useQuery({ queryKey: ["accounts"], queryFn: api.accounts });
const mailboxesQuery = useQuery({ queryKey: ["mailboxes"], queryFn: api.mailboxes });
const threadsQuery = useQuery({
queryKey: ["threads", view],
queryFn: () => api.threads(view),
@@ -444,6 +445,8 @@ export function MailLayout() {
subject={detailQuery.data.subject}
messages={detailQuery.data.messages}
order={detailQuery.data.order}
mailboxes={mailboxesQuery.data ?? []}
accounts={accounts}
replyKindSignal={replySignal}
/>
</div>