diff --git a/components/calendar/calendar-agenda-view.tsx b/components/calendar/calendar-agenda-view.tsx index 1aa0513c..a31510e8 100644 --- a/components/calendar/calendar-agenda-view.tsx +++ b/components/calendar/calendar-agenda-view.tsx @@ -148,6 +148,9 @@ export function CalendarAgendaView({ const color = getEventColor(ev, calendar); const start = getEventStartDate(ev); const end = getEventEndDate(ev); + // iTIP CANCEL marks the attendee's copy with status "cancelled" + // instead of deleting it (#572). + const isCancelled = ev.status === "cancelled"; const locationName = ev.locations ? Object.values(ev.locations)[0]?.name : null; @@ -159,7 +162,10 @@ export function CalendarAgendaView({ onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())} onMouseLeave={() => onHoverLeave?.()} onContextMenu={onContextMenuEvent ? (e) => onContextMenuEvent(e, ev) : undefined} - className="w-full flex items-start px-4 hover:bg-muted/50 transition-colors text-left" + className={cn( + "w-full flex items-start px-4 hover:bg-muted/50 transition-colors text-left", + isCancelled && "opacity-60" + )} style={{ gap: 'var(--density-item-gap)', paddingBlock: 'var(--density-item-py)' }} >
@@ -181,7 +187,7 @@ export function CalendarAgendaView({ />
-
+
{ev.title || t("events.no_title")}
{locationName && ( diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index 10808d0e..33d9e484 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -88,7 +88,10 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM try { return format(d, fmt); } catch { return "--:--"; } }; const timeString = `${safeFormat(startDate, timeFmt)} – ${safeFormat(endTime, timeFmt)}`; - const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`; + // iTIP CANCEL marks the attendee's copy with status "cancelled" instead of + // deleting it (#572) - render it struck through and dimmed. + const isCancelled = event.status === "cancelled"; + const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}${isCancelled ? `, ${t("detail.cancelled")}` : ""}`; const handleDragStart = useCallback((e: DragEvent) => { e.stopPropagation(); @@ -136,6 +139,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM "hover:opacity-80 transition-opacity", isSelected && "ring-2 ring-primary", isBeingDragged && "opacity-50", + isCancelled && !isBeingDragged && "opacity-60", className )} style={{ backgroundColor: `${color}20`, color, ...style }} @@ -144,7 +148,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} /> - {event.title || t("events.no_title")} + {event.title || t("events.no_title")} ); } @@ -165,6 +169,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM continuesAfter && "pr-2", isSelected && "ring-2 ring-primary", isBeingDragged && "opacity-50", + isCancelled && !isBeingDragged && "opacity-60", className )} style={{ backgroundColor: `${color}24`, borderLeft: `3px solid ${color}`, color, ...style }} @@ -173,7 +178,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM {showTimeInMonthView && !event.showWithoutTime && ( {format(startDate, timeFmt)} )} - {event.title || t("events.no_title")} + {event.title || t("events.no_title")}
); @@ -193,11 +198,12 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM "hover:opacity-90 transition-opacity cursor-pointer", isSelected && "ring-2 ring-primary", isBeingDragged && "opacity-50", + isCancelled && !isBeingDragged && "opacity-60", className )} style={{ backgroundColor: `${color}30`, borderLeft: `3px solid ${color}`, color, ...style }} > -
{event.title || t("events.no_title")}
+
{event.title || t("events.no_title")}
{!event.showWithoutTime && (
{timeString} diff --git a/components/calendar/event-detail-popover.tsx b/components/calendar/event-detail-popover.tsx index 79af4181..750f284e 100644 --- a/components/calendar/event-detail-popover.tsx +++ b/components/calendar/event-detail-popover.tsx @@ -290,7 +290,10 @@ export function EventDetailPopover({ className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} /> -

+

{event.title || t("events.no_title")}

@@ -303,7 +306,7 @@ export function EventDetailPopover({ )} {event.status === "cancelled" && ( - + {t("detail.cancelled")} )} diff --git a/lib/__tests__/calendar-alerts.test.ts b/lib/__tests__/calendar-alerts.test.ts index 5a791262..c57cd2ba 100644 --- a/lib/__tests__/calendar-alerts.test.ts +++ b/lib/__tests__/calendar-alerts.test.ts @@ -302,6 +302,19 @@ describe('getPendingAlerts', () => { expect(result).toHaveLength(0); }); + it('skips alerts for cancelled events', () => { + // iTIP CANCEL marks the attendee's copy with status "cancelled" instead + // of deleting it (#572) - its reminders must not fire. + const event = makeEvent({ + status: 'cancelled', + alerts: { 'a1': makeAlert() }, + }); + const calendars = [makeCalendar()]; + const now = fiveMinBefore + 1000; + const result = getPendingAlerts([event], calendars, new Set(), now); + expect(result).toHaveLength(0); + }); + it('skips email action alerts', () => { const event = makeEvent({ alerts: { 'a1': makeAlert({ action: 'email' }) }, diff --git a/lib/calendar-alerts.ts b/lib/calendar-alerts.ts index 69de556f..f7b4f4c9 100644 --- a/lib/calendar-alerts.ts +++ b/lib/calendar-alerts.ts @@ -103,6 +103,8 @@ export function getPendingAlerts( const pending: PendingAlert[] = []; for (const event of events) { + if (event.status === 'cancelled') continue; + const alerts = getEffectiveAlerts(event, calendars); if (!alerts) continue;