Files
webmail/lib/account-inbox-unread.ts
T
lucamzanon d632956231 feat: add Mountain View, an optional alternative layout
Add an "Interface layout" setting at the top of Settings > Layout with two
options, Default and Mountain View. Default is the existing layout and stays
the default, so nothing changes unless a user opts in.

Mountain View rearranges the chrome while keeping the theme's colours, icons
and type:

- desktop: a top bar with a wide search field (filters drop from the field),
  an apps grid and the account avatar instead of the left app rail; flat
  navigation with Starred under Inbox and the rest behind "More"; a Compose
  pill; one list toolbar with select-all, refresh, overflow menu, a 1-N of
  total range and the batch actions; rows led by a checkbox and a star; an
  icon-only message toolbar with Reply / Reply all / Forward under the
  message; compose in a docked window; an account popover with per-account
  unread counts; j/k cursor, x to select and g-sequences
- phone: rounded search header, compact rows with the star on the preview
  line, and Reply / Reply all / Forward at the end of a message instead of
  the bottom action bar

It is one setting (`interfaceLayout`), written to a data attribute on <html>
that keys a block of CSS tokens in globals.css, plus a few components gated
on the flag. The mail layout in force is derived (effectiveMailLayout), so
the user's saved mail layout is never rewritten. The setting is listed in
the admin policy tab, and the new strings are translated in all locales.

Includes unit tests and a Playwright smoke test against a running instance
(scripts/mountain-view-smoke/).
2026-10-05 10:36:20 +02:00

36 lines
1.5 KiB
TypeScript

import type { Mailbox } from "@/lib/jmap/types";
/**
* Unread messages waiting in an account's own inbox.
*
* Mountain View's account popover puts this number next to every address, so
* that switching account is a decision one can take from the popover instead
* of having to visit each account in turn.
*
* Shared folders are skipped: they are someone else's inbox, they are listed
* under their owner in the sidebar, and counting them here would attribute
* another person's unread mail to this account. `undefined` means "not known
* yet" - the account's folders have not been fetched - and is deliberately
* distinct from a known zero, which the caller should render as no badge
* rather than as a "0".
*/
export function inboxUnread(mailboxes: Mailbox[] | undefined): number | undefined {
if (!mailboxes || mailboxes.length === 0) return undefined;
const inbox = mailboxes.find((mailbox) => mailbox.role === "inbox" && !mailbox.isShared);
return inbox ? inbox.unreadEmails : undefined;
}
/**
* What "All inboxes" carries: the unread of every listed account's inbox.
*
* Accounts whose folders are still loading contribute nothing rather than
* blocking the total, so the badge grows as the fetches land instead of
* appearing all at once several seconds in.
*/
export function totalInboxUnread(
accountIds: readonly string[],
mailboxesByAccount: Record<string, Mailbox[] | undefined>
): number {
return accountIds.reduce((total, id) => total + (inboxUnread(mailboxesByAccount[id]) ?? 0), 0);
}