/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { AlertTriangle, CalendarDays, Check, Clock3, Loader2, MapPin } from 'lucide-react' import { firstCalendarInvitationAttachment } from '#features/calendar/lib/calendar-invitation' import type { CalendarInvitationDetails, InvitationWhen, } from '#features/calendar/server/calendar-invitation-fns' import { cn } from '#shared/lib/utils' import type { MailMessage } from '../state/mail-queries.js' const RESPONSE_OPTIONS = [ { status: 'yes', label: 'Accept' }, { status: 'maybe', label: 'Maybe' }, { status: 'no', label: 'Decline' }, ] as const const SYNC_RETRY_INTERVAL_MS = 2_000 const SYNC_LOOKUP_LIMIT = 5 export function CalendarInvitationCard({ message }: { message: MailMessage }) { const attachment = firstCalendarInvitationAttachment(message.attachments) if (!attachment) return null return ( ) } function CalendarInvitationContent({ messageId, attachmentId }: { messageId: string; attachmentId: string }) { const queryClient = useQueryClient() const queryKey = ['calendar', 'invitation', messageId, attachmentId] as const const invitation = useQuery({ queryKey, queryFn: async () => { const { getCalendarInvitation } = await import('#features/calendar/server/calendar-invitation-fns') return getCalendarInvitation({ data: { messageId, attachmentId } }) }, staleTime: 30_000, retry: false, refetchInterval: (query) => { const details = query.state.data // The invitation email and its provider-created event arrive independently. // Recheck briefly while that race settles. Any failed automatic lookup // permanently hands control to the explicit retry instead of starting a loop. return (details?.state === 'syncing' || details?.state === 'cancelling') && query.state.errorUpdateCount === 0 && query.state.dataUpdateCount < SYNC_LOOKUP_LIMIT ? SYNC_RETRY_INTERVAL_MS : false }, }) const response = useMutation({ mutationFn: async (status: 'yes' | 'maybe' | 'no') => { const { respondCalendarInvitation } = await import('#features/calendar/server/calendar-invitation-fns') return respondCalendarInvitation({ data: { messageId, attachmentId, status } }) }, onSuccess: (receipt) => { queryClient.setQueryData(queryKey, (current) => { /* v8 ignore next -- a successful mutation can only originate from the rendered ready state; cache removal during the request is a safe no-op */ if (current?.state !== 'ready') return current return { ...current, status: receipt.status } }) void queryClient.invalidateQueries({ queryKey: ['calendar', 'range'], refetchType: 'active' }) }, }) const addInvitation = useMutation({ mutationFn: async () => { const { addCalendarInvitation } = await import('#features/calendar/server/calendar-invitation-fns') return addCalendarInvitation({ data: { messageId, attachmentId } }) }, onSuccess: (details) => { queryClient.setQueryData(queryKey, details) void queryClient.invalidateQueries({ queryKey: ['calendar', 'range'], refetchType: 'active' }) }, }) if (invitation.isPending) { return (
Checking your calendar…
) } if (invitation.isError) { return ( void invitation.refetch()} retrying={invitation.isFetching} /> ) } const details = invitation.data if (details.state === 'invalid') { return ( ) } if (details.state === 'syncing') { const canAdd = details.canAdd !== false return ( void invitation.refetch()} retrying={invitation.isFetching} onAction={canAdd ? () => addInvitation.mutate() : undefined} actionPending={addInvitation.isPending} error={addInvitation.isError} /> ) } if (details.state === 'cancelling') { return ( void invitation.refetch()} retrying={invitation.isFetching} /> ) } if (details.state === 'ineligible') { return ( ) } if (details.state === 'cancelled') { return ( ) } const canRespond = details.canRespond !== false return (

Calendar invitation

{details.title}

From {details.organizer}

{formatInvitationWhen(details.when)}
{details.location ? (
{details.location}
) : null}

{canRespond ? responseLabel(details.status) : 'Added to your calendar'}

{canRespond ? (
Respond to invitation {RESPONSE_OPTIONS.map((option) => { const selected = details.status === option.status return ( ) })}
) : null}
{canRespond && response.isError ? (

Your response wasn’t saved. Check your connection, then try again.

) : null}
) } function ConflictNotice({ conflicts, }: { conflicts: Extract['conflicts'] }) { if (conflicts.state === 'clear') { return (

No conflicts on your calendar

) } if (conflicts.state === 'unknown') { return (

We couldn’t check your full schedule. Review your calendar before responding.

) } return (

This overlaps with {conflicts.count} {conflicts.count === 1 ? 'event' : 'events'} on your calendar.

) } function InvitationNotice({ title, message, onRetry, retrying = false, onAction, actionPending = false, error = false, }: { title: string message: string onRetry?: () => void retrying?: boolean onAction?: () => void actionPending?: boolean error?: boolean }) { return (

{title}

{message}

{onRetry || onAction ? (
{onAction ? ( ) : null}
) : null} {error ? (

We couldn’t add this invitation. Check your connection, then try again.

) : null}
) } function responseLabel(status: 'yes' | 'no' | 'maybe' | 'noreply'): string { if (status === 'yes') return 'You accepted' if (status === 'maybe') return 'You replied maybe' if (status === 'no') return 'You declined' return 'Awaiting your response' } function formatInvitationWhen(when: InvitationWhen): string { if (when.kind === 'timed') { const start = new Date(when.start * 1_000) const end = new Date(when.end * 1_000) const date = new Intl.DateTimeFormat(undefined, { dateStyle: 'full' }).format(start) const time = new Intl.DateTimeFormat(undefined, { timeStyle: 'short' }) return `${date} · ${time.format(start)}–${time.format(end)}` } const start = localCalendarDate(when.startDate) const end = localCalendarDate(when.endDate) const date = new Intl.DateTimeFormat(undefined, { dateStyle: 'full' }) if (end.getTime() - start.getTime() <= 86_400_000) return `All day · ${date.format(start)}` const inclusiveEnd = new Date(end.getTime() - 86_400_000) return `All day · ${date.format(start)}–${date.format(inclusiveEnd)}` } function localCalendarDate(value: string): Date { const [year = 0, month = 0, day = 0] = value.split('-').map(Number) return new Date(year, month - 1, day) }