mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 08:06:59 +00:00
fix: say cancelled when the organizer cancels an event
This commit is contained in:
@@ -159,8 +159,11 @@ liveTwo('live Stalwart calendar event notifications', () => {
|
||||
const cancel = notes.find((n) => n.calendarEventId === attendeeEventId && !ours.has(n.id));
|
||||
expect(cancel, JSON.stringify(notes)).toBeTruthy();
|
||||
ours.add(cancel!.id);
|
||||
// Both servers send it as an update that sets the status to cancelled
|
||||
// (the attendee's copy stays, cancelled); it must read as a cancellation.
|
||||
const toast = eventNotificationToast(cancel!, t)!;
|
||||
if (cancel!.type === 'destroyed') expect(toast.type).toBe('warning');
|
||||
expect(toast.type, JSON.stringify(cancel)).toBe('warning');
|
||||
expect(toast.title).toContain(`cancelled "${RUN} review"`);
|
||||
|
||||
await destroyCalendarEventNotifications([...ours]);
|
||||
const after = (await getCalendarEventNotifications()).map((n) => n.id);
|
||||
|
||||
@@ -20,8 +20,20 @@ export interface CalendarEventNotification {
|
||||
type: 'created' | 'updated' | 'destroyed';
|
||||
calendarEventId: string;
|
||||
isDraft?: boolean;
|
||||
/** The event as it is now; 1.0 returns it, 0.16 leaves it out. */
|
||||
/** The event before the change on 1.0 (as created for an invitation); 0.16 leaves it out. */
|
||||
event?: { title?: string | null } | null;
|
||||
/**
|
||||
* What changed, for an `updated` one: a JSCalendar patch on 1.0, the whole
|
||||
* event as it is now on 0.16. An organizer's cancellation reaches the
|
||||
* attendee as an update that sets `status` to `cancelled` (iTIP CANCEL
|
||||
* keeps the attendee's copy), never as `destroyed`.
|
||||
*/
|
||||
eventPatch?: { status?: string | null; title?: string | null } | null;
|
||||
}
|
||||
|
||||
/** An organizer cancelled the event: the change sets its status to cancelled. */
|
||||
export function isCancellation(n: Pick<CalendarEventNotification, 'type' | 'eventPatch'>): boolean {
|
||||
return n.type === 'destroyed' || (n.type === 'updated' && n.eventPatch?.status === 'cancelled');
|
||||
}
|
||||
|
||||
const USING = [CAPABILITIES.CORE, CAPABILITIES.CALENDARS];
|
||||
@@ -42,7 +54,7 @@ export async function getCalendarEventNotifications(): Promise<CalendarEventNoti
|
||||
['CalendarEventNotification/get', {
|
||||
accountId,
|
||||
'#ids': { resultOf: '0', name: 'CalendarEventNotification/query', path: '/ids' },
|
||||
properties: ['id', 'created', 'changedBy', 'comment', 'type', 'calendarEventId', 'isDraft', 'event'],
|
||||
properties: ['id', 'created', 'changedBy', 'comment', 'type', 'calendarEventId', 'isDraft', 'event', 'eventPatch'],
|
||||
}, '1'],
|
||||
], USING);
|
||||
const responses = (response.methodResponses ?? []) as [string, any, string][];
|
||||
|
||||
@@ -10,7 +10,8 @@ const h = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('react-native', () => ({ AppState: { addEventListener: vi.fn() } }));
|
||||
vi.mock('../../api/jmap-client', () => ({ jmapClient: h.client }));
|
||||
vi.mock('../../api/calendar-event-notifications', () => ({
|
||||
vi.mock('../../api/calendar-event-notifications', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../api/calendar-event-notifications')>()),
|
||||
getCalendarEventNotifications: h.list,
|
||||
destroyCalendarEventNotifications: h.destroy,
|
||||
}));
|
||||
@@ -71,6 +72,17 @@ describe('the toast for a calendar event notification', () => {
|
||||
.toEqual({ type: 'warning', title: 'Land cancelled "Planning"', message: 'Sorry, ill' });
|
||||
});
|
||||
|
||||
it('says cancelled, as a warning, when the organizer cancels (an update setting status cancelled)', () => {
|
||||
// What Stalwart 1.0.1 sends the attendee after CalendarEvent/set destroy
|
||||
// with sendSchedulingMessages; 0.16.27 sends the whole event as the patch.
|
||||
expect(eventNotificationToast(note('4', { eventPatch: { sequence: null, status: 'cancelled' } as never }), t))
|
||||
.toEqual({ type: 'warning', title: 'Land cancelled "Planning"', message: undefined });
|
||||
expect(eventNotificationToast(note('5', { event: null, eventPatch: { title: 'Planning', status: 'cancelled' } }), t)!.type)
|
||||
.toBe('warning');
|
||||
// Any other change stays an update.
|
||||
expect(eventNotificationToast(note('6', { eventPatch: { title: 'Planning v2' } }), t)!.type).toBe('info');
|
||||
});
|
||||
|
||||
it('falls back to the address, then "Someone", and to an untitled event', () => {
|
||||
expect(eventNotificationToast(note('1', { changedBy: { email: 'x@example.org' }, event: null }), t)!.title)
|
||||
.toBe('x@example.org updated "Untitled event"');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { jmapClient } from '../api/jmap-client';
|
||||
import {
|
||||
destroyCalendarEventNotifications,
|
||||
getCalendarEventNotifications,
|
||||
isCancellation,
|
||||
type CalendarEventNotification,
|
||||
} from '../api/calendar-event-notifications';
|
||||
import { useAuthStore } from '../stores/auth-store';
|
||||
@@ -33,7 +34,9 @@ export interface EventNotificationToast {
|
||||
/** The toast for one notification; null for a draft (the user's own unsent change). */
|
||||
export function eventNotificationToast(n: CalendarEventNotification, t: Translate): EventNotificationToast | null {
|
||||
if (n.isDraft) return null;
|
||||
const kind = n.type === 'created' ? 'invited' : n.type === 'destroyed' ? 'cancelled' : 'updated';
|
||||
// Stalwart sends an organizer's cancellation as an update setting the
|
||||
// status to cancelled (the webmail reads only `type` and says "updated").
|
||||
const kind = n.type === 'created' ? 'invited' : isCancellation(n) ? 'cancelled' : 'updated';
|
||||
const params = {
|
||||
name: n.changedBy?.name || n.changedBy?.email || t('calendar_event_notifications.someone', 'Someone'),
|
||||
title: n.event?.title || t('calendar_event_notifications.untitled', 'Untitled event'),
|
||||
|
||||
Reference in New Issue
Block a user