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>
11 KiB
React Native Calendar — Parity Plan
Bring repos/react-native/ calendar to parity with the webmail calendar at
components/calendar/. Webmail totals ~5,940 lines across 19 components plus
utility libs; mobile port targets ~2,500 lines with feature parity on the
essentials and explicit deferrals for things that don't fit a phone UX.
Current state
src/screens/CalendarScreenNew.tsx— month grid + agenda-for-selected-day, all renderingMOCK_EVENTS. Doesn't touch the store.src/stores/calendar-store.ts— CRUD wiring exists (fetchCalendars,fetchEvents,createEvent,updateEvent,deleteEvent) but nohandleStateChange, no per-calendar visibility, no range tracking.src/api/calendar.ts— JMAP methods wired (Calendar/get,CalendarEvent/query,CalendarEvent/get,CalendarEvent/set).src/api/types.ts—CalendarEventandCalendartypes match webmail.- No event modal, no detail view, no week/day view, no sidebar, no recurrence.
In scope
Phase 1 — Data layer (foundation for everything else)
src/stores/calendar-store.ts — expand to match webmail's store semantics:
hiddenCalendarIds: Set<string>persisted via AsyncStorage (matches webmail'sCalendarSidebartoggle state).loadedRange: { after: string; before: string } | null— track what's fetched so view changes don't re-fetch unnecessarily.ensureRange(after, before)— fetch only if the requested window isn't covered byloadedRange; expand the loaded range in place instead of replacing events wholesale, so switching Month → Week doesn't drop data.handleStateChange(change)— subscribe toCalendarandCalendarEventtypeState keys; on push, refetch calendars and re-query the current window. Already referenced fromApp.tsxpush handler — this finally implements it.toggleCalendarVisibility(id)/setCalendarHidden(id, hidden).- Selectors:
visibleCalendars,visibleEvents(filtered by hidden set).
src/lib/calendar-utils.ts (new) — port from webmail's lib/calendar-utils.ts:
parseLocalDateTime(iso)— JSCalendar start strings are floating local time (2026-04-20T09:00:00, no Z). Parse without timezone shifts.parseDuration(iso8601)—PT1H30M→ milliseconds.eventTimeRange(event)— returns{ start: Date, end: Date, allDay: boolean }handlingshowWithoutTime.eventsOnDay(events, day)— day overlap check (not just start-matches-day).getCalendarColor(cal)— fallback to a deterministic palette whencoloris missing, matchingtheme/tokens.tscolors.calendar.*.
src/lib/recurrence-expansion.ts (new) — minimal port of webmail's RRULE
expander. Webmail's version supports byDay/byMonth/byMonthDay/count/until.
Mobile port: same surface, same tests. This is the hard part — allocate ~400
lines. Drives correct rendering of recurring events across the visible window.
Phase 2 — Views
src/screens/CalendarScreenNew.tsx — replace mocks, gain view switching:
- On mount:
fetchCalendars(), thenensureRangefor visible window. - View mode toggle → Month / Week / Agenda (Day view folded into Week for phone).
- Header shows month/week label; prev/next shifts by the current view's step.
- "Today" button resets both
currentDateandselectedDate. - FAB opens create modal with
start = selectedDate at next half hour.
src/components/calendar/MonthView.tsx (extract from current screen,
~180 lines) — real events, up to 3 colored dots per day, "+N" indicator when
more. Tap day → selects + scrolls agenda below. Long-press day → opens create
modal with that date.
src/components/calendar/WeekView.tsx (new, ~350 lines) — port of
calendar-week-view.tsx (511 lines) trimmed for mobile:
- 7 day columns, hour rows from 6am–10pm scrollable to 0–24.
- All-day strip at top.
- Events positioned absolutely by
utcStart/utcEnd, width shared among overlapping events (port webmail's stacking algorithm). - Tap event → detail modal. Long-press empty slot → create modal pre-filled.
- Horizontal swipe → prev/next week (react-native-gesture-handler already available via react-navigation).
- Deferred: drag-to-reschedule, drag-to-resize. Phone targets are too small for it to be better than tap-edit.
src/components/calendar/AgendaView.tsx (new, ~120 lines) — port of
calendar-agenda-view.tsx. Flat list of upcoming events grouped by day, 30
days forward. Taps open detail modal.
Phase 3 — Event interaction
src/components/calendar/EventDetailSheet.tsx (new, ~280 lines) — bottom
sheet presenting a single event, port of event-detail-popover.tsx (631 lines
on web — phone version drops the hover card chrome and the inline-edit fields):
- Header: calendar color bar, title, date/time range (or "All day"), calendar name.
- Sections: location, description (linkified), participants with RSVP status, recurrence summary (e.g. "Weekly on Mon, Wed — until 2026-06-01"), reminders.
- Actions row: Edit, Delete, Duplicate, "Open in email" if created from invite.
- If recurring: delete/edit prompts via
RecurrenceScopeDialog("This event only" / "This and following" / "All events").
src/components/calendar/EventModal.tsx (new, ~550 lines) — full-screen
modal for create + edit, port of event-modal.tsx (1,072 lines on web, trimmed):
- Title, calendar picker (color swatches), all-day toggle.
- Start/end date+time pickers (
@react-native-community/datetimepicker— add dep). - Location, description (multiline).
- Participant input with autocomplete from contacts store (port of
participant-input.tsx, ~180 lines). - Repeat picker: None / Daily / Weekly (weekday chips) / Monthly / Yearly, plus end: Never / After N / On date. Advanced iCal rules (BYSETPOS etc.) are deferred — covers the 95% case.
- Alerts: None / At start / 5m / 15m / 30m / 1h / 1d before, mapping to
Alert.trigger.offset. - Timezone: default to device TZ; optional override picker hidden behind "Advanced" disclosure.
- Save path handles "this instance only" override vs master update for
recurring events (write to
recurrenceOverrides[recurrenceId]).
src/components/calendar/RecurrenceScopeDialog.tsx (new, ~90 lines) —
direct port of recurrence-scope-dialog.tsx. Three-button picker before
edit/delete of a recurring instance.
Phase 4 — Multi-calendar
src/components/calendar/CalendarSidebarDrawer.tsx (new, ~200 lines) —
port of calendar-sidebar-panel.tsx (273 lines). Slides in from the left
(reuses SidebarDrawer.tsx):
- List of calendars with color swatch + visibility toggle + name.
- "My calendars" vs "Subscribed" sections if
myRightsdiffers. - Tap calendar → highlight; long-press → rename/color/hide (optional, phase 4.5).
- Footer: "Add calendar" (deferred to Phase 6).
src/theme/tokens.ts — audit colors.calendar.* palette against webmail's
calendar color constants (lib/calendar-utils.ts CALENDAR_COLOR_PALETTE);
add any missing shades.
Phase 5 — Polish
- Pull-to-refresh on each view → forces re-fetch of current window.
- Empty states per view (no calendars yet / no events this month).
- Loading skeleton for month grid and week grid.
- Error banner when fetch fails (pattern from
EmailListScreen). - Respect
email-storepush viahandleStateChangeso events update live when another client creates one.
Explicitly deferred (out of scope)
These are in the webmail but don't make sense for a first mobile pass:
- iCal import/subscribe modals (
ical-import-modal.tsx455 lines,ical-subscription-modal.tsx220 lines) — file picker + URL subscription with auth flows. Better as a dedicated settings flow later. - Tasks module (
task-list-view.tsx,task-modal.tsx,task-toolbar.tsx) — JSCalendar@type: 'Task'is a separate surface from events; worth its own screen, not bolted onto calendar. - Drag to reschedule / resize — mobile tap targets are too small for the fine-grained control the web drag gives; tap-edit is the mobile idiom.
- Quick event input (
quick-event-input.tsx) — NLP parser ("Lunch tomorrow at noon"). Neat on desktop, marginal on phone where typing is already slow. - Event context menu (
event-context-menu.tsx) — right-click is desktop- only. Actions move into the detail sheet's action row. - Mini-calendar (
mini-calendar.tsx) — web uses it as a sidebar date picker. Mobile already has the main month view; redundant. - Birthday calendar auto-derived events — lower priority; leave the contacts-store integration for a follow-up.
Dependencies to add
@react-native-community/datetimepicker— native date/time pickers.react-native-reanimatedis already in (via react-navigation); reuse for bottom-sheet animations onEventDetailSheet.- No new calendar library — reimplement RRULE expansion in ~400 lines rather
than pulling in
rrule.js(which adds ~180KB and expects browser APIs).
File map (new / modified)
repos/react-native/src/
├── screens/
│ └── CalendarScreenNew.tsx [rewrite]
├── components/
│ └── calendar/ [new directory]
│ ├── MonthView.tsx [new]
│ ├── WeekView.tsx [new]
│ ├── AgendaView.tsx [new]
│ ├── EventCard.tsx [new]
│ ├── EventDetailSheet.tsx [new]
│ ├── EventModal.tsx [new]
│ ├── ParticipantInput.tsx [new]
│ ├── RecurrenceScopeDialog.tsx [new]
│ ├── CalendarSidebarDrawer.tsx [new]
│ └── index.ts [new]
├── stores/
│ └── calendar-store.ts [extend]
├── lib/
│ ├── calendar-utils.ts [new]
│ └── recurrence-expansion.ts [new]
└── theme/
└── tokens.ts [audit calendar palette]
Testing approach
vitest.config.tsalready exists. Unit tests for:recurrence-expansion(daily/weekly/monthly, count+until, byDay).calendar-utils(duration parse, all-day overlap, DST boundary).- Store:
ensureRangededup,handleStateChangerefetch trigger.
- Manual emulator test matrix:
- Create event (timed, all-day, recurring), verify server round-trip.
- Edit recurring event via scope dialog.
- Delete recurring instance vs series.
- Toggle calendar visibility, confirm events disappear.
- Push update from webmail, confirm mobile refreshes.
Build order (suggested)
- Phase 1 in full — nothing else works without real data.
- MonthView + EventDetailSheet (read path) — first thing user sees.
- EventModal create path — unblocks manual testing of write path.
- AgendaView + CalendarSidebarDrawer — pulls the rest of month together.
- WeekView — biggest UI, easiest to defer if time runs short.
- EventModal edit + recurrence scope — finishes the edit surface.
- Phase 5 polish.
Rough size estimate: ~2,500 lines of new RN code + ~300 lines of store/util changes. Each phase is ship-safe on its own — the app stays functional even if we stop at Phase 2.