Skip to content

Propose a new time for an invitation - #2926

Open
brhellman wants to merge 3 commits into
Foundry376:masterfrom
brhellman:propose-new-time
Open

brhellman wants to merge 3 commits into
Foundry376:masterfrom
brhellman:propose-new-time

Conversation

@brhellman

Copy link
Copy Markdown
Contributor

Only the organizer may revise a meeting (RFC 5546 section 2.1.4), so an attendee who cannot make the slot had accept, decline, or a hand-written reply. iTIP's third answer is COUNTER (section 3.2.7) - what Google Calendar calls "Propose a new time" - and the sync engine has accepted it in performRemoteSendRSVP since Mailspring-Sync#130 (merged yesterday).

The invitation header now offers Propose a new time as a link under the three RSVP buttons. ProposeTimePopover (in src/components, since the calendar package will share it) shows the current slot, a date and two time pickers - moving the start carries the end with it, editing the end changes the duration - and an optional note for the organizer. createCounterProposal builds the COUNTER from the emailed invitation: same UID and ORGANIZER so the organizer's calendar matches it, only the proposer listed with PARTSTAT=TENTATIVE (echoing the other guests' responses back would invite the organizer's calendar to overwrite them), no RRULE/RDATE/EXDATE since a counter names one time and an inherited rule would read as "move the whole series", DTEND rather than a carried-over DURATION (section 3.6.1), fresh DTSTAMP so a later proposal supersedes an earlier one. EventRSVPTask.forProposingNewTime sends it with method: 'COUNTER'; the engine reads an absent method as REPLY, so existing tasks are unaffected. A proposal is not an answer, so the task's onSuccess does not mark the invitation's RSVP buttons answered. After sending, the row reads "You proposed ".

The popover's stylesheet lives with the other shared component styles that index.less loads in every window; the events package's own stylesheet only loads in the main window, which is why the calendar window would otherwise render it unstyled.

Stacks on #2925 (conflicts) → #2924 → #2916; carries their commits.

Verified: createCounterProposal (14 cases incl. recurring invitation, DURATION, immutability of the source), EventRSVPTask.forProposingNewTime (3), the package smoke test that the component-kit export resolves; with the stack's other suites 231 passing. Lint, prettier and tsc clean. Extracted from the calendar-scheduling branch, where a COUNTER sent from it was delivered and rendered by Google Calendar on the organizer's side (2026-09).

🤖 Generated with Claude Code

@manilabui

manilabui commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

I tested this against a Google organizer: Google shows the proposed time on the event and marks me as Maybe, which seems right.

  1. What should happen for a recurring invitation? On one for a series that began Jul 31, the popover opens on Jul 31. The counter drops RRULE and RECURRENCE-ID but keeps the UID, so it names the series master. For an invitation to a single occurrence it also drops the RECURRENCE-ID, so a proposal for one occurrence reads as one for the whole series. I think the counter should keep RECURRENCE-ID, and for a series name the occurrence being countered.
  2. An all-day invitation gets time pickers, and the counter comes out timed: proposing Oct 13 for an all-day event on Oct 12 sends DTSTART:20261013T050000Z / DTEND:20261014T050000Z, which is midnight to midnight in my zone and a different span in anyone else's. For an all-day invitation, can the popover show dates only and the counter keep VALUE=DATE? I'll be reworking this UI afterwards, so no worries if it looks rough.
  3. Unprotected: keeping RECURRENCE-ID, dropping PARTSTAT=TENTATIVE, taking the first VEVENT instead of the master, and case-sensitive matching all leave 210 green.
  4. removeAllProperties('duration') is dead: ical.js's endDate setter already removes DURATION, and the spec passes without the line. options.name likewise; both callers pass the CN the helper already reads.
  5. Can the "Propose a new time" link come out of the invitation header for now? I'd like proposing to start from the calendar only (Answer or counter an invitation from the calendar's right-click menu #2928 adds it there) until I've reworked this UI.

A nice sync engine follow-up from testing this: the proposal email only names the event right now. It could mention the old and new slot, e.g. "from Fri, Oct 9 at 2:30 PM to Oct 12 at 12:00 PM".

🤖 Generated with Claude Code

Brian Hellman and others added 3 commits October 2, 2026 16:24
…hen there is none

Answering an invitation from the message did one of the two things an RSVP
is: it emailed the organizer a REPLY. Under CalDAV the attendee also writes
their PARTSTAT back to their own copy of the event (RFC 6638 section 3.2.5),
and Google only reconciles the two when the organizer is also on Google, so
the calendar kept showing the meeting as unanswered - or, for an invitation
Google had not put on the calendar at all, showed nothing.

resolveRSVPTarget picks the copy we are entitled to write to: one UID can sit
on our calendar, a room's and a colleague's at once, and the header now says
which calendar the answer goes to, or why it will only be emailed. When the
event is on none of our calendars, accepting creates it on our own (never for
an invitation naming us as ORGANIZER, which a scheduling server would turn
into outbound mail). Everything sent to the organizer is built from the
emailed attachment, never the synced copy, because Google rewrites ORGANIZER
on shared calendars to an address nobody reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Answering from the message meant opening the calendar to check the slot.
ICSEventHelpers.findConflicts expands the account's stored events across the
slot being answered - the next occurrence of a recurring invitation, else the
event itself - honouring TRANSP:TRANSPARENT, CANCELLED and this account's own
DECLINED, leaving out all-day events, read-only subscribed feeds, calendars
hidden in the sidebar and calendars the server says are someone else's. The
header lists what overlaps, the way Google Calendar does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Only the organizer may revise a meeting (RFC 5546 section 2.1.4), so an
attendee who cannot make the slot could accept, decline, or reply by hand.
iTIP's COUNTER (section 3.2.7) is the third answer - Google Calendar's
"Propose a new time" - and the sync engine has accepted it since
Mailspring-Sync#130. This adds the pieces the calendar's context menu (Foundry376#2928)
wires up: ProposeTimePopover picks the slot (dates only for an all-day
invitation) and an optional note, createCounterProposal builds the COUNTER
from the invitation with only the proposer listed and one occurrence named
by RECURRENCE-ID, and EventRSVPTask sends it without marking the invitation
answered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants