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:
2026-07-22 21:49:48 +02:00
co-authored by Claude Opus 4.8
parent fdb6ee002e
commit b4e07f11aa
28 changed files with 757 additions and 1291 deletions
+38
View File
@@ -314,6 +314,44 @@ async def test_folder_counts(api: ApiCtx) -> None:
assert body["inbox_unread"] == 2
async def test_thread_collapses_cross_account_duplicate(api: ApiCtx) -> None:
assert api.session is not None
# The kickoff thread's first message; give it a real delivery_id.
kickoff = next(t for t in (await api.client.get("/api/threads")).json()["threads"]
if t["message_count"] == 2)
detail = (await api.client.get(f"/api/threads/{kickoff['id']}")).json()
first_id = uuid.UUID(detail["messages"][0]["id"])
first = await api.session.get(Message, first_id)
assert first is not None
first.delivery_id = "<shared@x.com>"
# A second account receives the SAME email (same delivery_id) into the same thread.
acct2 = Account(
user_id=api.user_id, email="me2@example.com", imap_config={}, smtp_config={},
secret_ct=b"", discovery_src="manual",
)
api.session.add(acct2)
await api.session.flush()
inbox2 = Mailbox(account_id=acct2.id, user_id=api.user_id, path="INBOX", special_use="inbox")
api.session.add(inbox2)
await api.session.flush()
api.session.add(Message(
user_id=api.user_id, account_id=acct2.id, mailbox_id=inbox2.id, uid=1,
thread_id=first.thread_id, message_id_hdr="<synthetic@synthetic.local>",
delivery_id="<shared@x.com>", subject=first.subject,
internal_date=datetime.now(UTC), blob_sha256="z" * 64, # later -> first stays the rep
))
await api.session.commit()
body = (await api.client.get(f"/api/threads/{kickoff['id']}")).json()
ids = [m["id"] for m in body["messages"]]
# Still two rows (kickoff + reply) — the duplicate collapsed into the first, not
# a third row — and the first now notes the other account.
assert len(ids) == 2
rep = next(m for m in body["messages"] if m["id"] == str(first_id))
assert rep["also_in_account_ids"] == [str(acct2.id)]
async def test_message_html_is_sanitized(api: ApiCtx) -> None:
# the lunch message has HTML with a script
resp = await api.client.get("/api/threads")