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).
On 1.0 a vCard export sent every blob photo through the avatar download
queue, a burst of 40 and then 6 a second, two at a time: about 160 s for
1000 photos. Avatars and print (which gives up after 5 s) waited behind
it, the Export button showed nothing, and a second click started the
whole export again.
- Export fetches the blob media of all cards with Blob/get
(`data:asBase64`), asking for the sizes first and packing the requests
to about 4 MiB. `urn:ietf:params:jmap:blob` goes into `using` only
when the session advertises it; without it, or when a request is
refused, the rest is downloaded one by one behind avatars and print,
which keep half of the download burst for themselves.
- The export buttons in the settings, the bulk bar, the context menu and
the detail menu show that an export is running and ignore clicks until
it is done.
- When the photo changes, the form rebuilds `media` on the card as the
server has it now. A logo or sound blobId from the list's older copy
made 1.0 refuse the whole update ("The following blobIds do not exist
or are not accessible").
- A new photo takes the key, place, `pref`, `label` and `contexts` of the
one it replaces, so it stays the photo shown on cards with several.
Removing the photo removes every photo the card had instead of only the
shown one, which brought up the next; logos and sounds stay.
- A card whose login has no client downloads nothing, instead of asking
the active login's server and remembering the refusal for two minutes.
- Photo downloads run at half the server's `maxConcurrentRequests`
instead of a fixed two.
- `invalidProperties` naming `media/...` also counts as a refused blobId
in media, not only `media` itself.
Stalwart 1.0 enforces maxParticipantsPerEvent (20 by default) on every
CalendarEvent create and update, answers to invitations included, and
refuses anything over it with invalidProperties "An event cannot have
more than 20 participants.". 0.16 advertises the same value but never
enforced it. Creating, editing, moving or answering a bigger meeting
failed with a generic "Failed to save event".
- read the limit from the calendars account capability and warn in the
event editor when the attendees go over it, without blocking the save
- name the limit, and that an administrator can raise it, when the
server refuses a create, edit, move, resize or answer for it, in the
calendar and in the invitation banner
- keep the SetError type on refused event creates and updates
In the unified view each row can be shaded with its account colour
instead of carrying an account dot, behind a layout setting. Tags keep
their own generated hue on their chips.
Resolves the two conflicts in components/calendar/event-modal.tsx created
by #967 (RSVP controls), both keep-both:
- #967's canRsvp memo and #986's resolveContactName hook are both kept;
they are independent and were simply inserted at the same point.
- #967's RsvpBar component is kept, and StatusBadge takes #986's
signature ({ status, t }) since #986 drops the isOrganizer prop and
updates both call sites to match.
The calendar loads the principal's alias addresses so an alias-organised
event is recognised as the user's own. The fetch fired on mount, but after
a page load the JMAP client is restored asynchronously: the call failed
with "Not authenticated" in the console, and the once-only guard then
blocked the retry for the rest of the session, leaving the aliases empty.
Move the logic into useAccountPrincipalEmails, which waits for the client
and fetches once, and cover the sequence with a hook test.