Skip to content

feat(calendar): reply to invitations, and change your own settings on events you're invited to → 3.157.0-alpha (C14a-c of #884) - #977

Open
BBYU26 wants to merge 6 commits into
feat/calendar-new-guests-reminders-colourfrom
feat/calendar-invitations
Open

BBYU26 wants to merge 6 commits into
feat/calendar-new-guests-reminders-colourfrom
feat/calendar-invitations

Conversation

@BBYU26

@BBYU26 BBYU26 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

PR B of the regrouped plan for #884 (docs/calendar-remaining-plan-2026-10-22.md): C14a, C14b and C14c, one commit
per layer, then one commit for the version bump and What's New → 3.157.0-alpha.

Stacks on PR A (#976). The branch is rebased on #976's reviewed tip (1b6d1e5, its two review rounds and the
What's New fix). Merge #976 first; this PR's base then moves to main.

Remove from my calendar (C14d) is not here: the regroup moved it to PR G, at the top of the stack. Until then,
deleting an invitation (every scope of a repeating one included) stays refused with "Someone else organises this
event".

L-I3 matters most in the release test round (in the live tests below): its answer decides the words of the warning shown when the
organiser lets guests change the event.

What users get

  • Reply to invitations from PM. Click an event you're invited to and choose Yes, No or Maybe under "Going?"; on a
    repeating event, pick this occurrence or all of them. Google tells the organiser, as when you reply in Google
    Calendar. If you're on the guest list from more than one of your calendars, each gets its own "Going?", named by its
    calendar.
    Replying needs editing turned on for that account.
  • Declined events are struck through, as in Google Calendar, on the Calendar tab (every view, the Terminal look
    included) and in Focus.
  • Edit opens on events someone else organises. Add a note to your reply, and change your own notifications,
    colour and Busy or Free, which emails nobody and asks nothing. If the organiser ticked "Modify event", the title,
    time, place, description and visibility open too, under Google's warning, and PM asks whether Google should email
    the guests. How it repeats and who's invited stay the organiser's.
  • The line under an invitation says what stays the organiser's and what's yours, or, with editing off, to turn
    editing on to reply.
  • A reply whose popover closed before it finished says how it went in a notice.

Per layer

C14a: replying to invitations on the backend (1299a7e)

  • New calendar_write/rsvp.rs (pure): finds the user's own guest-list entry (Google's self, the calendar's own
    address first), builds the reply body, checks whether Google already holds the reply, tidies the note (at most
    1,000 characters, counted as characters), and reads the reply for the editor.
    • A guest sends their own entry alone, every key as Google sent it, under attendeesOmitted.
    • The organiser sends the whole list back, only their own entry changed, and only when Google sent it whole;
      otherwise the guest form. Never a one-entry list. The organiser's own reply emails nobody.
  • New command respond_to_calendar_event (fence and ipc-commands rows added, registered in lib.rs): editing off is
    answered before any read; a fresh read decides; at most one silent rebase on a 412; a lost answer is looked up
    before anything is said; it lands from a fresh read. Scopes: This and All ("following" refused; a scope on a
    one-off is ignored, per the Adopted list). All re-fetches the occurrences and waits (1 s, then 3 s) for a clicked
    occurrence that had the repeating event's reply.
  • Gate: EditFacts.invited, the blanket check split so the reply shares its first half, reply_block, and per-row
    ListRights (delete, answer) replacing edit_block on the list. The calendar view's Delete now comes from the
    backend's rule and appears exactly where it did. New reason not_invited; FieldPermissions.respond (kept out of
    any()); EventForEdit.reply and .organizer_self.
  • New warning kept_own_reply: on All, occurrences answered on their own that kept their answer, their days in
    left_behind ("N occurrences you answered on their own kept their answer.").
  • Frontend data layer: types, the respondToCalendarEvent wrapper, the hook's respond, calendarEdit/rsvp.ts,
    and the reason and reply words in editReasons.ts. Nothing on screen called it yet in this layer.

C14b: reply from the popover; declined events struck through (f45e8ad)

  • New RsvpControl: "Going?" (or "Going? Replying for {calendar}.") with Yes, No and Maybe, nothing pressed before a
    reply. A one-off reply goes at once; the reply already shown sends nothing. A repeating event asks This event, All
    events or Back, focus on Back; Back and Escape return to the reply chosen, and Escape doesn't close the popover.
    While a reply is on its way the buttons are aria-disabled so focus stays (SegmentedControl gains busy), one
    reply at a time, with a status line. A result that isn't a success, or one that arrives after the control has gone,
    also goes to the calendar's notices, naming the event.
  • Popover: one strip per copy between the body and the actions; a ResizeObserver re-places the panel; still
    non-modal. A new pure replyCopies offers a reply from every copy, the one the dedup hides included.
  • Strike-through: a new pure markDeclined strikes a merged event only when every visible copy with the user's own
    entry declined. It covers chips, cards, all-day bands, Month bands, Agenda, both Terminal views, and Focus's grid
    and list. Focus's list reads a new backend field AgendaEvent.declined, combined across copies by the same rule.
  • The reason line: new not_organizer words and notOrganizerText ("can't reply here", "editing off"); the
    backend's read_only_message uses the same words. Help, SECURITY.md, the README and EDITING_COPY.on updated.

