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.
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>
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>
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.
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.
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.
- 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.
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.