Selecting a tag does not clear an active search: `handleTagSelect` leaves
`searchQuery`/`searchFilters` alone and `selectKeyword` leaves
`selectedMailbox`, so a tag view and a search are active together. The
paths then disagreed about which one wins:
fetchEmails / refreshCurrentMailbox → `selectedKeyword` first, so the
tag fan-out ran and the query was dropped;
searchEmails / advancedSearch / loadMoreEmails → the search first, so
since #1082 the all-folders fan-out ran and the tag was dropped.
Both orders are defensible on their own; having both is not. A
push-triggered refresh silently replaced a search result list with the
whole tag, and "load more" appended rows the tag does not contain — the
same class of inconsistency #1082 set out to remove between a search and
its own next page.
Resolve it the way the codebase already documents ("a tag view takes
precedence over the selected folder"), and keep the query: a search inside
a tag view now narrows the tag, in all five paths. `fetchTagEmails` grows
an optional `extraFilter`, which `getEmails` already accepts alongside
`hasKeyword` — the same argument the message-list category tabs use — so
no new query plumbing is needed.
Searching within the tag is also the more useful reading: a tag view is a
list the user deliberately opened, and a search box above it reads as
"within this", not "forget this".
JMAP thread ids are only unique within their account - Stalwart hands out
per-account counters such as "b" - so the cross-account views merged
unrelated conversations that happened to share an id, and every
client-side thread map (expansion, cache, loading flag, counts,
virtualizer key) mixed them up the same way.
Give ThreadGroup an account-scoped `threadKey` derived from the email's
source stamps and key grouping and all thread state by it. `threadId`
stays the raw id for Thread/get, which #281 already routes to the owning
account. fetchThreadEmailCounts now asks each source account for its own
threads instead of one client for every id. Single-account views carry no
stamps, so their key is the bare id and nothing changes there.
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.
Some senders ship the images their HTML body embeds via cid: as
application/octet-stream with no Content-Disposition and no filename.
The body rendered them fine, but the "hide inline images" check only
recognised parts declared inline with an image/* type, so they also
showed up as nameless "Attachment" chips next to the real files.
Both viewers now scan the HTML they actually render for cid: references
(the same pattern the renderer rewrites to blob URLs) and hide a
referenced part when it is an image or generically typed. An explicit
attachment disposition always keeps its chip, as does a referenced part
with a real non-image type, since that chip is its only download. Plain
text rendering references nothing, so nothing is hidden there. The
shared rule lives in lib/attachment-visibility.ts so the desktop viewer,
the thread view and the decrypted-message path cannot drift apart.
isHttp2Available() only inspected "resource" timing entries, which can be
empty right after a fresh login or session restore, before any subresource
finishes loading. On an HTTP/2 page this made getMaxAccounts() fall back to
the HTTP/1.1 cap of five accounts. Include the "navigation" timing entry,
whose nextHopProtocol is known immediately, so h2/h3 is recognized on first
load. Environments without timing data still return false (no h2 assumed).
The browser paints a native <select>'s option list itself, so the
bg-muted / text-foreground classes on the control never reach it. Themes
whose --color-muted is translucent (Aurora Glass dark uses
rgba(255,255,255,0.05)) composited to a light popup with near-white
text, leaving every unselected option almost invisible.
Pin option background/colour to the popover tokens, which every built-in
theme defines as an opaque surface, and set color-scheme: dark on
selects under .dark so Safari (which ignores option colours) renders a
dark popup too. Fixes all ~40 native selects at once.
Adds a stylesheet test that locks the rules and asserts every built-in
theme's --color-popover stays opaque in each variant.
OAuth authorization-server metadata lives at the origin root (RFC 8414), but
getRequiredConfig() derived the discovery URL straight from JMAP_SERVER_URL. When
that is the session URL (for example https://mail.example.com/jmap/session, a
common setup so the client can skip the 307 redirect a preflighted CORS request
cannot follow), discoverOAuth fetched
https://mail.example.com/jmap/session/.well-known/oauth-authorization-server,
got a 404, and token refresh failed with "OAuth token endpoint not found", so the
session was dropped after one access-token lifetime.
Strip a trailing session path (/jmap/session or /.well-known/jmap, plus any query
or fragment) before discovery. A plain server root is unchanged, and only the
session suffix is removed, so a real base path prefix is kept.
Adds oauth-discovery-base.test.ts driving getRequiredConfig; the normalization
cases fail without the fix.
CalDAV-originated tasks can retain uppercase, underscore-separated, or single-l status spellings in CalendarEvent responses. Normalize known spellings at JMAP response boundaries so task filters and completion controls see canonical JSCalendar values while unknown future values remain intact.
Work around the upstream conversion defect tracked at https://github.com/stalwartlabs/calcard/issues/20.