The day header in the calendar month view was rendered outside the vertical scroll container, causing each header column to divide the full container width (100%) while the day grid cells below divided the remaining width excluding the vertical scrollbar. On platforms with classic scrollbars, this caused an escalating misalignment between the header columns and the day cells.
Move the day header inside the scroll container as a sticky row (sticky top-0 z-50 bg-background), matching the pattern established in the week and day views, and account for the header height in row minimum height, scrollToFocus, and sampleVisibleMonth.
The sidebar showed the same people icon for any calendar with a share,
labelled "Share calendar", and the settings badge only gave a count, so
there was no way to tell from either who could see the calendar. A share
granted to a service account read like a share with other people.
Both now carry a hint listing the recipients ("Shared with
bot@example.com (Booking robot)"), resolved from the principals the
server lists. Until every recipient is known the hint falls back to the
count. Principals are fetched only when a calendar on screen is shared.
The per-client principal cache of the attendee availability check moves
to lib/jmap/principal-cache.ts so both use it. It no longer keeps an
empty or failed answer: the next caller asks again instead of seeing
no principals for the rest of the session.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On wide screens a month row keeps its height instead of growing for
the busiest day. A day with more entries than fit shows the first ones
and a "+N more" link that opens the day view, and clicking a date
number opens that day too. Today keeps the filled circle and the
selected day is tinted instead of looking the same. Phones still grow
the row to fit and open a day by tapping the cell.
The hook ran before the modal entered its saving state, so Save and
Ctrl+Enter stayed live while a plugin handler was busy. Each extra press
created another event with a fresh uid and sent another invitation.
- mark the modal as saving before the hook runs
- pass uid and attendees to the conflict-check view too
- don't send a null meeting link when creating an event
The all-day/tasks area in the week view took its height from
(allDayRowCount + taskRowCount) across every loaded day. Since free
scrolling loads 37 to 180 days, one crowded day anywhere in that range
blew the header up to 250+ px for every week and pushed the time grid down;
tasks were also stacked below the global all-day row count, leaving a gap
on days without all-day events.
- Stack each day's tasks directly below that day's all-day events.
- Size the area from the days in view only: track the first visible day by
date (so columns prepended while loading do not shift it) plus the number
of visible columns (7 on desktop, by width on mobile).
- Cap the area at 3 rows with an expand/collapse toggle when a visible day
has more entries.
Tests cover per-day task stacking, the 3-row cap and toggle, an empty week
next to a crowded one, and the area growing when scrolling into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two leftovers of 9f7c12ab and 021a6732:
- A sharee who attends the owner's meeting and moves it in the owner's
calendar was asked again along with everyone else. Stalwart 0.16
counts the addresses of the user who writes as the account's own and
sends them nothing, so her answer stayed needs-action in the owner's
copy with nothing asking her to answer again, and her own copy kept
the old time (live on 0.16.23; 1.0 sends her the change). A move, a
drag or a new rule now keeps the answer of the user who makes it, in
the series and in the overrides it rewrites; the other attendees are
asked again as before. The rest of a split series still asks
everyone, the user too: 1.0 invites to a new event only who has not
answered.
- The SEQUENCE a change of every occurrence raises came from the series
as the app held it. A series opened from a link while another client
changed it gave a lower number than the attendees had, which 0.16
stores and the attendees refuse, and a pointer into an override
removed meanwhile failed the whole save. The store now reads the
series right before that write, through the account the write goes
to, and the raise starts from what the server holds: the series'
SEQUENCE, the overrides it has now, and for an override the change
rewrites at least one above the series. 1.0 raises the numbers the
same way itself, so the result there is unchanged (live).
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. Since 6d4b6954 Bulwark
decided who organizes by the user's own addresses only, so it took the
sharee for an attendee (live on 1.0 and 0.16.23):
- a drag, a resize or a month-view drop wrote the new time without
scheduling messages: the attendees kept the old time and their answers
while the owner's copy moved; before 6d4b6954 they got "Updated
invitation"
- the editor wrote no participants, hid "Send invitations", showed the
attendees read-only and sent nothing
- "All events" asked nobody again, and "This and following events" ended
the series quietly and made the rest a new series without attendees
`userOrganizes` now also counts the organizer's own copy in another
account: `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. Such an
invitation is still changed without messages. The editor no longer drops
the user's own address from the attendees it opens with, only the
organizer's, so a sharee who attends the owner's meeting stays on it.
The demo's invitation from Alice now says it is not the demo user's copy.
A move outside the editor kept every answer. A drag or resize in the
week and day views, a drop in the month view and an applied counter
proposal wrote the new time with the attendees' stored answers:
Stalwart's update then said "accepted" for a time nobody had accepted,
and a dragged occurrence, which gets its own override, reached no
attendee who had answered (live on 1.0 and 0.16.23). When the user
organizes the meeting these moves now ask the attendees again, as the
editor does; one helper (`userOrganizes`) decides for both.
Also:
- A changed recurrence rule counts as a move for the series: a custom
rule moved from Monday to Tuesday asks again. A rule the user did not
touch is no change however the server spells it, and on one
occurrence, where no rule applies, it is none.
- An event that lasts no time (a deadline) keeps its zero length on
save instead of becoming an hour long and asking everyone again.
- An attendee's save of their own copy - an alert on one occurrence, a
drag - wrote the participants again and sent the organizer a
"Reply:" mail. It now leaves the participants alone and sends
nothing. The editor shows someone else's attendee list read-only and
no "Send invitations" box, which only ever controlled the organizer's
messages; answers go out through the answer buttons.
- An existing override written on the base event (0.16 before 0.16.20,
and ranges the browser expands) gets the series' organizer again when
it has none, as every save wrote it before 4c20b09c: 0.16.19 refuses
an answer to such an occurrence. 0.16.23 takes the key and drops it;
1.0 refuses it inside an override once, and the store writes the
override again without it (both live).
- The comment on `organizerCalendarAddress` said Stalwart takes the
ORGANIZER solely from it. Both versions take it from the owner
participant when it is missing (live).
Saving "All events" from the editor opened on one occurrence wrote that
occurrence's data to the whole series. Renaming a weekly series from its
third occurrence moved the series' start to that day: the first two
occurrences were gone, a counted series ran two weeks longer, and
overrides before the new start were orphaned. The occurrence's
participants went to every occurrence as well, so an attendee who had
declined only that day now declined the series, and Stalwart sent them
no more updates (live on 1.0 and 0.16.23). The check for a move
compared the form with the occurrence, so the rename that moved the
series did not ask anyone again.
The series now keeps its own:
- when it happens, unless the occurrence was moved; then the series
moves by as much, from its own start and in its own time zone, and
the length changes only when the editor changed it
- its participants and their answers, with the attendees the user added
or removed in the editor; when the series moves, its organizer asks
the attendees again
The scope handling moved from calendar-app.tsx to
`lib/recurrence-scope-edit.ts` so it can be tested. The editor now hands
its attendees (as opened and as saved) and its dates to the save.
Four places took a narrow refusal from the server for a complete one:
- vCard export: Stalwart answers Blob/get only in accounts the user is
a member of, so for an address book shared with the user it came back
`forbidden` and the export fell back to one photo download after the
other. It checks the access to each blob by itself, whichever account
the request names, so `getBlobsAsBase64` now asks again in the user's
own account (checked live on 1.0 with a book shared by another user).
- Sending at a full submission quota: when making room found nothing,
no search ran for five minutes, for any send. Scheduled mail and
undo-send holds can go once their message is out, so a send the user
starts now always looks again; the pause stays for automatic read
receipts.
- Free/busy: a directory that could not be listed hid the attendees'
free/busy behind "Free/busy isn't available on this server". Listing
principals and looking up free/busy are separate permissions on 0.16
and 1.0, so the line now shows only for Stalwart 1.0 with directory
queries off or a lookup refused with `forbidden`; otherwise the
principals the server still returns are looked up.
- Mark as read and keyword writes over several requests: a request that
failed outright made the whole selection count as unchanged, and rows
already written showed unread until the next refresh. The write now
stops there and names only that batch and the ones after it.
Fixes from a second review of the Stalwart 1.0 calendar work.
- Changing the alerts of one occurrence of a series that uses the
calendar's default alerts said "Event updated" and kept the default
on 1.0: while a series uses them, 1.0 shows the defaults in place of
an occurrence's own alerts and refuses `useDefaultAlerts` on a single
occurrence. The series now stops using the defaults first, keeping
the alerts it shows under ids of their own, and then the occurrence's
alerts go out. 0.16.23 showed the edit already and still does.
- The participant limit counts the organizer entry a save adds (live on
1.0: 19 attendees and the organizer are accepted, 20 are refused), so
the editor warned one attendee late. It warns at the limit now, and
the warning and the refusal say the organizer counts, with plural
forms in every locale.
- Adding or answering an invitation from the mail view, and applying a
proposal, said "could not complete" when the server refused the
event: the import swallowed its SetError. The banner now asks the
import to reject with the refusal and names the participant limit
like the calendar does.
- A range load that finished after a later one could put back its own
events, range or failure. Only the latest load writes the store, and
a load for an account the user left is dropped.
- A search filter with an OR, a NOT or a condition of several keys next
to its date bounds goes out as it is instead of being expanded, which
1.0 refuses.
- A time zone setting typed in another letter case ("europe/berlin")
is used as Intl spells it. 1.0 refused it as a query argument, and
0.16 and 1.0 both dropped it from new events, which were stored
floating.
- A failed save no longer shows the store's English toast next to the
translated message.
- Tests for a limited read that asks later chunks of a request again
after a refused chunk or a full page.
Saving an event in the editor keeps the attendees' answers. When the
user organizes the event and the save changes when it happens, RFC
5546 has the organizer ask the attendees again, and Stalwart's update
to them carries each attendee's stored answer: the old answers would
have read as answers to the new time.
- the editor compares the event's start and end instants, and whether
it is all-day, with its form, to the minute the form shows; writing
the event in the user's time zone instead of its own is no change
- when the user organizes the event, under any of their addresses, and
it moves, every other attendee goes back to needs-action; the
organizer's own entry stays as it is
- every other save keeps the answers, and so does any save of an event
someone else organizes
- the answers are reset in what the save covers: one occurrence, this
and the following ones, or the whole series
- Stalwart 0.16.23 answers `utcStart` and `utcEnd` of a counted
series' master with "32548-12-04T15:30:07Z" once every occurrence is
overridden or excluded, and with the next plain occurrence's times
once the first one is; 1.0 answers the master's own. Expanding such
a series in the browser took the occurrences' length from those
instants and copied them onto the occurrences, so a moved occurrence
fell out of the range. The master's instants now count only where
they fit its wall clock, the length falls back to the JSCalendar
duration, the range check ignores instants that do not parse or fit,
and no occurrence gets the master's instants, so the alerts of an
all-day series expanded in the browser go off before each day, not
before the first.
- The event editor rebuilt the participants around the user's first
address on every save, set `organizerCalendarAddress` to it and put
every attendee back to needs-action. An invitation the user saved in
their own calendar became theirs and lost their answer, and one
occurrence of a series organized under another address or letter
case got a different owner; Stalwart 1.0 refuses a changed
`organizerCalendarAddress` there. The editor now keeps the event's
own organizer, writes `organizerCalendarAddress` only for an event
without one, and keeps the id and answer of every attendee who
stays.
With directory queries off on Stalwart 1.0, the free/busy list under the
attendees disappeared without a word. The share dialog explained every
`forbidden` from Principal/query with the 1.0 text: "only your groups
and people who have shared with you are listed", and a hint at
Stalwart's sharing settings. On 0.16 that refusal only means the role
lacks the permission, Principal/get then lists everyone, and the setting
has no effect over JMAP.
- When the server refuses free/busy for the user (`mayGetAvailability`
false on the user's own principal, or a lookup refused with
`forbidden`), one muted line says "Free/busy isn't available on this
server". The block stays empty only when the server offers no
free/busy at all.
- The share dialog keeps the 1.0 wording for Stalwart 1.0's own refusal
("The administrator has disabled directory queries.") and otherwise
says "Your server doesn't let you browse its user directory.", still
listing whoever Principal/get returned.
- send a calendar's colour only when the user changed it: Stalwart 1.0
accepts only #rgb, #rrggbb or a CSS name, so renaming a calendar that
another client coloured with an 8-digit hex (Apple Calendar) failed
- "Refresh cached data" kept nothing of the calendar store's persisted
state, so it deleted the user's iCal subscriptions, which exist only
there; keep them and drop the rest
- show a notice with a retry when the task list fails to load; it keeps
the tasks it had
- treat a method-level error on creating a task as a failure with its
JMAP type instead of "no id returned"
Stalwart 1.0 enforces maxParticipantsPerEvent (20 by default) on every
CalendarEvent create and update, answers to invitations included, and
refuses anything over it with invalidProperties "An event cannot have
more than 20 participants.". 0.16 advertises the same value but never
enforced it. Creating, editing, moving or answering a bigger meeting
failed with a generic "Failed to save event".
- read the limit from the calendars account capability and warn in the
event editor when the attendees go over it, without blocking the save
- name the limit, and that an administrator can raise it, when the
server refuses a create, edit, move, resize or answer for it, in the
calendar and in the invitation banner
- keep the SetError type on refused event creates and updates
While an event or task uses its calendar's default alerts, Stalwart 1.0
shows those defaults in `alerts` instead of the item's own, and drops
alerts under the default alerts' ids on save. The event editor renamed
every alert to alert1..N and sent them on every save, with the defaults
left on, so on 1.0 an alert change never showed up again, and every
save stored hidden copies of the defaults.
- send `alerts` only when the user changed them; untouched alerts keep
their id and stay exactly as they were
- when the alerts of an item that uses the defaults change, turn the
defaults off for it (`useDefaultAlerts: false`, as the native app
does) and give its alerts ids of their own, in the event and the
task editor
The local reminder engine reads either the calendar's defaults or the
item's own alerts, never both, so the defaults 1.0 fills in do not
fire twice.
Stalwart 1.0 allows 100 Principal/getAvailability calls per account a
minute and counts every call, however they are batched. The event form
sent one request per attendee whenever the time or the attendees
changed, and showed "Available" for an empty answer, which is also what
the server returns when the user may not see that person's free/busy.
- Answers are cached for three minutes per principal and the local
days the event touches, so moving an event within its day asks
nothing new; the status for the exact time is worked out locally.
- Lookups already on their way are shared, and the principals without
an answer go out in one request, split by `maxCallsInRequest`.
- After `rateLimit` nothing is sent for a minute, and attendees without
an answer show "Couldn't check".
- An empty answer shows "No busy times shared" instead of "Available".
0.16 has no rate limit; it gets the same labels with fewer requests.
Stalwart 1.0 refuses Principal/query and Principal/getAvailability with
`forbidden` unless the administrator allows directory queries, which is
off by default. `getPrincipals` turned that into an empty list with no
signal: the share dialog said "No other users or groups found", existing
shares showed bare ids, and free/busy called every attendee "Not a user
on this server".
- `listPrincipals` says whether the listing is complete, `restricted`
or failed. On `forbidden` it falls back to Principal/get without ids,
which 1.0 still answers with the user's own account, their groups and
the accounts that shared something with them.
- Principal/get asks for `id`, `type`, `name`, `description` and
`email` only; 1.0's default list adds `capabilities` and `accounts`.
- One listing per client, kept for five minutes, serves the share
dialog, recipient autocomplete and free/busy, which used to download
the directory three times.
- The share dialog explains a restricted directory while still offering
its candidates, says when the list failed to load, and shows "Unknown
user" with the id for sharees it can't name.
- Free/busy first reads `mayGetAvailability` from the user's own
principal and stays hidden when it is false or a lookup is refused
with `forbidden`. "Not a user on this server" now needs a complete
listing; failures and `rateLimit` show "Couldn't check".
On 0.16 with the default roles the listing is complete and the dialog
behaves as before.
Stalwart 1.0 refuses a CalendarEvent/get that asks for
recurrenceOverrides together with utcStart or utcEnd, and every calendar
read did. The grid, tasks, reminders, search, contact activity and the
agenda plugin came back empty with no error, and creating an event or
task threw after the server had created it, so a retry made a
duplicate.
- read overrides with a second get in the same request, only where
they are needed
- treat an error answer to a get or query as a failure; the calendar
keeps the last events and says it could not load them, with a retry
- never throw after the server created an event or task
- let the server expand series for /api/calendar-agenda