C14c: your own settings on invitations (93edb1e)

  • Gate allow-list (gate.rs): the blanket NotOrganizer is gone. On someone else's event without "modify", title,
    time, place, description and visibility close with NotOrganizer. How it repeats (new FieldPermissions.repeat),
    the guest list and delete always close; under "modify" the guest list closes with organizer_only_guests. A lock
    closes repeat too. EditFacts.guests_can_modify is read from the fresh copy and from the mirror row. A new
    gate::reason_for(perms, field) gives a refused field its own reason; one helper serves refused_field,
    send_delete and the choices' delete.
  • One event: nothing new in save_event; C12 already counts the organiser in the email question.
  • Repeating invitations: Master::read_only no longer refuses someone else's series. invitation_check runs before
    the guest checks: "this and following" past the first occurrence is refused (InvitationFollowing); guest
    changes, and shared fields without "modify", are refused; under "modify" a new rule or moving every occurrence's
    start is refused (InvitationRule), while a new length alone goes through. delete_plan refuses every scope. After
    a 412, a step that sends only the user's own settings goes again (it emails nobody), rather than stopping as a guest
    conflict (C1-M6).
  • Editor: "Going?" on top with the note, Google's warning under it where the organiser lets guests change the event;
    the Repeat row reads its own right and says why it's greyed; after a reply, withReply / guestsWithReply show
    the reply and your guest-list entry without making the draft dirty.
  • Calendar view: the line under an invitation; the dedup ranks copies (editable and yours, then editable, then the
    first).
  • Copy: not_organizer in C14c's words, organizer_only_guests, the group wording for not_invited, A11_NOTE,
    repeatTheirsText; help, README and EDITING_COPY.on.

Version (aa7c149)

3.157.0-alpha in all seven lockstep entries (lockfiles regenerated) and one What's New entry for the three layers.

Where PM differs from Google Calendar

PM Google Calendar
A reply offers This event and All events Google web may also offer "This and following" (probe I2-4 checks)
A guest list that comes back shorter after a reply is reported as failed, never saved No such check
A group or hidden invitation (no entry for this calendar) can't be replied to: "not invited" A group member can reply
The note is in the editor only The popover has "Add note"
A failed reply is said under the strip and as a notice while the popover is open One toast
A merged event is struck through only when every visible copy with your own entry declined (PM shows one row for all copies). Focus's list counts every copy, hidden calendars included, so it can stay plain where the grid strikes through Each calendar's copy is drawn, and struck through, on its own
Under "Modify event", how it repeats and who's invited stay the organiser's; a guest can't add guests "Modify event" lets guests change everything, invite others included (probe L-I5 checks what Google accepts)
On a repeating invitation, "this and following" past the first occurrence, a new repeat and moving every occurrence are refused Allowed under "Modify event"
Remove from my calendar isn't offered yet (PR G) Google offers Remove
On All, an occurrence answered on its own with the same reply as the repeating event reads as following it; if Google keeps its own reply PM says "saved, shows after the next refresh" Unknown (probe I2-2 settles it)

