use-event-rsvp.ts3.8 KBView on GitHub import { useCallback } from 'react';
import { toast } from 'sonner';
import { toRsvpStatus, type CalendarEvent, type RsvpStatus } from '../types/calendar-types';
import { useUpdateCalendarEvent } from './use-calendar';
/**
* Answering an invitation — the whole write, in one place.
*
* Lifted out of `EventDetailsPopover.handleRsvpChange` (:1850). RSVP looks like a
* one-field update and is not: Google replaces the ENTIRE attendee array on write, so
* every other guest's `responseStatus` has to be re-sent verbatim or the write silently
* resets everyone else to `needsAction`. That rule is why this is a shared hook and not
* a mutation call each caller writes for itself.
*
* The optimism stays with the caller, via `onEventChange`: the hook does not know where
* the caller keeps its copy of the event. It is invoked up to three times per RSVP —
* once with the optimistic event, once with the server's answer, or once with the
* ORIGINAL event if the write failed (the rollback). A caller that only wants the happy
* path can pass nothing and let the mutation's own cache invalidation refresh the view.
*/
export interface UseEventRsvpOptions {
/** Optimistic apply, server confirm, and rollback all arrive here. */
onEventChange?: (event: CalendarEvent) => void;
}
export interface UseEventRsvp {
/** Resolves `true` when the RSVP reached Google, `false` when it was rolled back. */
setRsvp: (event: CalendarEvent, status: RsvpStatus) => Promise<boolean>;
isPending: boolean;
}
export function useEventRsvp({ onEventChange }: UseEventRsvpOptions = {}): UseEventRsvp {
const updateEventMutation = useUpdateCalendarEvent();
const setRsvp = useCallback(
async (event: CalendarEvent, newStatus: RsvpStatus): Promise<boolean> => {
if (!event.id) return false;
const attendees = event.attendees ?? [];
// The full array, ours rewritten and everyone else's preserved. `toRsvpStatus`
// narrows Google's open-string field to the four values the update accepts —
// an unknown value would be rejected for the whole request, not just that row.
const updatedAttendees = attendees.map((a) => {
if (a.self === true) {
return {
email: a.email || '',
displayName: a.displayName || undefined,
responseStatus: newStatus,
};
}
return {
email: a.email || '',
displayName: a.displayName || undefined,
responseStatus: toRsvpStatus(a.responseStatus),
};
});
// Keep the pre-write event for the rollback path.
const previousEvent = event;
const optimisticEvent: CalendarEvent = {
...event,
attendees: updatedAttendees.map((a) => ({
...attendees.find((orig) => orig.email === a.email),
email: a.email,
displayName: a.displayName,
responseStatus: a.responseStatus,
})),
};
onEventChange?.(optimisticEvent);
try {
const updatedEvent = await updateEventMutation.mutateAsync({
eventId: event.id,
sendUpdates: 'all',
requestBody: {
attendees: updatedAttendees.map((a) => ({
email: a.email,
displayName: a.displayName,
responseStatus: a.responseStatus,
})),
},
});
if (updatedEvent) {
onEventChange?.({
...event,
attendees: updatedEvent.attendees as CalendarEvent['attendees'],
});
}
return true;
} catch (error) {
onEventChange?.(previousEvent);
toast.error('Failed to update RSVP');
console.error(error);
return false;
}
},
[onEventChange, updateEventMutation],
);
return { setRsvp, isPending: updateEventMutation.isPending };
}