Commit Graph
32 Commits
Author SHA1 Message Date
Linus Rath b50270d8f3 feat: search events in the calendar 2026-10-29 04:12:48 +01:00
Linus Rath 9b87100a15 feat: set a calendar's default reminders and start new events with them 2026-10-29 04:02:40 +01:00
Linus Rath acbd085ab4 fix: end a zero-length event at its start instead of at midnight 2026-10-29 03:38:25 +01:00
Linus Rath c0c3429c75 fix: forget a shared calendar refusal that arrives after a new sign-in 2026-10-29 02:59:53 +01:00
Linus Rath 8a71495e1a fix: apply subscribed feed changes to imported events and stop a feed sync when the account changes 2026-10-29 02:40:13 +01:00
Linus Rath b6c0cf1820 docs: say that Stalwart 1.0.0 answers the Metadata properties the calendar list leaves out 2026-10-29 02:12:16 +01:00
Linus Rath 7c775571c9 fix: load the calendar on Stalwart 0.16 when a range starts or ends in the hour the clocks change 2026-10-29 01:58:24 +01:00
Linus Rath f3e11d4c7c fix: stop asking an account that shares only mail for its calendars on every load on Stalwart 1.0 2026-10-29 01:54:55 +01:00
Linus Rath 198e952f61 fix: tell attendees when someone with write access changes the owner's meeting
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. The app
decided who organizes by the user's own addresses only, so it took the
sharee for an attendee (0.16 and 1.0 alike):

- the editor wrote no participants, hid "Send invitations" and sent
  nothing, so the attendees kept the old time and their answers
- "All events" asked nobody again, and "This and following events"
  made the rest a new series that kept the old answers, which Stalwart
  sends to nobody who had answered

