Commit Graph
34 Commits
Author SHA1 Message Date
Linus Rath 4af42858f0 Merge pull request #1168 from lucamzanon/feat/mountain-view-layout
feat: add Mountain View, an optional alternative layout
2026-10-10 19:23:59 +02:00
lucamzanon 4347d25890 fix(mountain-view): show the admin console link in the top bar
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.
2026-10-09 08:28:40 +02:00
dealerweb 8abb817111 fix: keep a subject with runs of whitespace from freezing the tab title
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.
2026-10-08 18:10:47 +02:00
Linus Rath 2e262b9d5c fix: title the browser tab after the Pro tab in view #1186 2026-10-07 19:34:19 +02:00
Oğuz YılmazandClaude Opus 5.5 8ca7dbbe9b feat: name the active account in the browser-tab title
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>
2026-10-07 08:26:23 +03:00
Linus Rath 6e5c5390df Merge pull request #1162 from dealerweb/fix/focus-trap-rerender
fix: keep the focus in dialogs while the page behind renders again
2026-10-06 23:28:51 +02:00
Linus Rath 0b4d0b0f3c feat: limit the day and week views to working hours and days #1164 2026-10-02 22:56:16 +02:00
dealerweb 0c376dd0e4 fix: keep the focus in a dialog's field while the page behind renders again
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.
2026-10-02 12:42:40 +02:00
Linus Rath 770fb2c84a fix: resolve trash and archive in a background login's own folders #1132
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.
2026-09-30 20:35:13 +02:00
Linus Rath 2d2b0753be Merge pull request #1132 from lucamzanon/perf/multi-account-startup
perf: stop the startup request storm with many logins
2026-09-30 20:33:32 +02:00
Linus Rath cd14807133 Merge pull request #1112 from theLucius7/fix/config-load-recovery-20260927
fix: recover from transient application configuration failures
2026-09-30 20:32:44 +02:00
lucamzanon 612dcc81ee perf: stop reloading every login's folders and identities on each connection
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.
2026-09-30 19:04:49 +02:00
Linus Rath 9d140fa762 Merge pull request #1052 from lucamzanon/feat/account-row-colours
feat: colour unified rows by account and give every tag a hue of its own
2026-09-29 23:30:24 +02:00
Lucius7nya d923b92034 fix: recover from transient application configuration failures
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.
2026-09-27 15:41:21 +08:00
Linus Rath daf67850d2 fix: reply from the receiving account's identity in the unified inbox #1104 2026-09-26 12:31:29 +02:00
lucamzanon 42b6cc08b0 feat: colour unified rows by account and give every tag a hue of its own
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.
2026-09-19 05:44:44 +02:00
Linus Rath 2d4a562432 fix: harden Bulwark Lite login, deep-link replay and settings gating 2026-09-18 22:16:57 +02:00
Linus Rath 5de98bc04b feat: add Bulwark Lite static export build 2026-09-17 23:54:33 +02:00
Linus Rath ee4763d5c0 Merge pull request #1036 from dealerweb/fix/calendar-principal-fetch
fix(calendar): stop the account principal fetch from failing on every load
2026-09-17 22:03:26 +02:00
Linus Rath 2e88a0c830 Merge main into fix/calendar-participants
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.
2026-09-17 21:54:08 +02:00
dealerweb bc5302568d fix(calendar): wait for the JMAP client before loading the account principal
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.
2026-09-15 12:44:36 +02:00
Linus Rath 15a5497a12 feat: freely scrolling month, week and day views #759 2026-09-09 16:38:55 +02:00
Linus Rath 12416d9c47 fix: wire the x shortcut to thread expansion #683 2026-09-07 22:34:47 +02:00
ibayueandClaude 6f99c017a9 fix(calendar): resolve bare-address participant names across alias domains
The organizer of a self-created event still rendered as a bare address
(#738 follow-up): the event stores the organizer under the server's
node domain while the name lives elsewhere — the account/principal
name, the identity From-name, or a contact card on the account's
primary domain. Exact contact matching alone never hit.

useContactNameResolver now resolves in layers, first hit wins:

1. Contact card holding the exact address (curated data outranks all).
2. Contact card with the same local part on a *related* domain — one
   domain extends the other at a "."/"-" label boundary
   (node-example.com vs example.com, mail.example.com vs example.com).
   Applied only when the related cards agree on a single name.
3. The account display name (live principal "Full name") for the
   user's own addresses.
4. The identity From-name (a one-time snapshot of the same, so it only
   serves when the account layer had nothing).

Name sources that just echo the address itself are skipped — they add
nothing over the bare address renderers already show.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:48:26 +08:00
ibayueandClaude 8cbc5bc539 fix(calendar): dedupe participants, resolve contact names, show organizer status
Three bugs when viewing an event's participants (#738):

1. Phantom participant. Stalwart can emit the organizer both as the
   ORGANIZER-derived participant and as an ATTENDEE line, so the same
   address rendered twice — "4 participants" for three people.
   getParticipantList() now folds same-address entries into the
   first-seen one (real RSVP beats needs-action on merge), and
   getParticipantCount() counts addresses so event-card and agenda
   badges agree.

2. Bare email for a known contact. The organizer's display name is
   dropped on Stalwart's iCalendar round-trip, so the organizer
   rendered as a bare address despite having a contact card with a
   备注 name. New useContactNameResolver() hook fills missing
   participant names from the contact store (fill-only — event data
   stays authoritative), threaded through the panel, the hover
   popover, the RSVP view, and the organizer name written on save.

3. Organizer status. The organizer with no explicit status read as
   待处理; they owe no reply, so they now display as accepted, and the
   panel's status badge no longer repeats the organizer label the
   (组织者) suffix already carries.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-07 16:48:26 +08:00
KazNIISA IT e7d3782344 feat(pwa): badge the installed app icon with unread count (Badging API)
Adds useAppBadge alongside the existing useFaviconBadge, mounted from the
same FaviconBadge component and gated by the same faviconUnreadBadge
setting. Targets the separate icon Chrome/Edge/WebKit show for an
installed PWA (taskbar/dock/Home Screen/pinned-app-shortcut), which does
not react to favicon changes at all - a merely pinned browser tab is
unaffected, only an actually installed app.

No unmount cleanup: the badge is scoped to the installed app/origin, not
to this component or window, so clearing on every unmount would blank
another still-open window's accurate badge. A count of zero or the
setting being off already clears it through the main effect.
2026-09-03 11:37:36 +05:00
Linus Rath 7e9aeaf0d4 fix: calendar click/double-click created events near current time instead of the clicked slot 2026-08-21 18:04:33 +02:00
Shuki Vaknin 78c84f19a5 feat(mobile): swipe list rows left/right for quick actions
Adds Gmail-style horizontal swipe actions to message rows on touch
devices. Swiping a row past a threshold fires a configurable action and
snaps back; a coloured background with an icon is revealed under the
finger and grows when the release would commit.

- New `useSwipeActions` hook: claims a touch as a swipe only once the
  horizontal travel dominates (vertical stays native list scrolling via
  `touch-action: pan-y`, no preventDefault), resists past the commit
  distance, and suppresses the tap that would otherwise follow.
- Two settings (Settings -> reading/email behavior): "Swipe right
  action" (default Archive) and "Swipe left action" (default Delete),
  each one of none / archive / delete / toggle read / toggle star /
  spam. A direction set to an action the current folder does not expose
  simply does nothing.
- Composes with the existing long-press (context menu) handlers; desktop
  is unchanged (hover actions + right-click).
- Scoped to single-message rows; unit tests cover commit/short-drag/
  no-action-side/vertical-scroll/tap-suppression.
2026-08-19 02:18:01 +03:00
Linus Rath d9f9ed10cd fix: add embedded custom app origins to CSP frame-src #787 2026-08-16 23:48:58 +02:00
Linus Rath 66be55f6a7 feat: show a pull-to-refresh indicator when dragging down #826 2026-08-16 22:50:51 +02:00
Linus Rath e2da05c130 feat: deep links for mail, calendar, contacts, files, settings and improve screen reader support
#733 #722 #721  #720  #719
2026-08-06 16:47:20 +02:00
Mathy Vanvoorden 108406a885 feat: improve visualization of tags
Previously tags where very much focused on color coding email and less about
adding additional information. They were also visualized in different ways in
different locations.

This commit gets rid of all "Color-coding" references, aligns visualization of
the tags across the whole project and tries to improve user experience of using
tags in general.

A search box is shown in the tagging control so the user can quickly search for
a tag if they have a huge (more than 10) amount of tags.
2026-07-29 17:18:42 +02:00
Mathy Vanvoorden ca0ba818b7 feat: Add nesting of tags in a tree
- levels are joined by forward slashes in the keywords
- behaviour is opt-in for now
- long paths are shortened if there is not enough display room

Closes #687.
2026-07-29 17:18:38 +02:00
honzup 334fdbfb86 feat: show unread count badge on favicon
Closes #560.

Composes the active inbox's unread count over the base favicon as an SVG
badge, served as a percent-encoded data: URL, so new mail is visible on a
tab that is not focused — including when the browser collapses tabs to
icon-only, where a title-based count disappears entirely.

The base icon is read from the rendered <link rel="icon"> rather than from
config, so admin and per-domain branding overrides are inherited for free:
the count is drawn on whatever logo the deployment actually serves. Keeping
the badge in SVG rather than rasterising to a canvas also means the browser
can rasterise it at whatever size it asks for, so a HiDPI tab is not served a
16px bitmap.

Notes on the approach:

- The badge link is an *additional* icon link that we append and mark as
  ours; we never remove or mutate a link we did not create. Next's metadata
  icons are rendered by React, which keeps a fiber pointing at that DOM node,
  so removing it would leave React holding a detached node and throw
  "Cannot read properties of null (reading 'removeChild')" on the next
  commit that deletes the fiber. Appending instead means the last-declared
  icon wins, and non-SVG fallback links survive with their type/sizes intact.
  (The usual recipe for this feature — assign canvas.toDataURL() to the
  existing link's href — does both of the things that break here.)

- Every change of state is an *insertion* of a fresh link of ours, never a
  mutation or a removal, because that is the only signal a browser reliably
  re-reads the favicon on. Firefox ignores an in-place href change, and it
  equally ignores a removal — so clearing the badge by deleting our link left
  a stale count painted on the tab until a hard reload. Clearing it instead
  inserts a new link of ours carrying the original base href.

- Holding last place has to be defended: on a client-side navigation React
  re-hoists its metadata icon link into <head>, landing after ours, and the
  base icon silently wins again. A MutationObserver on <head> moves our own
  link back to the end whenever a foreign icon link appears — moving only our
  node, never anyone else's. It no-ops once ours is last again, so a move
  cannot feed itself.

- The badge is a full-width band across the foot of the icon, drawn to the
  metrics measured from Gmail's own 16px favicon: band height 0.625 of the
  icon, digit cap height 0.44, flush to the edges, corners rounded by about a
  pixel. Full width is what keeps a three-glyph label legible — rounded ends
  waste exactly the horizontal space it needs. Neutral white with black digits
  rather than the conventional red: faviconUrl is admin-overridable and
  Bulwark's own icon is rgb(219,45,84), so a red badge sat red-on-red.

- The base SVG may be admin-uploaded, and the branding route deliberately
  serves it under a sandboxing CSP because SVG can carry script. Re-emitting
  it as a same-origin data: URL would un-fence that, so script, foreignObject
  and every on* handler are stripped before serialising.

- Mounted in the root layout, not on the mail route: the badge belongs to the
  tab, so mounting it on the page would clear it on every hop to settings,
  calendar or contacts.
2026-07-16 18:00:41 +02:00