With the navigation collapsed, Mountain View gives the sidebar a 72px
column, but the sidebar inside kept the default layout's 48px width. The
round compose button was cut in half, the selected folder's highlight
ran off the edge, and an empty strip showed before the message list.
The toolbar's run() had only try/finally, so a failed archive or tag
write rejected silently. Delete, move and archive now go through
runBatchEmailAction with the same messages as the default list, and
tag, read and spam actions show their success and error toasts.
The top bar also starts update polling. It replaces the rail, which
used to start it, so the update dot on the admin icon never showed.
Under the message the reply buttons sat on the tinted strip the default
layout uses for its quick reply, a different colour from the message
above them in the light theme. Mountain View now reads the whole
conversation on the page colour.
The account avatar in the top bar got a square highlight while every
other button there is round.
The top bar replaces the navigation rail on desktop, and with it went
the shield that takes a Stalwart admin to the console, so an admin under
Mountain View had no way in. The bar now shows it, with the same update
dot. The check that decides it moves out of the rail into
useStalwartAdmin so both use the same one.
- The refresh button was labelled "Loading" and the overflow menu
"Mark folder as read"; they now say Refresh and More actions.
- The Starred row stayed highlighted while a tag was open, and its icon
ignored the colourful-icons setting.
- Reply, Reply all and Forward use the standard small button size.
- With a single login the account popover offers one Sign out pill
instead of a pill-less footer row.
- The compose backdrop matches the other dialogs.
- Layout settings hide the mail layout and toolbar label options, which
do nothing under Mountain View; their values still apply to the
default layout.
Calendar, contacts, files and settings still drew the left navigation
rail under Mountain View, so moving away from mail switched to the
default frame. They now show the Mountain View top bar too, without the
mail search and the menu button, which only make sense in mail.
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/).
An aria-label on a plain span is not reliably read out, so the count
now comes with a visually hidden label and the digits themselves are
hidden from screen readers. Also reuses the sidebar's existing accounts
selector instead of subscribing to the same list twice.
After a load the logins other than the one on screen reconnect in the
background, and until they are all back the unified counts cover only
some of them. With many logins that lasts seconds, and a partial total
looks exactly like the whole one.
While that restore runs, the "All accounts" header shows how many logins
are already in the scope, e.g. "4/10", with a translated label for screen
readers. It goes away when the scope is complete or the restore is over -
a login that failed to reconnect does not keep it up.
`restoringAccounts` in the auth store marks the background restore; the
count itself is a pure function with its own tests.
Once a search runs the header field shows the active query, but the only
way back to the full list was to open the panel and clear it there.
Put a clear button at the trailing end of the field, shown only while a
query is applied. The field is now a container rather than one button,
since nesting a button inside a button is invalid markup — the label
area and the clear control are siblings.
Phones stacked two 56px bars: the mailbox title, then a row holding
select-all, the search box, the filter toggle and refresh. That is 112px
of chrome before the first message, and the title merely repeats what
the drawer already shows.
The header now carries the account avatar and a tappable search field,
and the second row is presented full-screen when that field is tapped —
so the same SearchBox, suggestions, history and filter chips appear,
none of it rebuilt. Select-all and refresh remain in that panel, and the
list already supports long-press selection and pull-to-refresh.
Tablet and desktop are unchanged; the row is only relocated on phones.
The rail variant flies its popover out sideways, which is right for a
narrow vertical sidebar but wrong at the top of the screen: anchored to
a trigger at the inline-end edge it runs straight off the viewport. In
RTL that edge is the left one, so the menu opened past the left border
and was unreachable.
Add a "header" variant that drops the menu below the trigger and pins it
by its trailing edge, with a max-width so it can never exceed the
viewport. rail and expanded are untouched.
A delayed send composed from a shared/group identity is created in the
*shared* JMAP account, because sendEmail() passes the target account to
getSubmissionAccountId(). Every read/write on the scheduled surface,
however, asked the primary submission account only:
getScheduledEmails / cancelEmailSubmission / rescheduleEmailSubmission
-> getSubmissionAccountId() // no argument = primary account
So a message scheduled from a shared address is accepted and stays
pending on the server — it *will* be delivered at sendAt — while the UI
cannot list it, cancel it or reschedule it. Cancelling appears to work:
EmailSubmission/set against the primary account answers `notFound` in
notUpdated, which the client never surfaced, and the mail goes out
anyway. Silent unrecallable mail is worse than a failed send.
Stalwart advertises urn:ietf:params:jmap:submission (with FUTURERELEASE
and a non-zero maxDelayedSend) on group accounts as well as personal
ones, so scheduling from a shared address is legitimate.
Data layer:
- getSubmissionAccountIds() enumerates every submission-capable account;
getScheduledEmails() queries each, tags every result with the account
that owns it, and fetches each submission's Email from that same
account (it lives beside the submission, not in the primary account).
It also returns per-account totals computed over all pending
submissions, so a badge stays correct past the first page.
- resolveSubmissionAccountId() locates a submission's owning account so
cancel/reschedule (and the sendAt/envelope helpers) act on the right
one. Callers that know the account pass it and skip the lookup.
- sendEmail() reports the submission's account in SendEmailResult, and
the pending-undo record carries it, so the "Undo send" / "Send now"
toast actions — which know only a submission id — address the right
account directly rather than searching for it inside the undo window.
- The delayed-send gate considers any reachable submission account
rather than only the primary one.
UI:
- Each shared account gets its own "Scheduled" sidebar row, scoped to
that account; the own row keeps showing the combined view. The scope
travels beside the virtual mailbox id, so the store's single
__scheduled__ id is unchanged, and it also drives which row renders as
selected and whether the scoped list has more to load.
Tested against a real Stalwart (integration/tests/11-shared-scheduled-send):
two server-truth tests pin the mechanism (the submission is invisible to
the primary account, and cancelling through it is a silent no-op) and two
UI tests cover the fix. All three UI-level assertions — the combined
list, the scoped row, and which row is selected — fail without this
change.