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 browser reads document.title back with its whitespace stripped and
collapsed, so a title with two spaces in a row, a tab or a space at the
edge never reads back as written. useDocumentTitle put its title back on
every <head> mutation whenever the two differed, and its own write was
such a mutation: an endless loop that froze the tab at full CPU, for
example on opening a message whose subject has two spaces in a row. It
now also accepts what the browser read back after its own write.
The tab title was "<context> - <app>", so two mailboxes open side by side
read the same in the tab strip and in the browser's session-restore list.
It is now "<context> - <account> - <app>", the order Gmail and Outlook Web
use: browsers cut a long title from the end, so the part that changes comes
first and the app name last. There is no setting for it; the account is what
tells the tabs apart.
The formatting, and the mail view's choice of context (composer, then
message, then mailbox), live in lib/tab-title.ts with tests. The mail view
sets its title through useDocumentTitle(), which other surfaces can use too.
Next renders the root layout's metadata <title> again on every client-side
navigation, after the surface has mounted; the hook puts its title back when
that happens. Only the surface that set the title last does so, so two
titles never fight.
Refs #1159
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useFocusTrap started over whenever its onEscape changed, and callers pass
a new function on every render. Each start moved the focus back to the
opener and then to the dialog's first element, so a field lost the focus
whenever the page behind rendered again: every 30 seconds through the
connection check, or when new mail updated the folder counts. The trap
now reads the latest onEscape from a ref and starts once per opening.
A view change in a dialog (a list giving way to a form) can remove the
element that had the focus. The restart used to pull it back by
accident; the trap now does that on purpose, and only when the focused
element was removed.
The folder-list cache from #1132 published a reused list only when it
was freshly fetched. When the Pro folder tree had loaded a login first,
the unified build reused that list and never stored it under the
login's JMAP id, so trash and archive for that login's mail were
resolved against the active login's folders until it pushed a change.
A reused list now fills any key nothing has published yet, without
overwriting a live one.
The Pro folder tree also marked a login as loaded before its first
fetch finished and never retried a failure, which left that login's
tree empty until an account switch. A failed load is now retried on
the next run, as the identities hook already does.
Two effects keyed on the account list reloaded every connected login each
time the list changed - and while a browser restores its logins it changes
once per connection, so the cost was 1 + 2 + … + N:
- useProMultiAccountMailboxes, which the Pro shell runs for its per-account
folder tree, reloaded every login's folders: 55 Mailbox/get with ten logins.
It now loads each login once per client, through the same cache as the
unified scope, and reloads all of them once on an account switch.
- useProMultiAccountIdentities reread every other login's identities: 45
Identity/get. Each mount now reads each account once; what is already
known shows at once, and a read that outlives the run that started it
still reaches the dropdown.
Both are keyed on the set of connected logins instead of `accounts`, which
also changes for display-name and login-time updates.
Retry interrupted or invalid runtime configuration requests with bounded backoff and per-attempt timeouts. Validate the complete response before caching it, and prevent stale HTTP-cached configuration.
Cover request and body timeouts, shared hook consumers, retry bounds, the server response contract, and both Lite configuration paths.
Follow-up to 0e0d99e7, which sends the batch actions of a unified view,
a tag view or an "All folders" search to each message's own login and
account.
- Batch Archive in a tag view or the default "All folders" search
failed with "archive mailbox not found", and "Not spam" in a tag view
entered from Junk with "Inbox not found": the folder lists came from
a cache only the unified views fill. In the Pro shell the list cached
under the bare account id could be another server's, so a browsed
login's messages were archived or deleted into whatever folder has
the other server's Archive or Deleted Items id. A batch's Archive,
Inbox and Deleted Items now come from the list cached for its login
and account, else from that login's own folder list, never from the
bare account id.
- "Move to" in a list that mixes accounts looked the picked folder up
in the login browsed before. In the Pro shell, after browsing a
folder of another login, the active login's mail went to that other
server. The menus list the active login's folders, so the folder is
looked up there now.
- Dragging one row moved every listed row with its id, including
another account's unrelated message. The drop hands the store the
dragged rows now.
- "Spam" and "Not spam" on a selected id that is not a row of the list,
such as a member of an expanded thread, wrote to the account the list
is reached through, where the id can name another message. Such ids
are looked up among the expanded threads now, and one found nowhere
is reported and left alone.
- A move sent in batches (500 messages each by default) stopped at a
refused batch while the list counted the unsent ones as moved. The
other batches go out, and only what the server left alone stays
listed.
Someone who writes to a calendar shared with them (read & write) changes
the owner's meeting there on the owner's behalf: Stalwart sends the change
from the addresses of the account the event is in. Since 6d4b6954 Bulwark
decided who organizes by the user's own addresses only, so it took the
sharee for an attendee (live on 1.0 and 0.16.23):
- a drag, a resize or a month-view drop wrote the new time without
scheduling messages: the attendees kept the old time and their answers
while the owner's copy moved; before 6d4b6954 they got "Updated
invitation"
- the editor wrote no participants, hid "Send invitations", showed the
attendees read-only and sent nothing
- "All events" asked nobody again, and "This and following events" ended
the series quietly and made the rest a new series without attendees
`userOrganizes` now also counts the organizer's own copy in another
account: `isOrigin`, which both versions compute from the addresses of
the account the event is in, true for the owner's meeting and every
expanded occurrence of it, false for an invitation the owner got. Such an
invitation is still changed without messages. The editor no longer drops
the user's own address from the attendees it opens with, only the
organizer's, so a sharee who attends the owner's meeting stays on it.
The demo's invitation from Alice now says it is not the demo user's copy.
In the unified views, a tag view and an "All folders" search a
selection can span logins and accounts, and message, account and folder
ids repeat across both: Stalwart numbers the accounts and the system
folders of every install alike.
- Archive sent the whole selection through one login into the Archive of
the selected folder's account, which is a virtual folder there, so the
own one: messages of a group account or of another login were archived
by their ids in the own account, where the same ids can name unrelated
messages, or failed after the own ones had moved. The list dropped
every selected row either way. Each login and account now archives
into its own Archive.
- "Move to" and dragging messages applied one folder id to every
account, so another account's messages landed in whatever folder has
that id there, and failed moves were dropped from the list anyway.
Only messages in the folder's own login and account change folders in
place now; the others take the cross-account move (Email/copy within
one login, download and import across logins), a single message moved
with "Move to" or dragged as well.
- A drop goes to the login whose sidebar list shows the folder. It went
to the first login whose own account id matched the folder's, which
with two servers that both number the user `e` could be the other one.
- "Spam" did nothing while the app said the messages were marked, and
"Not spam" failed, because the selected folder is a virtual one there.
Both now write each message in its own account and move it to that
account's Junk or Inbox. The spam shortcut in "All Junk" means "Not
spam", as the toolbar does.
- What could not be archived, moved or marked stays listed and selected,
and the error says why.
A move outside the editor kept every answer. A drag or resize in the
week and day views, a drop in the month view and an applied counter
proposal wrote the new time with the attendees' stored answers:
Stalwart's update then said "accepted" for a time nobody had accepted,
and a dragged occurrence, which gets its own override, reached no
attendee who had answered (live on 1.0 and 0.16.23). When the user
organizes the meeting these moves now ask the attendees again, as the
editor does; one helper (`userOrganizes`) decides for both.
Also:
- A changed recurrence rule counts as a move for the series: a custom
rule moved from Monday to Tuesday asks again. A rule the user did not
touch is no change however the server spells it, and on one
occurrence, where no rule applies, it is none.
- An event that lasts no time (a deadline) keeps its zero length on
save instead of becoming an hour long and asking everyone again.
- An attendee's save of their own copy - an alert on one occurrence, a
drag - wrote the participants again and sent the organizer a
"Reply:" mail. It now leaves the participants alone and sends
nothing. The editor shows someone else's attendee list read-only and
no "Send invitations" box, which only ever controlled the organizer's
messages; answers go out through the answer buttons.
- An existing override written on the base event (0.16 before 0.16.20,
and ranges the browser expands) gets the series' organizer again when
it has none, as every save wrote it before 4c20b09c: 0.16.19 refuses
an answer to such an occurrence. 0.16.23 takes the key and drops it;
1.0 refuses it inside an override once, and the store writes the
override again without it (both live).
- The comment on `organizerCalendarAddress` said Stalwart takes the
ORGANIZER solely from it. Both versions take it from the owner
participant when it is missing (live).