Tests flipped on purpose

  • the_list_marks_what_can_be_edited → the_list_marks_what_can_be_edited_deleted_and_answered; it,
    the_fresh_copy_decides_the_gate, an_organisers_event_with_guests_is_editable and
    the_editor_opens_with_the_users_reply read the new list rights and C14c's allow-list.
  • The fence test's example command is now send_raw_calendar_write, plus a case where one command defined twice hides
    a missing one (its list check compares as sets).
  • gate.rs: every_blanket_reason_blocks_everything drops its NotOrganizer row;
    a_blanket_reason_closes_notifications_and_colour and a_reply_alone_is_not_an_edit get new examples; the
    locked-copy test adds repeat.
  • dto.rs: the reason serialisation and any() tests.
  • series/mod.rs: a_master_pm_cant_read_or_change_says_why now expects None (someone else's series is no longer
    refused outright).
  • series/plan.rs: what_refuses_a_series_plan_and_what_only_warns gains a title row.
  • calendar_edit.rs: the_gate_refuses_before_any_row (busy/free is offered; a delete under "modify" says
    NotOrganizer) and a_draft_touching_a_closed_field_is_refused_with_the_reason; the banned-words walk adds the two
    new refusals.
  • TS: editReasons.test.ts (not_invited, kept_own_reply, C14c's words), eventDraft.test.ts,
    CalendarView.test.tsx.
  • Fixtures only: permission and editor-event fixtures gain the new fields; the calendar view's row fixture carries
    the list's rights; the CalendarView and editor test mocks gain respondToCalendarEvent, getSeriesSummary,
    respond and noteReply.

New tests: 60 in Rust and 79 in TypeScript across the three layers. Every new guard was broken by hand and seen
failing (the per-layer reports list them).

Live tests for Bobby

All in docs/calendar-live-tests-2026-10-10.md, in this order. Setup: account 2 connected in the dev build with
editing on, both accounts' Gmail and Google Calendar open; act only on events called "PM probe I…". For every step,
write down who was emailed and the subject (Q6).

  1. "Replying to invitations: C14a probes" (devtools; the section prints the allow-list line for account 2's main
    calendar and gives the console helpers).
    • L-I0: account 1 invites account 2 and its "+pm" alias to "PM probe I0". Note how many entries Google marks
      as yours; Yes, Maybe with a note and No with an empty note each land with the alias and the organiser unchanged;
      does the note show in Google web, and does the empty note remove it; a repeated reply sends nothing; a
      1,001-character note is refused.
    • L-I0b: the same with "Modify event" on. The alias must still be on the list afterwards; note every email.
      If PM says the guest list came back shorter, stop and tell me.
    • I0-7 to I0-9: the organiser's own reply. Does Google web show the organiser declined, and does anyone get an
      email?
    • L-I5 (APIs Explorer): does Google accept a guest adding a guest, and who is emailed?
    • If I0-2 fails with a message from Google, copy it (spec risk 11).
  2. "Replying to invitations from the popover (C14b)".
    • I1-1: reply Yes from account 2's strip; the popover's guest list catches up within about 30 seconds.
    • I1-2: with PM write test unticked, Maybe then No; struck through in every view; is it struck through in
      Google web? Ticking PM write test again shows it plain.
    • I1-5: the organiser declines their own event.
    • I2-1 to I2-4: a weekly invitation. Does the 3rd occurrence keep its own Yes after "All events"? Maybe for
      all after the organiser moved one; does Google web offer "This and following"?
    • I3-1 to I3-4: keyboard (focus on Back, Escape stays in the strip), the notice after the popover closes, the
      line with editing off, Focus's list and grid.
  3. "Your own settings on invitations (C14c)": account 2 makes a calendar "PM guest test", account 1 invites its
    Calendar ID, the allow-list is PM write test,PM guest test, PM write test unticked.
    • I13-1 to I13-5: the editor's reply and note (add, change, remove, the 1,000-character limit).
    • I2s-1 to I2s-4: Busy/Free, a notification and a colour each save with no question, account 1's copy
      unchanged; does Google let a guest change visibility?
    • I3m-1 to I3m-4 (L-I3), the one that can change words: with "Modify event" on, change the title and choose Don't send.
      Does account 1's copy change? If it does, the warning becomes "Your changes reach everyone invited…". Then
      Send, and write down every email.
    • I4r-1 to I4r-6: a weekly invitation with "Modify event": "This and following" refused in words, moving all
      refused, moving one asks the email question (did account 1's occurrence move?), a new length alone offered.
    • L-I5b (optional): an invitation sent to a group, via the APIs Explorer.

Gate

Full just check on the rebased branch with the version bump: passed (exit 0).

Recipe Result
prettier, tsc (app and node), cargo fmt, ruff, ruff format pass
eslint pass: 0 errors, 26 warnings, all in files this PR doesn't touch
version lockstep all 7 entries 3.157.0-alpha
files-in-place, SPDX headers, licence subset, CI membership, sync-set, script-deps, action pins, requirements lock, Node version, npm / sidecar / model licences pass
ipc-commands 311 wrapped commands match the 314 the backend registers
calendar write fence pass (153 Rust and 521 TypeScript files)
vitest 185 files, 2,753 tests passed
vite build pass
clippy -D warnings, cargo check pass
cargo test 2,547 passed, 2 ignored
sidecar tests 111 ran, OK (8 skipped)
cargo deny, pip-audit, npm audit pass, no known vulnerabilities
gitleaks (tree and history), zizmor no leaks, no findings

After the gate, one sentence of the What's New entry was reworded ("If you're on the guest list from more than one of
your calendars…"); just check-fast passed on it.

The rebase onto #976's tip had five conflicts, all "both sides added": WriteWarning gained partly_applied (PR A)
and kept_own_reply (C14a) in dto.rs, types.ts, editReasons.ts and its test, kept both; the help entry "Your
calendar" kept PR A's "except for this and the following events" and gained C14b's and C14c's sentences.

Review

Reviewed by four areas (replying on the backend, the popover, what a guest may change, tests and copy), each finding checked by a skeptic, then fixed and re-reviewed until a round came back dry (3 rounds; 7 of 14 round-1 findings kept):

  • Round 1 (1b6b754): an "All events" reply sent from an occurrence you'd answered on its own now waits until Google's listing has caught up (the same check repeating-event saves use) before it says which occurrences kept their own answer, and otherwise says it will show after the next refresh and records nothing; Going? never shows a reply as pressed on an occurrence that kept its own answer; a reply you sent stops overriding the shown answer once Google's answer moves on, so a change made in Google wins; Send note during a reply no longer loses the note; help and README no longer suggest guests can add guests or change how an invitation repeats.
  • Round 2 (66e7e98): a pending "All events" reply no longer presumes which occurrences took it.
  • Round 3: dry. Full gate passed after the fixes.

Known cost, by design: after a pending "All events" reply, an occurrence that did take it shows its old answer until the next refresh (never a reply Google doesn't hold). Live step I2-2b checks the "caught up" inference for replies.

Part of #884

🤖 Generated with Claude Code

BBYU26 and others added 4 commits October 22, 2026 23:55
PM can now send the user's reply to an invitation (Yes, No or Maybe, with a
note, for one occurrence or all of a repeating one) by changing only their own
entry on the guest list, read fresh from Google and landed from a fresh read.
The organiser's own reply sends the whole list back and emails nobody. The
calendar list now says, per row, whether it can be deleted and answered, as
the gate does, and the view takes Delete from it. Nothing on screen calls the
reply yet; it is probed from devtools first (L-I0).

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An event you're invited to now has "Going?" with Yes, No and Maybe at the foot of its details, one strip per copy you can reply from (with both accounts shown, the organiser's copy and the invitation's are one event, and each is named). A repeating invitation asks This event or All events first, with focus on Back. The reply goes at once and Google tells the organiser; what it came to is said under the strip, and as a notice when the details have closed or the reply didn't go. The details read their row afresh, so a landed reply shows there.

Events you declined are struck through, as Google draws them: on the Calendar tab only when every copy with your own guest entry declined, in every view and the Terminal look, and on the Focus tab, whose agenda now says which events you declined. The line under someone else's event says you can reply, or how to turn editing on to. Help, README, SECURITY and the editing setting's words mention replies.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (C14c of #884)

Edit opens on someone else's event: your reply and its note sit on top, your own
notifications, colour and busy or free save without an email question, and the
title, time, place, description and visibility change only where the organiser
lets guests change the event, under Google's warning, with Send / Don't send
counting the organiser. How it repeats, who is invited and deleting stay the
organiser's. A repeating invitation takes this event or all events, never this
and following, and a guest never re-rules it or moves every occurrence. A
refusal names the refused field's own reason.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#884)

PR B's version bump and one What's New entry for its three layers:
replying to invitations from the popover, declined events struck
through, and your own settings on events someone else organises.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:03
@BBYU26 BBYU26 added enhancement New feature or request rust Pull requests that update rust code javascript Pull requests that update javascript code labels Oct 22, 2026
@BBYU26 BBYU26 self-assigned this Oct 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

BBYU26 and others added 2 commits October 23, 2026 00:29
- All events from an occurrence answered on its own: the listing counts as
  current only once an occurrence in it is updated no earlier than the
  repeating event's copy read after the reply, as land_series floors a
  change to every occurrence. A lagging listing is read again (1 s, 3 s)
  and never landed with its occurrences said to have kept their own
  answer; never caught up, saved_pending with nothing recorded.
- Going? after an All events reply that some occurrence kept its own
  answer against no longer shows the reply sent: the occurrence that kept
  No keeps No pressed, in the editor and the popover.
- The reply and the note sent give way for good once the row moves on, so
  a change made in Google back to the earlier answer shows.
- Send note sends nothing while a reply is on its way, and no longer swaps
  the occurrence question that reply was sent from.
- Help: "On events you organise, you can add guests", and Busy or Free as
  What's New writes it. README: what opens where the organiser lets guests
  change an invitation, and what never does.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An all-events reply PM couldn't read back yet (round 1's floor makes a
  lagging listing end that way) no longer shows as the clicked occurrence's
  own answer: from an occurrence answered on its own, "Going?" and the
  editor's guest list showed the reply sent while Google kept its answer,
  and the reply couldn't be sent to that occurrence alone. One pure helper,
  replyHeld (rsvp.ts), now decides for RsvpControl and EventEditor.onReply:
  no_change, a reply saved to this copy, or a landed all-events reply whose
  left_behind doesn't name the occurrence's day.
- land_every_reply keeps the days of occurrences that kept their own answer
  when the mirror can't take the listing: Saved with MirrorRefreshPending
  and KeptOwnReply, the hash dropped, rather than a bare saved_pending.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request javascript Pull requests that update javascript code rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants