Commit Graph
6 Commits
Author SHA1 Message Date
jcoffey-dev 373822c2ae Resolve the base event id in the calendar store, not at the call sites
Closes #133.

`updateEvent`, `destroyEvent` and `rsvp` took an id and sent it. The
`baseEventId ?? id` that made them hit the series lived at four call
sites instead, and every one of them happened to be right.

That was backstopped by the server until now. Through 0.16.19 a synthetic
id reaching `destroy` came back as "Deleting synthetic ids is not yet
supported" and the user saw a toast. 0.16.20 accepts it and removes one
date instead, reporting success under a dialog that said "Delete all
occurrences?" - so a forgotten `??` became silent data loss rather than
an error.

The three methods now take the event and a required `scope`, and there is
exactly one place that turns an event into an id. A caller that wants the
series cannot get an occurrence by forgetting anything; a caller that
wants one occurrence has to say so.

`rsvp` takes the event rather than an id for the same reason, and no
longer looks it up: its patch is `participants/{key}/participationStatus`,
which is one of the pointers 0.16.20 *allows* on an occurrence, so aimed
at an instance it would quietly mean "only that day".

`findByUid` says in a comment that its query omits `expandRecurrences` on
purpose, since InviteCard hands the result straight to `destroyEvent`.
2026-08-30 21:06:56 -07:00
jcoffey-dev c1ef19849e Say it in the words Stalwart 0.16 answers to
Guests added to an event vanished on save and no invitation was ever
sent. Not a guard in the editor, and nothing the server complained
about: ihasmail addresses a participant the way RFC 8984 does, with
sendTo and email, and Stalwart 0.16 keeps that address under
calendarAddress. Handed the RFC's spelling it stores the event, drops
the entire participant map, and reports success. Six shapes were tried
against a live 0.16.19, down to sendTo and roles alone; all six were
dropped, and patching a participant onto an existing event fails
outright with "Patch operation failed".

The same disagreement runs through two more properties. The organizer is
organizerCalendarAddress, not replyTo. A recurrence is a single
recurrenceRule, not a recurrenceRules array — and that one Stalwart
refuses honestly, with invalidProperties, so no recurring event could be
created at all and existing ones showed no repeat.

So writes now use Stalwart's names and reads accept either, since a
mailbox may hold events written by other clients. The mock now refuses
what the real server refuses and drops what it drops: advertising the
RFC spelling is exactly how this reached a live server unnoticed, the
same way the capability-placement bug did.

Verified against 0.16.19: participants, organizer and rule all survive a
create, an update and a re-read, with the roles kept as sent.

Fixes #26
Fixes #30
2026-08-25 08:26:34 -07:00
jcoffey-dev 8b22ea9aab Stop calling every event a series
A one-time event opened for editing said "this is a recurring event —
changes apply to the whole series", and deleting one offered to delete
all occurrences of an event that has exactly one.

Three places asked whether an event had a baseEventId and took that for
recurrence. It isn't: the calendar loads its range with expandRecurrences,
and Stalwart puts a baseEventId on everything it returns that way, a
one-off pointing at itself included. The mock never sets the field at
all, which is why this only showed up against a real server.

They now share isRecurring(), which asks about recurrence rules, and
treats a base that is some other event as an occurrence of a series too
— so an expanded instance that travels without its rules is still
described honestly on the way to being deleted.

Fixes #25
2026-08-25 07:01:23 -07:00
jcoffey-dev 2518605126 Custom date and time pickers that follow the configured format
Browsers render <input type="date"> and datetime-local in their own locale and
ignore the page's, so #1 left a German user on an English browser reading
22.11.2025 everywhere but still entering dates through an mm/dd/yyyy widget.
#3 makes the case that people use the picker rather than typing, which is
where the AM/PM mistakes happen.

New DateField and DateTimeField (web/src/ui/datefield.tsx) replace all nine
native controls — event editor (all-day and timed start/end, recurrence
until), out-of-office, contact birthday, advanced search. They take and emit
the same ISO strings the native inputs did, so call sites barely changed.

Each is a text box in the configured order plus a popover: a month grid
(week start from settings, locale weekday and month names, today and the
selection marked) and, for date-times, a list of times in the configured
clock. Keyboard: arrows move by day, PageUp/PageDown by month, Home/End
across the week, Enter picks, Escape closes, ArrowDown opens; the focused day
holds DOM focus so screen readers follow, and the dialog has an accessible
name (Popover gained an ariaLabel prop).

Text entry is lenient — the configured order with any separator, unseparated
digits (221125), day and month alone, non-Latin digits, and bare ISO always;
times take 18:23, 1823, 6:23pm, 930. What will not parse reverts on blur
rather than clearing the field, and impossible dates like 31 February are
rejected instead of rolling into March.

Editable boxes stay Gregorian and Latin-digit even where display does not
(fa-IR, th-TH, ar-EG): the locale's field order and separator are kept, but a
Buddhist-era year in a text box cannot round-trip against a Gregorian grid.
Noted in the README.

The out-of-office format echo added in #2 is gone — the fields now show the
right format themselves.

Closes #3
2026-08-23 13:12:17 -07:00
jcoffey-dev d82ff15921 Configurable date and time formats, defaulting to the Stalwart locale
Every user-visible date now goes through web/src/lib/datetime.ts, driven by
three settings (Settings > General > Locale):

- Language & region: automatic, or any of the 618 locales CLDR has data for,
  each named in its own language and script (web/src/lib/locales.ts, generated
  by probing Intl over the subtag space).
- Date format: automatic (locale order), 22.11.2025, 22/11/2025, 11/22/2025,
  or ISO 8601 2025-11-22.
- Time format: automatic (locale), 24-hour, or 12-hour.

Automatic takes the locale Stalwart has for the account, read best-effort at
login via x:Account/get (urn:stalwart:jmap) and passed to the client in the
session; servers without the capability, or that deny sysAccountGet to a
regular user, fall back to the browser locale. POSIX forms are normalised
(de_DE.UTF-8 -> de-DE) and script modifiers kept (sr_RS@latin -> sr-Latn-RS,
uz_UZ@cyrillic -> uz-Cyrl-UZ), while dialect/variant/currency modifiers are
dropped and a script the locale already implies is not appended.

Numerals follow the locale (22.11.2025 renders as Arabic-Indic digits under
ar-EG); ISO 8601 is the exception and pins date and clock to Latin digits so
one line never mixes digit systems.

Rewired: message list and headers, quoted reply headers, calendar (titles,
weekday and hour gutters, mini calendar, agenda, popovers, invite cards,
free/busy), contacts, files, sessions. No raw toLocale*String date calls are
left in web/src.

Native <input type="datetime-local"> pickers always follow the browser locale
and cannot be restyled by a page, so the out-of-office fields echo the entered
instant in the chosen format underneath.

Also: month-grid day labels no longer wrap when they hold a date, and the mock
server serves x:Account/get (MOCK_LOCALE, default en_US).

Closes #1
2026-08-23 12:32:11 -07:00
jcoffey-dev 645b8b510f ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
2026-08-23 01:07:13 -07:00