The app now reads `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. `userOrganizes` counts such a copy as the organizer's; an
invitation is still changed without messages. "This and following"
asks by the old series, since the new one carries no `isOrigin` yet.

The editor no longer leaves the user's own main address off the
attendees it opens with, only the organizer's (or the user's, for an
event that names none yet): a user who invited their main address to a
meeting organized under an alias lost it on the next save.
2026-09-26 04:00:59 +02:00
Linus Rath a562ea59b8 fix: keep attendees' answers on save and deliver occurrence changes
Every save in the event editor rebuilt the participants with new ids
and put every attendee back to needs-action, the user's own answer on
an invitation included. Saves now keep each attendee's id and answer;
new attendees are asked. As on the web, only an organizer who moves an
event (its start, end, all-day or recurrence rule; not a time zone
rewrite, and not a zero-length event) asks the others again.

The series scopes did not fit that:

- "All events" wrote the opened occurrence's start, length, zone and
  participants onto the series: a stretched occurrence stretched every
  one, and one day's decline spread to the whole series. The series now
  keeps its own list and takes only the attendees the user added or
  removed, and its times change only when the occurrence moved (by the
  same delta), its rule only when it changed.
- "This and following" makes a new series, which Stalwart sends only to
  attendees who have not answered: its attendees are asked again.
- Stalwart sends a new occurrence override, the first change to one
  occurrence, only to attendees who have not answered (0.16.6 to 1.0).
  A change that keeps answers now goes out in two writes: the override
  as the occurrence stands without scheduling, then the change as an
  update of it, to the occurrence looked up again. Checked live on 1.0
  and 0.16.23.
- A save of an invitation someone else organizes writes no participants
  while the user left them alone, and never asks for scheduling, so the
  organizer gets no reply for a changed alert. The switch to send
  invitations is hidden there.

Also:

- a reminder change on one occurrence of a series that uses the
  calendar's default alerts turns the defaults off for the series first
  (keeping what it shows under ids of its own); 1.0 kept showing the
  defaults
- all-day and floating occurrences expanded on the device no longer
  carry the master's utcStart/utcEnd, so their reminders are scheduled
  for each day; master instants that are not the master's own (0.16.23's
  year 32548, or the next occurrence's) are ignored
- the editor warns when the attendees and the organizer go over the
  calendars capability's maxParticipantsPerEvent, and a refusal for it
  (Stalwart 1.0 enforces it, answers included) is explained in the
  editor, the series dialogs, the detail sheet and the invitation
  banner, in all languages; a failed save used to fail silently
- refused event creates, updates and answers keep their SetError type
- Calendar/get asks for its properties by name
2026-09-25 23:50:29 +02:00
Linus Rath f490886a8b fix: keep iCal subscriptions safe when the mail server was replaced
When the server behind an account is replaced (a fresh Stalwart 1.0
install at the address 0.16 ran on), its ids start over. The app
already dropped its caches then, but kept what the user set up with
server ids inside:

- iCal subscriptions kept the ids of their mirror calendars. The next
  sync deleted the "stale" events of whatever calendar now had the id
  and filled it with the feed, and "Remove subscription" destroyed it.
  Now the account's subscriptions keep their feed, name, colour and
  interval but lose their mirror, and the next sync makes a new one as
  adding a subscription does. Renaming or removing an unlinked one
  touches no calendar. Events are only pruned from, and remove only
  destroys, a calendar that still carries the subscription's name.
  Subscriptions now record the signed-in account, so accounts on two
  servers that got the same JMAP id no longer share them.
- the preferred sender identity kept its id, which a new server can
  give to another address (live: identity "b" is usera@ on 0.16.23 and
  team@ on 1.0). It is forgotten for the account's old and new JMAP
  id, which the app now records with the server fingerprint.
- hidden calendars kept their ids. The list is shared by all accounts,
  so it is cleared.

Offline changes the drop discards are now announced once ("3 offline
changes were discarded because the mail server changed"), in all
languages.

Revoking this device from another one turns push off here again: a
subscription gone before its recorded expiry counts as revoked, as it
did before the server check came in. A replaced server forgets the
recorded expiry, so there the subscription is re-created instead.
2026-09-25 23:49:36 +02:00
Linus Rath 90b78da8c8 fix: keep time zones, default alerts and calendar renames working on 1.0
Four smaller calendar changes that Stalwart 1.0 needs.

- Time zone: 1.0 fails every CalendarEvent get and query whose
  timeZone its database lacks, with invalidArguments "Invalid timeZone".
  It matches IANA names case-sensitively and knows no offsets, so a
  "+01:00" or "europe/berlin" that Intl accepts empties the calendar.
  0.16 reads such a zone as UTC. The zone is now checked with a get of
  no events that rides along with the first calendar list; once refused,
  reads ask in UTC and log one warning, and a read refused before the
  check answered is repeated once in UTC. The comment that said Stalwart
  ignores bad values is corrected.
- Default alerts: 1.0 fills a calendar's default alerts into the
  alerts of an event that uses them. The editor wrote them back with
  useDefaultAlerts false on every save, freezing today's defaults into
  the event. It now writes nothing while the reminders are unchanged; a
  change turns the defaults off and saves the alerts under ids of their
  own, since 1.0 drops alerts saved under a default alert's id.
- Calendar colour: renaming sent the colour along, and 1.0 accepts only
  #rgb, #rrggbb or a CSS name, so a calendar another client coloured
  with an 8-digit hex could not be renamed. The edit sheet and the iCal
  subscription settings now send only the fields that changed.
- Reply stubs: 0.16 stored an attendee's answer to one occurrence
  without a title, place or description, and 1.0 builds that occurrence
  from the override alone. Hydration now takes a missing title from the
  series, and the place and description when the override removes the
  title.
2026-09-25 20:44:34 +02:00
Linus Rath 2a3febcc79 fix: edit or answer one occurrence of a series in the app on Stalwart 1.0
Stalwart 1.0 refuses series properties such as the organizer on a
single occurrence unless they repeat the series' value, and refuses
them inside a recurrence override on the base event. The event editor
wrote the user's first address as organizerCalendarAddress on every
save of an event with attendees, so editing one occurrence failed with
"This property cannot be modified on a single occurrence." whenever the
series was organized by someone else, under another alias or in
another letter case. Removing every attendee from one occurrence failed
the same way.

- leave series keys and per-event metadata out of changes to one
  occurrence, as Stalwart's validate.rs lists them, and pointers at a
  participant's calendarAddress
- write overrides on the base event as before (0.16.19 needs the
  organizer there) and, when the server refuses series keys inside
  them, repeat the write once without them and remember that
- answer one occurrence through its synthetic id without the organizer
- let the editor keep the organizer of an event: an invitation the
  user received is no longer rewritten with the user as organizer, and
  organizerCalendarAddress is only written for an event that has none
- keep the SetError type on a refused event update

0.16.20 to 0.16.23 dropped these keys on synthetic ids anyway; the
base-event override payload for older servers is unchanged.
2026-09-25 20:36:58 +02:00
Linus Rath ef6f1540dd fix: keep the app's calendar filled past a year on Stalwart 1.0
Stalwart 1.0 refuses an expanded CalendarEvent/query that spans more
than the account's maxExpandedQueryDuration (P52W1D by default) with
expandDurationTooLarge, and one that expands to more than 3000
occurrences with cannotCalculateOccurrences. The scrolling views load
windows of up to about 460 days, and refresh() reloads the whole
window on every calendar push, after series edits and on pull to
refresh, so the calendar showed a raw error over stale events from
then on. The fallback to expanding on the device only knew 0.16's
invalidArguments.

Expanded range queries are now cut into chunks a day shorter than the
advertised limit, with overlapping borders, sent together as far as
maxCallsInRequest allows, and their ids de-duplicated. A chunk the
server still refuses is halved; if a week still expands to too many
occurrences, the whole range is expanded on the device as before.

The expanded query keeps a filter of the two bounds only: 1.0 refuses
operators there, so filtering several calendars would take a query per
calendar, and the store loads every calendar anyway. 0.16 advertises
the same limit without enforcing it and returns the same events.
2026-09-25 20:35:00 +02:00
Linus Rath 28ade775e3 fix: load calendar events and tasks in the app on Stalwart 1.0
Stalwart 1.0 refuses a CalendarEvent/get that asks for
recurrenceOverrides together with utcStart or utcEnd, and every event
read of the app did. The calendar showed a raw error over stale events,
and tasks, reminders, "all events" edits, contact activity, the
invitation banner's UID look-up and iCal subscriptions stopped working.
Subscriptions and imports also lost their UID check, so every sync tried
to create the whole feed again and ended in refusals.

- read a stored event's overrides with a second get in the same
  request; server-expanded occurrences get them from their base event
  as before
- read only ids, UIDs and calendars for the import and subscription
  UID look-ups
- fail a read when either get answers with an error, so the calendar
  keeps the events it has and shows the error
- keep the JMAP error type on calendar method errors (JMAPMethodError,
  plus a shared jmapErrorType helper in jmap-result)

0.16 accepts the split reads and returns the same events.
2026-09-25 19:45:19 +02:00
Linus Rath 84548898c1 feat: let the server expand recurring events when it can
Recurring series were always fetched whole and expanded on the device,
the webmail's fallback for Stalwart before 0.16.20. Newer servers
expand them with `expandRecurrences` and accept the synthetic
occurrence ids in `CalendarEvent/set`; the webmail uses that since
`lib/recurrence-instances.ts`.

- `supportsSyntheticCalendarIds` probes once per server and account
  with an update of an id that cannot exist (`notFound` means
  supported); the probe rides along with the first `Calendar/get`
- when supported, a range is loaded with `queryExpandedEvents`, the
  occurrences are fetched with `baseEventId` and hydrated with their
  base event's rules, overrides, all-day flag and duration; duplicate
  listings of one occurrence are collapsed like the webmail does
- a range the server refuses to expand ("expanded recurrences
  exceeds") and older servers keep the device-side expansion, which
  now passes server occurrences through untouched
- changes, deletes and RSVPs resolve a synthetic id to its base event
  (`resolveSeriesTarget`), including a master fetched for an
  occurrence of a shared calendar, and reload the range afterwards
  since synthetic ids reshuffle
- reminder keys use the base event and recurrence id, so a reshuffle
  does not reschedule them
- client-only fields are stripped from `CalendarEvent/set` payloads
2026-09-24 20:10:08 +02:00
Linus Rath a4ac6cad5c fix: list the events an .ics import could not bring in
A partial import dropped the refused events silently: the sheet said
"Imported 3 events" for a file of five, and the server's reason never
reached the user. The webmail reports how many events failed and why.

- `batchCreateEvents` returns the refused events by index with the
  server's reason instead of just the created count
- `importEvents` resolves with `{ imported, refused }`, also counting a
  refused link of a UID that lives in another calendar and every event
  of a chunk whose request failed; when nothing got in it rejects with
  an `ImportRefusedError` carrying the list, so the invitation banner
  still never reads a refusal as "Already in your calendar"
- the import sheet shows the imported count and lists each refused
  event with its date and reason
2026-09-24 20:10:07 +02:00
Linus Rath a6ef0abc2a fix: report calendar deletes and imports the server refused
`deleteEvents` never looked at the `CalendarEvent/set` answer, so a
refused delete (read-only calendar, method error) removed the event
from the screen and said nothing until the next refresh brought it
back. Clearing a calendar ignored `notDestroyed`/`notUpdated`, and an
import the server refused entirely returned 0, which the invitation
banner showed as "Already in your calendar".

- check the destroy result in `deleteEvents` and `clearCalendarEvents`
  with `assertSetResult`, and the unlink result as well
- throw from `batchCreateEvents` when nothing was created and the
  server refused events; a partial import still returns its count
- show an alert when deleting an event or a task fails, and refresh
  after a partly failed import or clear
2026-09-24 19:12:02 +02:00
Linus Rath c0582ab950 fix: send the RSVP for invitations outside the loaded calendar window
Accepting an invitation looked the event up in the calendar store,
which only holds the window the calendar last showed. For an event
further out the lookup came back empty, no response was sent, and the
banner still said "Added to calendar".

- find the event on the server with a `uid` filter, like webmail's
  `queryCalendarEvents({ uid })`, both when the banner opens (so it
  shows as already in the calendar) and after importing it
- let `rsvpEvent` take the event when it isn't in the store, so the
  organizer repair still applies
- show "Could not send your response" whenever no response went out
2026-09-24 19:12:02 +02:00
Linus Rath 1b64c14280 fix: treat CalDAV tasks marked COMPLETED, NEEDS_ACTION or canceled as such
Tasks created over CalDAV can come back with their iCalendar STATUS
spelling or as `canceled` (stalwartlabs/calcard#20). The app compared
against the JSCalendar values only, so a completed CalDAV task counted
as pending, still raised its reminder, and a tap on it completed it
instead of reopening it.

- normalize the known spellings when events are read, like webmail's
  `normalizeCalendarTask` (5f1d2795), and keep unknown values as they
  are
- reopen a cancelled task on tap, since the tasks sheet shows it ticked
2026-09-24 19:12:02 +02:00
Linus Rath 3f2dfe83ee chore: merge upstream fixes for reply threading, iOS session auth drop, calendar paging and the camera prompt (#35, #38, #39, #40) 2026-08-29 20:16:33 +02:00
Linus Rath 8206893860 feat: assess invitation trust, read the iTIP method from the ICS and pick the target calendar in the invitation banner 2026-08-29 18:22:00 +02:00
Linus Rath dae5700688 feat: manage calendars from the drawer and scope, edit and auto-refresh iCal subscriptions 2026-08-29 18:14:48 +02:00
Linus Rath 58e3ef6827 fix: query events by date window, scan tasks separately and stop re-probing denied shared accounts (#907) 2026-08-29 18:05:56 +02:00
Linus Rath 5dc40706fa fix: send invitations with a server-scheduled organizer and match RSVPs against identities and aliases (#731) 2026-08-29 17:52:26 +02:00
lucletoffe 32b5298cdd fix(calendar): page a server-side date filter through CalendarEvent/query
queryEvents used to ignore its after/before arguments and ask Stalwart
for every event in the account with limit:1000. When an account holds
more than one page of events, the response was silently capped and the
grid rendered empty for the visible month — the missing events sat past
the truncation, never fetched. Meanwhile any recurring master whose
base start predates the window was returned exactly the same way, so
the client-side recurrence expander (already wired) never saw it.

Now the query sends after/before as a real filter ANDed with the
inCalendar restriction, and iterates position by page size until a
short page comes back (or 20 pages, as a bug backstop). Stalwart's
matcher accepts a master when any occurrence overlaps the window, so
the expander gets the raw event and produces the visible occurrences.

The refactor factors buildEventFilter() so the calendar / no-calendar
and window / no-window combinations produce the right filter shape
(single condition, AND, or absent). Tests updated to lock the new
filter shape and to cover the paging path (regression: 1000 events
that used to become 1000, now 1002).

Picked up from Jim Preston's uncommitted diff in the sister worktree
during the 2026-08-03 diagnostic session (artifact PkOWkT8C).
2026-08-06 09:20:40 +02:00
Linus Rath 802e1e1c71 fix: send-time editor readback, base-view restore after search, Stalwart recurrence #9, #10, #13 2026-07-22 22:17:40 +02:00
Linus Rath 643d704b74 fix: namespace shared calendar ids to avoid collisions with own calendars 2026-06-14 14:46:15 +02:00
Linus Rath e6d47b6a16 feat: shared calendars, custom recurrence editor, and file sharing 2026-06-12 17:15:50 +02:00
Linus Rath d616943c25 feat: add unified inbox, scheduled send, iCal import/subscriptions, tasks, and contact tagging/import 2026-05-29 15:34:39 +02:00
Linus RathandClaude Opus 4.7 5e99bb36b2 Sync local work: multi-account, calendar/contacts, compose, perf
Contents accumulated in the working tree since the initial commit:

- Multi-account: account-store, per-accountId credential scoping, account
  switcher in sidebar, add-account flow on LoginScreen
- Calendar: recurring event expansion, EventModal/EventDetailSheet, week/
  agenda views, calendar sidebar, participant input, per-day event index
  for O(1) cell lookup (MonthView/WeekView/AgendaView)
- Contacts: contact detail/form/group screens, vCard import/export,
  avatar utils, contact picker sheet, sidebar drawer
- Email: compose screen, thread screen, sender avatar, EmailBodyView with
  CID image resolution and external-content policy
- Infrastructure: navigation types, mailbox tree helper, settings store

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 01:21:50 +02:00
Linus Rath 11b9b86acd Initial commit: React Native JMAP webmail app 2026-03-30 00:49:22 +02:00