Tick the parity items the fix pass finished (server-side recurrence, filters and vacation for shared accounts, in-app file preview, the trusted-senders toggle, UnifiedPush, sender taps in the viewer), and note on the already ticked ones what this pass changed, such as the tag view and search reaching shared accounts or contact photos as avatars. Items that only moved forward say what is left. Fix hints and "verified" lines that named removed functions (`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`, `buildRecurrenceOverridePatch`) or the old whole-map keyword writes now point at what replaced them. PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are done, 36 open.
50 KiB
Calendar & tasks
Summary
RN has a solid read path (month/week/agenda, shared-calendar namespacing, per-viewer colours, client-side recurrence expansion that is a faithful port of WEB's, birthday calendar, tasks-only calendar hiding, push-driven refresh) but the write path is far behind WEB and has several data-corrupting bugs: the recurrence scope dialog is a stub (every choice edits/deletes the whole series, and editing an occurrence moves the whole series), all-day events grow by one day on every create/edit, events are created without a timeZone, and invitations are never actually sent because RN builds participants with the retired sendTo and never sets organizerCalendarAddress. There is no calendar management UI (create/rename/colour/delete/clear/share), no rights-based editability, no reminders/alerts firing, no trust assessment on invitations, no custom time zone, no Jalali, and the whole calendar UI is unlocalized even though RN has an i18n layer. Roughly half of the WEB changelog fixes since 1.4.5 apply to RN and are not there.
Findings
Recurrence
-
Recurrence scope dialog is a stub: "This event" / "This and following" behave as "All events" —
P1—missing— fixed in161baf1- What WEB does:
handleScopeSelectimplements all three scopes: "this" writes arecurrenceOverrides/<recurrenceId>patch (or the synthetic id on Stalwart >= 0.16.20), "this_and_future" truncates the master withuntiland creates a new series, "all" patches the master; delete does the mirror (exclude / truncate / destroy) and refetches the range (refcomponents/calendar/calendar-app.tsx:842-998,lib/recurrence-overrides.ts:21-32, changelog 1.9.0 "Edit a single recurring occurrence via a one-shot override patch"). - What RN does:
handleScopeSelectignores the chosen scope with a TODO ("treat all scopes as 'all'") and either opens the edit modal on the occurrence or deletesaction.event.id, which the store resolves to the master viaoriginalId(refsrc/screens/CalendarScreen.tsx:347-363,src/stores/calendar-store.ts:306-329). Choosing "This event" on delete destroys the entire series. - Fix hint: port WEB's
handleScopeSelect/truncateRecurrenceAtEvent/findMasterEventinto CalendarScreen; addbuildRecurrenceOverridePatch(WEBlib/recurrence-overrides.ts; removed again inf397ee3, where "This event only" goes toupdateEvent/deleteEventwith the occurrence's id) andRECURRENCE_OVERRIDE_IMMUTABLE_KEYS; for "this" write{ [recurrenceOverrides/${recurrenceId}]: override }onoriginalId; for delete-this write{ excluded: true }; refresh after.
- What WEB does:
-
Editing an expanded occurrence rewrites the master's
startto the occurrence date (series shifts / earlier occurrences vanish) —P1—bug— fixed in161baf1- What WEB does: for scope "all" the updates go to the master but
startfrom an occurrence is never applied to the master unless the user changed it; scope "this" goes into an override (refcomponents/calendar/calendar-app.tsx:930-943). - What RN does:
EventModalseedsstartfrom the occurrence (getEventStartDate(event),src/components/calendar/EventModal.tsx:197) andhandleSavealways sendsstart+duration(EventModal.tsx:262-281);updateEventresolves the occurrence tooriginalIdand PATCHes the master (src/stores/calendar-store.ts:306-318). Any occurrence edit moves the whole series to that occurrence's date. - Fix hint: part of the scope work above; when scope is "all", drop
startfrom the patch unless the user actually changed the date/time relative to the occurrence, or compute the delta and apply it to the master'sstart.
- What WEB does: for scope "all" the updates go to the master but
-
Store's optimistic merge after an occurrence edit only updates one occurrence —
P2—bug— fixed in161baf1- What WEB does: after any occurrence or master mutation with expanded occurrences in view it refetches the visible range (
stores/calendar-store.ts:186-206, 749-751). - What RN does:
updateEventmaps onlye.id === id(src/stores/calendar-store.ts:315-317), so sibling occurrences keep the old title/time until a pull-to-refresh;deleteEventremoves only the tapped occurrence from state while the server destroyed the master (:320-329). - Fix hint: after mutating a
recurrenceId/recurrenceRulesevent callget().refresh()(RN already has it) like WEB'srefetchAfterOccurrenceMutation.
- What WEB does: after any occurrence or master mutation with expanded occurrences in view it refetches the visible range (
-
RSCALE=GREGORIAN;SKIP=OMIT still emitted by the custom recurrence editor (#805) —
P2—bugfix-parity— fixed in58a6f45- What WEB does:
createRecurrenceRuledeliberately omitsrscale/skipbecause Stalwart serialises them into the RRULE and DAVx5 rejects it (reflib/recurrence-rule.ts:5-33, changelog 1.9.2 #805). - What RN does:
buildRuleFromEditorValuesetsrscale: 'gregorian', skip: 'omit'on every custom rule (refsrc/lib/recurrence.ts:220-221). RN affected: any custom recurrence created on mobile breaks Android CalDAV sync. - Fix hint: delete the two properties in
buildRuleFromEditorValue; the simple presets inEventModal.tsx:143-146are already clean.
- What WEB does:
-
Synthetic occurrence ids / server-side expansion (Stalwart >= 0.16.20) not used —
P3—missing— done in8454889(server expansion when the probe finds synthetic ids, device-side expansion otherwise),f397ee3(one occurrence changed or deleted through its own id) and96c5a3f(RSVP to one occurrence)- What WEB does: probes
CalendarEvent/setwithh333333once, then queries withexpandRecurrences: true, hydrates occurrences from base events and writes occurrence patches through the synthetic id with a fallback to base-event overrides (reflib/jmap/client.ts:5648-5680, 5747-5751, 5828-5868,lib/recurrence-instances.ts:35, 73, 208-232,stores/calendar-store.ts:82-184). - What RN does: always fetches raw events and expands on the client (
src/stores/calendar-store.ts:228,src/lib/recurrence-expansion.ts). Functionally fine on all server versions; RN's expansion file is a line-for-line port of WEB's (diffed: only comments, formatting and theisServerRecurrenceInstancepass-through differ). - Fix hint: optional. If adopted, port
recurrence-instances.tswholesale and theisServerRecurrenceInstanceguard inexpandRecurringEvents.
- What WEB does: probes
-
Removing recurrence / participants / reminders / location on edit does not persist —
P2—bug— fixed in58a6f45- What WEB does: sends explicit
nullforrecurrenceRules(+recurrenceOverrides,excludedRecurrenceRules),alerts,participants(+replyTo,organizerCalendarAddress),locations,virtualLocationswhen the field was cleared (refcomponents/calendar/event-modal.tsx:528-549, 560-563, 588-592). - What RN does:
handleSaveemitsundefinedfor cleared fields (recurrenceRules: undefined,participants: undefined,alerts: remindersToAlerts([]) === undefined,locations: undefined) which JSON serialisation drops, so the server keeps the old value (refsrc/components/calendar/EventModal.tsx:262-281,src/lib/calendar-alerts.ts:72-73). Setting "Does not repeat" on a recurring event, removing all attendees, deleting the last reminder or clearing the location silently does nothing. - Fix hint: in edit mode compare against
eventand sendnullfor fields that existed and are now empty; RN'scleanRecurrenceRulesalready turns[]/nullintorecurrenceRule: null(src/api/calendar.ts:115-118), sorecurrenceRules: []works today for recurrence.
- What WEB does: sends explicit
-
this_and_future/ huge-span RRULE (#735) — RN not affected —P3—bugfix-parity— verified, nothing to fix (caps already in place; "this and following" truncates withuntiland dropscount)- What WEB does: memory note on #735 (Stalwart u32 overflow for spans > 136 years). WEB's editor caps
untilto a picked date andcountto 999. - What RN does: same caps (
src/lib/recurrence.ts:227-229,RecurrenceEditor.tsx:139-144, 244-249), no alarms with absolute triggers far in the future. Not affected; noted so it isn't re-investigated.
- What WEB does: memory note on #735 (Stalwart u32 overflow for spans > 136 years). WEB's editor caps
Events: create / edit
-
All-day events are created and saved one day too long —
P1—rn-only-bug— fixed in58a6f45- What WEB does: seeds the end field from
getEventDisplayEndDate(inclusive, end - 1 ms) for all-day events and buildsbuildAllDayDuration(start, inclusiveEnd)(refcomponents/calendar/event-modal.tsx:274-280, 471-477). - What RN does: edit seeds
endwithgetEventEndDate(event)which is the exclusive end (start + P1D = next day 00:00) (src/components/calendar/EventModal.tsx:198); the all-day toggle effect pushesendto the next day wheneverend <= start(:231-242);handleSavethen callsbuildAllDayDuration(start, end)which is inclusive (:269,src/lib/calendar-utils.ts:183-189), yieldingP2Dfor a one-day event. Every create-via-toggle and every re-save of an all-day event adds a day. - Fix hint: seed/display the inclusive end (
getEventDisplayEndDate) for all-day events, and in the all-day effect sete = s(same day) whenend <= start; keepbuildAllDayDurationinclusive.
- What WEB does: seeds the end field from
-
Events created without
timeZone(floating time) —P1—bug— fixed in58a6f45- What WEB does: every timed event carries
timeZone: getEffectiveTimeZone()(all-day:null) so other clients and invitees resolve the wall-clock correctly (refcomponents/calendar/event-modal.tsx:494-502,lib/timezone.ts:64-75). - What RN does:
handleSavenever setstimeZone(src/components/calendar/EventModal.tsx:262-281) andcreateEventdoes not add one (src/api/calendar.ts:248-275). Stalwart stores a floating DTSTART; attendees in other zones, CalDAV clients, and the user after travelling see wrong times; iMIP invites sent for such events are ambiguous. - Fix hint: set
timeZone: allDay ? null : Intl.DateTimeFormat().resolvedOptions().timeZone(reusegetUserTimeZonefromsrc/api/calendar.ts:15); once a custom time-zone setting exists (see below) use that.
- What WEB does: every timed event carries
-
Changing the calendar in the edit modal is ignored (cannot move an event between calendars) —
P2—rn-only-bug— fixed in161baf1+3fd5f10(cross-account moves are not offered)- What WEB does:
calendarIds: { [calendarId]: true }is part of the saved patch and the store remaps store ids to server ids (refcomponents/calendar/event-modal.tsx:503,stores/calendar-store.ts:706-715). - What RN does:
EventModal.handleSavepassescalendarIdas the second argument, butCalendarScreen.handleSaveonly uses it on create (src/screens/CalendarScreen.tsx:365-374); the picker in edit mode is a no-op. - Fix hint: on update include
calendarIds: { [cal.originalId || calendarId]: true }in the patch; for shared calendars the owning account differs, so either block cross-account moves or delete + recreate.
- What WEB does:
-
Edit-modal calendar picker offers the Birthdays calendar, iCal-subscription calendars and read-only shared calendars (#762) —
P2—bugfix-parity— fixed in3fd5f10- What WEB does:
canCreateEventsInexcludes subscription calendars and calendars withoutmayWriteAll/mayWriteOwn(reflib/calendar-editability.ts:20-24, changelog 1.9.0 #762). - What RN does:
EventModalreceiveseventCalendars, which still contains the virtual birthday calendar (src/screens/CalendarScreen.tsx:213-216, 227-231, 544-552) and all shared calendars regardless of rights; picking Birthdays yields a server error, picking a subscription calendar lets the next feed sync delete the event (src/stores/calendar-subscriptions-store.ts:58-74). - Fix hint: filter with a RN
canCreateEventsIn(cal, isSubscriptionCalendar)(subscriptions fromuseCalendarSubscriptionsStore), excludeBIRTHDAY_CALENDAR_ID.
- What WEB does:
-
No rights-first editability (edit/delete offered on read-only and RSVP-only events) —
P2—missing— fixed in3fd5f10- What WEB does:
getEventEditabilitygates on calendarmyRightsfirst (mayWriteAll,mayWriteOwn+ owner,mayRSVP+ participant), subscription calendars are read-only, and the popover shows edit/delete vs. RSVP-only accordingly (reflib/calendar-editability.ts:47-76,components/calendar/event-detail-popover.tsx:161-171, 540-575, changelog 1.9.0 "Rights-first event editability, including alias organizers"). - What RN does:
EventDetailSheetalways renders Edit/Delete (src/components/calendar/EventDetailSheet.tsx:328-351); the only read-only guard is the birthday calendar (src/screens/CalendarScreen.tsx:313-316). The server rejects the write, but the user sees a generic error. - Fix hint: port
calendar-editability.ts; passcalendarsById, the user's addresses (identities + aliases) andisSubscriptionCalendarinto the sheet; hide Edit/Delete forread-only, show only RSVP forrsvp-only.
- What WEB does:
-
myRights.mayWrite(non-existent JMAP property) used for writability everywhere —P2—rn-only-bug— fixed in3fd5f10- What WEB does: uses
mayWriteAll/mayWriteOwn/mayDeletefrom RFC-styleCalendarRights(reflib/calendar-editability.ts:20-24,components/calendar/calendar-sidebar-panel.tsx:328-334). - What RN does:
CalendarRights.mayWriteis documented as a "legacy short flag" (src/api/types.ts:~507) and is whatCalendarSidebarDrawer(:92-98),TasksSheet(:84-87),ICalImportSheet(:51-54) andCalendarInvitationBanner(:96) test. Stalwart never sends it, so every calendar counts as writable and the "Subscribed" section can never populate. - Fix hint: replace with
!r || r.mayWriteAll || r.mayWriteOwnand dropmayWritefrom the type.
- What WEB does: uses
-
No "send invitations" toggle in the editor —
P3—missing— fixed in2abea97- What WEB does:
sendInvitationscheckbox (default on) decidessendSchedulingMessages(refcomponents/calendar/event-modal.tsx:409, 594, 1333-1338). - What RN does: always sends when participants exist (
src/stores/calendar-store.ts:22-24, 293-297). - Fix hint: add a switch in the Participants section and thread it through
onSave.
- What WEB does:
-
Reminder picker is presets-only —
P3—partial— fixed in2abea97(custom amount + unit row; absolute / end-relative / non-display alerts survive a save)- What WEB does: alert rows with a number + unit (minutes/hours/days/weeks/at time), multiple rows, preserving exotic alerts (ref
components/calendar/event-modal.tsx:101-135, 358-386, changelog 1.6.x #170). - What RN does: fixed preset list, multiple allowed, non-offset alerts are dropped from the UI but kept until save (
src/components/calendar/EventModal.tsx:299-312,src/lib/calendar-alerts.ts:51-70). Note the "kept until save" comment is wrong:remindersToAlertsrebuilds the map from presets only, so an absolute-trigger alert is lost on the next save. - Fix hint: add a custom value+unit row; when rebuilding alerts, carry over alerts whose trigger has no
offset.
- What WEB does: alert rows with a number + unit (minutes/hours/days/weeks/at time), multiple rows, preserving exotic alerts (ref
-
Duplicate event, export .ics, copy meeting link / title, add note —
P3—missing— duplicate (+1 day, opens editor), share as .ics (eventToICSport + expo-sharing) and copy meeting link done in 5fe8e40; deferred: copy title, append-a-note- What WEB does: popover/context-menu actions duplicate (+1 day, opens editor), export via
downloadEventICS, copy meeting link, copy title, append a timestamped note to the description (refcomponents/calendar/calendar-app.tsx:1017-1125,lib/calendar-ics-export.ts:96, 176). - What RN does:
EventDetailSheethas anonDuplicateprop thatCalendarScreennever passes (src/components/calendar/EventDetailSheet.tsx:59, 336-342); no export/copy/note. - Fix hint: wire
onDuplicate(clone like WEBhandleDuplicateFromDetail), add "Share .ics" viaexpo-sharing+ a port ofeventToICS, add "Copy link" withClipboard.
- What WEB does: popover/context-menu actions duplicate (+1 day, opens editor), export via
Invitations / scheduling
-
Invitations are never sent: participants built with retired
sendTo, no organizer, noorganizerCalendarAddress—P1—bugfix-parity— fixed in5dc4070- What WEB does:
buildParticipantMapemits an owner-only organizer participant withcalendarAddress, attendees withcalendarAddress+scheduleAgent: 'server', and the modal setsorganizerCalendarAddress: mailto:<user>— without it Stalwart emits no ORGANIZER and silently skips iTIP (reflib/calendar-participants.ts:164-214,components/calendar/event-modal.tsx:573-586, changelog 1.7.0 "Send calendar invites by setting organizerCalendarAddress", 1.7.2 #500 "Use calendarAddress, drop retired sendTo", 1.8.1 #731). - What RN does:
ParticipantInput.addParticipantwrites{ email, sendTo: { imip }, roles: { attendee } }(src/components/calendar/ParticipantInput.tsx:71-92), no organizer participant and noorganizerCalendarAddressanywhere inEventModal.handleSave(:262-281).sendSchedulingMessages: trueis passed (src/stores/calendar-store.ts:293-297) but Stalwart has nothing to schedule with. On Stalwart the attendee'ssendTois stored as an inert JSPROP. - Fix hint: port
buildParticipantMap+collectUserCalendarAddresses; on save with attendees setparticipants = buildParticipantMap({name, email: activeAccount.email}, attendees)andorganizerCalendarAddress = mailto:<email>; on edit keep existing participant ids and never re-add the organizer (#731).
- What WEB does:
-
RSVP matches only the active account e-mail (no identities / aliases) —
P2—missing— fixed in5dc4070- What WEB does:
currentUserEmails= identities + account aliases viacollectUserCalendarAddresses, used forgetUserParticipantId, organizer detection and editability (reflib/calendar-participants.ts:71-112, changelog 1.9.0 "including alias organizers"). - What RN does:
findParticipantByEmail(event, [activeEmail])with the single login address (src/components/calendar/EventDetailSheet.tsx:115, 173,src/components/email/CalendarInvitationBanner.tsx:98). Invitations addressed to an alias show no RSVP buttons. - Fix hint: RN already loads identities for the composer; collect their emails (+ aliases from
x:Account/getif available) and pass the array.
- What WEB does:
-
RSVP writes
replyToinstead of repairingorganizerCalendarAddress—P3—partial— fixed in5dc4070- What WEB does: only when the event lacks
organizerCalendarAddressit sets it fromreplyTo.imip; never sendsreplyTo(retired) (refstores/calendar-store.ts:782-789). - What RN does:
rsvpEventsetspatch.replyTo = replyTowheneverbuildReplyToreturns something (src/api/calendar.ts:344,src/lib/calendar-invitation.ts:74-83). Harmless on Stalwart (ignored) but the repair path for imported invites lacking an organizer is missing, so those REPLYs are not routed. - Fix hint: mirror WEB:
if (replyTo?.imip && !storeEvent.organizerCalendarAddress) patch.organizerCalendarAddress = replyTo.imip.
- What WEB does: only when the event lacks
-
Store rejects participant ids containing
..—P3—rn-only-bug— fixed in5dc4070- What WEB does: participant ids are opaque and RFC 6901-escaped; only empty ids are rejected (ref
stores/calendar-store.ts:763-770). - What RN does:
if (!participantId || participantId.includes('..')) throw(src/stores/calendar-store.ts:332-334); a server-generated id likea..bcannot RSVP. - Fix hint: drop the
..check.
- What WEB does: participant ids are opaque and RFC 6901-escaped; only empty ids are rejected (ref
-
Invitation banner: no trust assessment, no METHOD from Content-Type / raw ICS, no inline body-part detection —
P2—partial— fixed in8206893(auth results parsed fromemail.headers)- What WEB does:
getInvitationTrustAssessment(DMARC/DKIM/SPF + sender/organizer mismatch -> trusted/caution/warning banner),getInvitationMethodreadsmethod=from the attachment/body Content-Type, thenextractMethodFromRawIcs, then infers;findCalendarAttachmentalso walkstextBody/htmlBodysub-parts (reflib/calendar-invitation.ts:341-446, changelog 1.6.x "RSVP with trust assessment"). - What RN does:
inferInvitationMethod(parsed)only (src/components/email/CalendarInvitationBanner.tsx:62;extractMethodFromRawIcsexists atsrc/lib/calendar-invitation.ts:103-106but is unused);findCalendarAttachmentchecksemail.attachmentsonly (:139-148); no trust UI. A spoofed invitation from an unauthenticated sender looks identical to a real one, and a REPLY/CANCEL whose ICS carries no participants is treated asunknownand offered "Add to calendar". - Fix hint: port
getInvitationTrustAssessment(RNEmailhasauthenticationResults?; if not, add to the fetched properties) and show a coloured banner + reason; readattachment.typeparams and fall back to downloading the blob text forMETHOD:.
- What WEB does:
-
Invitation banner imports into the first writable calendar, not the default calendar, and parses against the primary account —
P3—partial— fixed in3fd5f10+8206893- What WEB does: imports into the default calendar of the account that owns the email; shared-folder emails are parsed against the folder owner (#867) and multi-account lookups are routed by source account (#847) (changelog 1.9.0).
- What RN does:
calendars.find(cal => !cal.myRights || cal.myRights.mayWrite !== false)(src/components/email/CalendarInvitationBanner.tsx:96) andparseCalendarBlob(attachment.blobId)onjmapClient.accountId(:59,src/api/calendar.ts:355-372). RN shows only the active account's mailboxes, so #847/#867 are largely N/A; the "first writable" pick is wrong when a subscription calendar sorts first. - Fix hint: prefer
cal.isDefault && !cal.isShared, exclude subscription/birthday calendars; passemail.accountId(if RN exposes shared mailboxes later) toparseCalendarBlob.
-
Organizer not identified in the participant list; no "invited by", status counts or cancelled/tentative rendering —
P3—partial— fixed in5dc4070+0f80e98(organizer label, cancelled strike-through in sheet/card/month chips, tentative badge on cards; week-view blocks are untouched)- What WEB does:
getParticipantListmarks the organizer (also viaorganizerCalendarAddress, #731), the popover shows "(organizer)", strikes throughstatus === 'cancelled'and badgestentative(reflib/calendar-participants.ts:126-157,components/calendar/event-detail-popover.tsx:299-312, 445-449, changelog 1.7.2 #572). - What RN does: participants listed with status dots only (
src/components/calendar/EventDetailSheet.tsx:298-325); no cancelled/tentative styling in sheet,EventCardor views. - Fix hint: port
getParticipantList/getStatusCounts; addline-throughwhenevent.status === 'cancelled'inEventCard,WeekView,EventDetailSheet.
- What WEB does:
Calendars
-
No calendar management UI: create, rename, colour, description, delete, clear events, copy CalDAV URL —
P2—missing— fixed indae5700(no CalDAV URL copy: RN has no DAV base URL)- What WEB does: sidebar context menu + settings section with create (with kind picker), edit name/colour/description, clear all events (unlink-aware), delete, share, copy URL (ref
components/calendar/calendar-sidebar-panel.tsx:328-395,components/settings/calendar-management-settings.tsx,stores/calendar-store.ts:1028-1206). - What RN does:
createCalendarexists in the store but is only reachable through the subscription sheet (src/stores/calendar-store.ts:378-382,src/stores/calendar-subscriptions-store.ts:82-84); there is noupdateCalendar/removeCalendar/clearCalendarEventsAPI or UI (src/api/calendar.tshasdeleteCalendarat 417 but nothing calls it for own calendars). Long-press only offers set-default and shared-calendar recolour (src/components/calendar/CalendarSidebarDrawer.tsx:208-211). - Fix hint: add
Calendar/setupdate + destroy (onDestroyRemoveEvents) tosrc/api/calendar.ts, expose in the store, and add "New calendar" + long-press "Rename / Colour / Delete / Clear" in the drawer. Deleting must be blocked forisDefaultand shared calendars like WEB.
- What WEB does: sidebar context menu + settings section with create (with kind picker), edit name/colour/description, clear all events (unlink-aware), delete, share, copy URL (ref
-
Own-calendar colour cannot be changed (only shared calendars are recolourable) —
P3—partial— fixed indae5700- What WEB does: colour picker writes
Calendar/set { color }for own calendars and a local override for shared ones (refcalendar-sidebar-panel.tsx:330, 366,lib/shared-calendar-colors.ts:33-40). - What RN does:
canRecolor = !!onSetColor && !!cal.isShared(CalendarSidebarDrawer.tsx:209). - Fix hint: for
!cal.isSharedcall the newupdateCalendar(id, { color }).
- What WEB does: colour picker writes
-
Shared-calendar colour key differs from WEB (not portable, but internally consistent) —
P3—partial— fixed in0f80e98- What WEB does: key =
${localAccountId}|${accountId}|${originalId}(reflib/shared-calendar-colors.ts:33-40). - What RN does: key =
${accountId}|${id}whereidis already the namespaced${accountId}:${raw}(src/lib/calendar-utils.ts:474-478). Works, but the account id is duplicated in the key and would break if the namespacing format changes. - Fix hint: use
cal.originalId ?? cal.idlike WEB.
- What WEB does: key =
-
Calendar creation does not pin
supported-calendar-component-set(#760) —P3—bugfix-parity— deferred: the drawer now creates calendars overCalendar/set(dae5700); MKCALENDAR against Stalwart's/dav/endpoint needs a CalDAV client + calendar-home discovery that RN does not have- What WEB does: MKCALENDAR through the WebDAV proxy with
VEVENT/VTODO/both, then finishes over JMAP; falls back toCalendar/set(reflib/jmap/client.ts:5366-5400, 5436-5485,components/calendar/calendar-kind-picker.tsx:16-18, changelog 1.9.0 #760). - What RN does: plain
Calendar/set(src/api/calendar.ts:374-396). Only matters once RN gains a create-calendar UI; RN has no WebDAV proxy, but it can talk to Stalwart's CalDAV endpoint directly (the same host serves/dav/). - Fix hint: when adding calendar creation, issue
MKCALENDARagainst the account's calendar-home withsupported-calendar-component-set, thenCalendar/getto find the new id.
- What WEB does: MKCALENDAR through the WebDAV proxy with
-
No first-touch gate: duplicate default calendars on clustered Stalwart (#907) —
P2—bugfix-parity— fixed in58e3ef6(in-flight dedupe of fetchCalendars/fetchTasks; ensureRange awaits it)- What WEB does:
FirstTouchGateserialises the firstCalendar/*/CalendarEvent/*request per account so concurrent first-touch requests cannot each create a default calendar (reflib/jmap/first-touch-gate.ts:56-101,lib/jmap/client.ts:678, 1129, changelog 1.9.0 #907). - What RN does:
CalendarScreenfiresfetchCalendars()on mount and, in a second effect,ensureRange()which seescalendars.length === 0and callsfetchCalendars()again thenCalendarEvent/query(src/screens/CalendarScreen.tsx:261-269,src/stores/calendar-store.ts:235-251);auth-storealso kicksfetchCalendars/refreshafter login (src/stores/auth-store.ts:92-98). Two to three concurrent first-touch calls — exactly the #907 race. RN affected. - Fix hint: port
FirstTouchGateintosrc/api/jmap-client.tsrequest(), or at minimum dedupe in-flightfetchCalendarsand makeensureRangeawait the mount fetch.
- What WEB does:
-
Tasks-only calendar detection misses CalDAV tasks without
@type—P2—bugfix-parity— fixed in58e3ef6- What WEB does:
isTaskLikeObjecttreats@type: TaskOR presence ofdue/progress/percentComplete(and@type !== 'Event') as a task, both for the tasks list and for hiding tasks-only calendars (reflib/calendar-component-detection.ts:30-58,lib/jmap/client.ts:6300-6316, changelog 1.5.x #84 "Detect tasks created by external CalDAV clients such as Thunderbird"). - What RN does:
fetchEventsclassifies solely on'@type' === 'Task'(src/stores/calendar-store.ts:208-227); Thunderbird/Todoist VTODOs that Stalwart returns without@typeare neither shown as tasks nor counted fortaskOnlyCalendarIds(they fall through as start-less "events" and are dropped by!!e.start). - Fix hint: port
isTaskLikeObjectand use it in both filters.
- What WEB does:
-
Shared accounts without calendar access are re-probed on every fetch —
P3—bugfix-parity— fixed in58e3ef6- What WEB does: remembers
forbidden/accountNotFoundper account incalendarAccessDeniedand skips it (reflib/jmap/client.ts:5806-5813, changelog 1.8.1 "Stop re-probing shared accounts that have no calendar access"). - What RN does:
getCalendarsandfetchEventstry every calendar-capable session account each time and swallow errors (src/api/calendar.ts:140-186,src/stores/calendar-store.ts:194-204). - Fix hint: keep a module-level
Setof denied account ids, reset on reconnect.
- What WEB does: remembers
-
Calendar sharing (JMAP
shareWith) not available for calendars —P2—missing— fixed indae5700- What WEB does:
shareCalendarviaCalendar/set shareWith/<principal>with a principal picker and share indicators (refstores/calendar-store.ts:1078-1100,lib/jmap/client.ts:4921, changelog 1.6.x "JMAP sharing for calendars and address books", #244, #257). - What RN does: sharing exists only for Files (
src/api/files.ts:229-241,src/components/files/ShareSheet.tsx); nothing for calendars (also likely nothing for address books — that belongs to the contacts audit). - Fix hint: reuse
ShareSheet+ the principal picker with aCalendar/setshareWith/{principalId}patch; requestshareWith/myRightsinCalendar/getproperties.
- What WEB does:
Views / navigation / locale
-
Entire calendar UI is hard-coded English although RN has i18n —
P2—missing— partial in0f80e98: screen, month/week/agenda views, event card/sheet/modal, drawer, scope dialog, settings, banner and calendar/share sheets uset()+ date-fns locales; TasksSheet (09c24fa),formatReminder(2abea97) and ICalImportSheet (b7f7110) localized too; deferred: ICalSubscriptionSheet and RecurrenceEditor still hard-code English- What WEB does: everything through
next-intl(t('calendar.*')), month/day names and popover dates localized (changelog 1.6.x "Localize event start date in detail popover and event modal"). - What RN does:
src/i18n/index.ts+useLocaleStoreare used by 18 other files, but none ofsrc/components/calendar/*,src/screens/CalendarScreen.tsxorCalendarSettings.tsximport it; all labels ("Today", "No events", "Does not repeat", "Going?", RSVP labels, drawer titles, etc.) anddate-fnsformat()calls without alocaleare English (CalendarScreen.tsx:408, 448, 504, 640-641,EventModal.tsx:60-67, 326, ...,MonthView.tsx:25-26,AgendaView.tsx:33-37). - Fix hint: add a
calendar.*namespace tolocales/*.json(WEB's keys can be copied) and pass a date-fns locale from the locale store intoformat().
- What WEB does: everything through
-
First day of week limited to Monday/Sunday; no Saturday, no Jalali (#490) —
P3—missing— Saturday (0|1|6) done in 0f80e98; deferred: Jalali month grid needs a port oflib/jalali-utils.ts+useCalendarLocale- What WEB does:
firstDayOfWeek0/1/6 with Saturday default for Persian and a full Jalali month grid / headers (refhooks/use-calendar-locale.ts:35-120,lib/jalali-utils.ts:161-169, changelog 1.8.0 #490). - What RN does:
FirstDayOfWeek = 0 | 1(src/stores/settings-store.ts:76),MonthView/WeekViewtyped0 | 1(MonthView.tsx:34,WeekView.tsx:40), no Jalali. - Fix hint: widen the type to
0|1|6, add Saturday toCalendarSettings; Jalali is a larger port ofjalali-utils.ts+ auseCalendarLocaleequivalent (only if the fa locale is added to RN).
- What WEB does:
-
Week numbers are not ISO when the week starts on Monday; no week numbers in week view —
P3—rn-only-bug— fixed in0f80e98(month view; the week view header has no room for it)- What WEB does:
getISOWeekwhenweekStartsOn === 1, elsegetWeek(..., { weekStartsOn: 0 })(refcomponents/calendar/mini-calendar.tsx:74-79). - What RN does:
getWeek(row[0], { weekStartsOn })(src/components/calendar/MonthView.tsx:137) — withoutfirstWeekContainsDate: 4this is not ISO numbering and can be off by one around New Year. WeekView has no week number. - Fix hint: use
getISOWeekfor Monday start; optionally show the number in the WeekView header row.
- What WEB does:
-
"Show time in month view" setting is ignored (#666) —
P3—bugfix-parity— fixed in0f80e98- What WEB does: on mobile,
showChips = !isMobile || showTimeInMonthViewrenders event chips with times instead of dots (refcomponents/calendar/calendar-month-view.tsx:49-52,event-card.tsx:80, 178, changelog 1.8.0 #666). - What RN does: setting exists (
src/stores/settings-store.ts:169,CalendarSettings.tsx:435-443) butMonthViewalways draws up to 3 dots (src/components/calendar/MonthView.tsx:83-86). RN affected. - Fix hint: pass the setting into
MonthViewand render a compact chip (title + start time) per event when on.
- What WEB does: on mobile,
-
Hover-preview setting exposed on mobile —
P3—rn-only-bug— fixed in0f80e98(store key kept for settings-sync compatibility)- What WEB does: hover preview is a desktop pointer feature.
- What RN does:
CalendarSettingsshows "Hover preview" with delay options and nothing consumescalendarHoverPreview(src/components/settings/CalendarSettings.tsx:87-102; no other reference insrc/). - Fix hint: remove the setting row (keep the store key for settings-sync compatibility or drop it).
-
No day view (falls back to agenda); tapping a month day does not open the day —
P3—partial— fixed in0f80e98("Day" hidden from the mobile default-view options; a synced "day" shows as Agenda); a real day view is back sincede3d697, with the week and month views scrolling freely (de3d697,caf61ec, #759) and an endless agenda (71522b9)- What WEB does: day view with time grid; on mobile, tapping a date in month view switches to day view and offers "back to month" (ref
components/calendar/calendar-app.tsx:461-470,calendar-day-view.tsx). - What RN does:
calendarDefaultView === 'day'maps to agenda (src/screens/CalendarScreen.tsx:131-137); month view shows a list below the grid instead. Acceptable UX, but the settings option "Day" is misleading. - Fix hint: either reuse
WeekViewwith a single-dayweekDaysarray as a day grid, or hide "Day" from the RN default-view options.
- What WEB does: day view with time grid; on mobile, tapping a date in month view switches to day view and offers "back to month" (ref
-
Week view: event block time label ignores 12h setting; timed grid has no long-press-at-minute precision —
P3—partial— fixed in0f80e98- What WEB does:
formatSnapTime(minutes, timeFormat)(reflib/calendar-utils.ts:278). - What RN does:
minutesToTimeLabelalwaysHH:mm(src/components/calendar/WeekView.tsx:315-319) while the gutter honours 12h (:229-231). Long-press creates at the whole hour (:123-128), fine for mobile. - Fix hint: use
timePattern(timeFormat)withformat().
- What WEB does:
-
EventCard shows the description under a location (MapPin) icon and never shows the location —
P3—rn-only-bug— fixed in0f80e98- What WEB does: card shows time, location and calendar (ref
components/calendar/event-card.tsx). - What RN does:
event.descriptionrendered with<MapPin/>(src/components/calendar/EventCard.tsx:65-72);locationsis not read. - Fix hint: show
Object.values(event.locations)[0]?.namewith MapPin; drop or move the description.
- What WEB does: card shows time, location and calendar (ref
-
Detail sheet shows exclusive end date for all-day events (#318 off-by-one) —
P2—rn-only-bug— fixed in5dc4070- What WEB does: multi-day all-day events show the inclusive end date (changelog 1.6.x #318,
getEventDisplayEndDate). - What RN does:
formatRangeuseseventTimeRange->getEventEndDate(exclusive), so a single-day all-day event on Mar 1 prints "Mar 1 – Mar 2" and a two-day one "Mar 1 – Mar 3" (src/components/calendar/EventDetailSheet.tsx:63-74,src/lib/calendar-utils.ts:84-90). - Fix hint: for
allDayusegetEventDisplayEndDate(event)before comparing/formatting.
- What WEB does: multi-day all-day events show the inclusive end date (changelog 1.6.x #318,
-
Deep links to calendar/event —
P3—missing— partly: incoming links are parsed since5802cae(src/navigation/linking.ts), and an event link opens the event since 9282ee6; a date link only opens the Calendar tab- What WEB does:
parseCalendarPath/buildCalendarPathhandle/calendar/<view>/<date>?event=(reflib/deep-links.ts:272-300, changelog 1.8.3 "Deep links for mail, calendar, contacts, files"). - What RN does:
Calendar: undefinedroute params (src/navigation/types.ts:33);handleDeepLinkhands an event to the Calendar tab throughpending-calendar-open, the same path a tapped reminder takes, but ignores the date. - Fix hint: accept
{ date?, eventId? }params on the Calendar route and open the detail sheet.
- What WEB does:
Data loading / sync
-
Range queries fetch every event in the account (no
after/before), capped at 1000 —P2—bug— fixed in58e3ef6- What WEB does:
CalendarEvent/querywith{ after, before }+timeZone(changelog 1.7.0 "timezone-aware calendar queries", 1.7.1 "Stalwart-compatible calendar filters") (reflib/jmap/client.ts:5722-5745). - What RN does:
queryEventsignores its_after/_beforearguments citing "Stalwart rejects after/before filters" (outdated) and useslimit: 1000(src/api/calendar.ts:188-208);fetchEventsthen re-fetches everything wheneverensureRangewidens the union (src/stores/calendar-store.ts:235-251). Accounts with more than 1000 objects silently lose events; every navigation past the loaded range re-downloads all events. - Fix hint: send
filter: { after, before, inCalendar... }as WEB does ({ operator: 'AND', conditions: [...] }when combined), keeptimeZone; thenloadedRangecan be the requested window rather than a growing union.
- What WEB does:
-
iCal import passes raw parsed objects to
CalendarEvent/set; existing UIDs are skipped instead of linked (#113) —P2—partial— fixed in5fe8e40- What WEB does: whitelists the JSCalendar properties it sends (drops server-computed
utcStart/utcEnd/isOrigin/created/updated, rewrites participants tocalendarAddress, normalises all-day duration/timeZone) and links UIDs that exist in another calendar by adding the target calendar tocalendarIds(refstores/calendar-store.ts:822-958, changelog 1.5.x #113). - What RN does:
importEventsskips existing UIDs and batch-creates{ ...parsed, calendarIds }untouched (src/stores/calendar-store.ts:352-376,src/api/calendar.ts:278-297). Whether Stalwart rejects computed properties on create depends on the parse output; WEB chose to whitelist after hitting failures ("Deduplicate UIDs during iCal import to prevent mass failures"). - Fix hint: port WEB's
preparedmapping (stores/calendar-store.ts:885-957) and the link-by-calendarIdsbranch.
- What WEB does: whitelists the JSCalendar properties it sends (drops server-computed
-
iCal subscriptions: no per-account scoping, no edit, no auto-refresh/interval, no rollback,
webcals://, basic-auth URLs —P2—partial— fixed indae5700- What WEB does: subscriptions carry
accountIdand are skipped for other accounts (stores/calendar-store.ts:1360-1363, 1468-1472), editable name/colour/url/interval (:1294-1329), refreshed every 5 min per interval (components/calendar/calendar-app.tsx:320-324), rolled back when the first fetch fails (:1274-1289),webcals?://normalised (:1240), basic auth in the URL supported server-side (#275), stale events unlinked rather than deleted when also in another calendar (:1401-1427). - What RN does:
calendar-subscriptions-storepersists{ id, name, url, color, calendarId }globally (src/stores/calendar-subscriptions-store.ts:16-24, 162-166) — after switching accounts the list still shows andsyncSubscriptionimports into a calendar id of the other account (or a colliding raw id);syncAllis never called (:155-160); no edit UI (ICalSubscriptionSheet.tsx); a failed first sync leaves the empty calendar + sub withlastError(:82-116); onlywebcal://is rewritten (:38-40);fetch('https://user:pass@host')throws in RN's WHATWG fetch so #275 URLs fail; stale events are deleted outright (:58-74). - Fix hint: add
accountId: jmapClient.accountIdto each sub and filter by it; callsyncAllon calendar mount +AppStateforeground with a per-sub interval; add an edit sheet; on first-sync failure delete the calendar; regex/^webcals?:\/\//i; parse credentials out of the URL into anAuthorizationheader.
- What WEB does: subscriptions carry
-
Subscription feeds are fetched on-device (security note, not a gap) —
P3—partial— fixed indae5700(10 MB cap)- What WEB does:
/api/fetch-icalserver-side with an SSRF guard, DNS pinning and redirect validation (GHSA-24w9, changelog 1.9.2). - What RN does:
fetch(url)from the phone (src/stores/calendar-subscriptions-store.ts:42-53). No server is exposed, so the SSRF class does not apply; the device can be pointed at LAN hosts, which is the user's own network. Redirects are followed byfetchwithout validation (fine). N/A for the advisory; worth a 10 MB size cap and atext/calendar-ish content check (already checksBEGIN:VCALENDAR). - Fix hint: optional size cap.
- What WEB does:
-
Created recurring event appears as a single instance until refresh; created event lacks
utcStart/utcEnd—P3—partial— fixed in58e3ef6- What WEB does:
createCalendarEventrefetches the created event with full properties (lib/jmap/client.ts:5942-5960) and the store refetches when occurrences are in view. - What RN does:
createEventmerges the/setecho over the payload (src/api/calendar.ts:272-274) and appends without expansion (src/stores/calendar-store.ts:286-304). - Fix hint: after create,
CalendarEvent/getthe id (RNgetEvents([id])) and, ifrecurrenceRulesis set, runexpandRecurringEventsforloadedRangeor callrefresh().
- What WEB does:
-
Push: FCM subscription excludes
CalendarEvent/Calendartypes —P3—partial— deferred: local reminders (8e1d03c) are resynced at launch, onCalendar/CalendarEventstate changes and on return to the foreground (4b903ed), but nothing refreshes them in the background; adding the calendar types to the push subscription lives insrc/lib/push-notifications.ts(push area) and would need a background refresh + reschedule hook- What WEB does: push subscription covers calendar types so the sidebar refreshes on external changes (FEATURES.md "JMAP push keeps everything in sync").
- What RN does: foreground EventSource/polling dispatches to
useCalendarStore.handleStateChange(App.tsx:390-397,src/stores/calendar-store.ts:261-284) — at parity while the app is open; the background push subscription carries only['EmailDelivery']since00faceb(src/lib/push-notifications.ts), so calendar changes never wake the app. - Fix hint: leave as is unless background calendar refresh becomes needed.
Alerts / notifications
- Reminders never fire on mobile (no alert scheduler, no local notifications) —
P2—missing— fixed in8e1d03c(expo-notifications local notifications scheduled from the store for the next 7 days, calendar default alerts honoured, cancelled events / completed tasks / acknowledged alerts skipped); moving the calendar view cancelled them until4b903ed(audit B17), and a tapped reminder opens its event since83db70a- What WEB does:
useCalendarAlertspolls every 60 s, proactively fetches the next 24 h, computes fire times fromalerts(offset relative to start/end, absolute triggers, calendar default alerts viauseDefaultAlerts), dedupes through a persisted acknowledged store, plays a sound and toasts; task due alerts too; cancelled events muted (#572) (reflib/calendar-alerts.ts:39-201,hooks/use-calendar-alerts.ts:21-158,stores/calendar-notification-store.ts). - What RN does:
src/lib/calendar-alerts.tsonly converts offsets <-> reminder presets (1-103); thecalendarNotificationsEnabled/calendarNotificationSoundsettings are shown inNotificationSettings.tsx:52-53, 235-240but nothing reads them;package.jsonhas noexpo-notifications. Users who set "15 minutes before" get nothing. - Fix hint: add
expo-notifications, portcomputeFireTime/getPendingAlerts, and schedule local notifications for the next N days on everyfetchEvents(cancel + reschedule by event id); honouruseDefaultAlertsviaCalendar.defaultAlertsWithTime/WithoutTime(add those toCalendar/getproperties).
- What WEB does:
Tasks
- Task sheet supports only title + due date; no edit, description, priority, due time, progress states, alerts, filters, "show on calendar" —
P2—partial— fixed in09c24fa(edit, description, priority, due date + time, calendar move, all/pending/completed/overdue filters, overdue-first sort,needs-action<->completedwithprogressUpdated, due tasks overlaid on the grid; task alerts still come only from the server data); completing failed on Stalwart, which rejectsprogressUpdated, untildcdda5e(audit B12, #958)- What WEB does: task modal with description, due date + optional time, priority (none/high/medium/low -> 1-9), progress, calendar, alert; list filters all/pending/completed/overdue, overdue-first sort,
showTasksOnCalendaroverlays due tasks on the grid; completion togglesneeds-action<->completedwithprogressUpdated(refcomponents/calendar/task-modal.tsx:55-140,task-list-view.tsx:80-110,stores/task-store.ts:366, 442-452). - What RN does:
TasksSheetcreates{ title, progress, due }only, no tap-to-edit, delete + toggle only (src/components/calendar/TasksSheet.tsx:117-135, 210-235); un-completing setsin-process(src/stores/calendar-store.ts:418-426);showTasksOnCalendar(settings-store.ts:174) is unused; tasks are never drawn in month/week/agenda. - Fix hint: add a task edit sheet mirroring
task-modal.tsx, filter chips, and render due tasks as all-day chips whenshowTasksOnCalendar.
- What WEB does: task modal with description, due date + optional time, priority (none/high/medium/low -> 1-9), progress, calendar, alert; list filters all/pending/completed/overdue, overdue-first sort,
Settings
- No custom time-zone setting (#755); no birthday-calendar colour; no "Day" view on mobile (see above) —
P3—missing— time-zone setting (calendarTimeZone, used for saved events and JMAP queries) done in58a6f45+ 0f80e98; the editors and views work in calendar-zone wall time sincea8e6f6eand8af163a, and floating task dues since 1a651a9; deferred: birthday-calendar colour setting- What WEB does:
timeZone: 'auto' | IANAoverriding browser detection, used for display (displayNow,toDisplayDate) and for thetimeZoneon saved events (reflib/timezone.ts:29-75,stores/settings-store.ts:317, 553, changelog 1.9.2 #755);birthdayCalendarColor(settings-store.ts:377, 609). - What RN does: device zone only (
src/api/calendar.ts:15-21); birthday colour fixed (src/lib/birthday-calendar.ts:7). - Fix hint: add
timeZoneto the settings store (synced with WEB's key so it round-trips through settings sync), pass it as the JMAPtimeZonearg and into saved events; convert display viaIntllike WEB'sgetWallClock.
- What WEB does:
Verified at parity (brief list, so the fixer knows what NOT to redo)
- Client-side recurrence expansion:
src/lib/recurrence-expansion.tsis a faithful port oflib/recurrence-expansion.ts(byX filtering, bySetPosition, RDATE overrides, fast-forward, per-occurrenceutcStart/utcEnd#116); only the server-instance guard differs. - Stalwart singular
recurrenceRule/excludedRecurrenceRulenormalisation on read and write (#13, JSCalendar 2.0) —src/api/calendar.ts:64-126. - Singular
inCalendarOR-filter,timeZoneon query/get, batchedCalendarEvent/get(#141) —src/api/calendar.ts:30-38, 210-231. - Shared calendars via other session accounts, namespaced ids (
${accountId}:${id}),originalId, event remapping, per-account query routing, best-effort failure isolation —src/api/calendar.ts:140-186,src/stores/calendar-store.ts:35-58, 170-204. - Per-viewer colours for shared calendars with auto-assigned unused palette colour (#345) —
src/screens/CalendarScreen.tsx:181-211,src/lib/calendar-utils.ts:465-511. - Tasks-only calendars hidden from event UI and pickers (#28 / 9c084d9d) —
CalendarScreen.tsx:227-242. - Set default calendar via
onSuccessSetIsDefault—src/api/calendar.ts:403-413. - Malformed
utcStart/startfallback (#316) —src/lib/calendar-utils.ts:35-56; undefined alert trigger guard (#143) —src/lib/calendar-alerts.ts:57-60. - Multi-day all-day bars aligned across the week, timed full-day promotion, cluster-based overlap layout —
src/lib/calendar-utils.ts:240-420,WeekView.tsx:69-101. - Start move keeps duration (
EventModal.tsx:244-248), Save disabled until title, long-press slot/day creates at the tapped time (7e9aeaf0 equivalent). - Server-sent iMIP via
sendSchedulingMessagesonly (no client-side duplicate emails); import uses no scheduling (#411) —src/api/calendar.ts:233-297. - RSVP via RFC 6901-escaped
participants/<id>/participationStatus—src/api/calendar.ts:331-352. - Birthday calendar generation incl. Feb 29 clamp and age — identical to WEB (
diffclean apart from the extra WEB null fields). - iCal import with preview, select all/none, 5 MB cap, UID dedupe —
ICalImportSheet.tsx. - Push StateChange refresh for
Calendar/CalendarEventon primary + shared accounts —App.tsx:390-397,src/stores/calendar-store.ts:261-284. - Calendar tab hidden when the account lacks the JMAP calendars capability —
App.tsx:95, 160,src/lib/capabilities.ts:25. - Week start setting, week numbers in month view, 12h/24h time format threaded through views.
N/A on mobile
- Mouse drag to move/resize, click-drag create, 15-minute snapping, top-edge resize, hover preview (
hooks/use-time-grid-interactions.ts) — long-press create is the mobile equivalent. - Keyboard shortcuts (t/m/w/d/a/k/n, arrows), right-click context menus (long-press could carry the same actions, see "Duplicate/export" finding).
- Pro multi-account aggregation (
hooks/use-pro-multi-account-calendars.ts,localAccountId::namespacing,reconcileSelectedIds) — RN is single-active-account. /api/fetch-icalserver-side SSRF guard / DNS pinning (GHSA-24w9) — no server component in RN.webcal:/mailto:protocol-handler registration and import-or-subscribe prompt for detected webcal links.- Agenda plugin sidecar, plugin event-action slot (Jitsi), Pro tab deep-link URL writing.
- Refresh-gesture interception (F5/Ctrl-R) — RN has pull-to-refresh.
- Quick natural-language event input: the webmail has no parser for it either (dropped from the findings by the 2026-09 audit).