mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: keep all-day events all-day on Stalwart 0.16
This commit is contained in:
@@ -251,7 +251,8 @@ describe('calendar operations', () => {
|
|||||||
id: 's1', baseEventId: 'base', recurrenceId: '2026-03-02', start: '2026-03-02T00:00:00',
|
id: 's1', baseEventId: 'base', recurrenceId: '2026-03-02', start: '2026-03-02T00:00:00',
|
||||||
timeZone: 'Europe/Berlin', title: 'Daily',
|
timeZone: 'Europe/Berlin', title: 'Daily',
|
||||||
} as any;
|
} as any;
|
||||||
const single = { id: 's9', baseEventId: 'other', start: '2026-03-02T09:00:00', title: 'Once' } as any;
|
// Stalwart 1.0 returns a one-off event under its own id.
|
||||||
|
const single = { id: 's9', baseEventId: null, start: '2026-03-02T09:00:00', title: 'Once' } as any;
|
||||||
|
|
||||||
const [hydrated, untouched] = await hydrateExpandedOccurrences([occurrence, single]);
|
const [hydrated, untouched] = await hydrateExpandedOccurrences([occurrence, single]);
|
||||||
|
|
||||||
@@ -267,10 +268,40 @@ describe('calendar operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('skips the round trip when nothing recurs', async () => {
|
it('skips the round trip when nothing recurs', async () => {
|
||||||
const single = { id: 's9', baseEventId: 'other', start: '2026-03-02T09:00:00' } as any;
|
const single = { id: 's9', baseEventId: null, start: '2026-03-02T09:00:00' } as any;
|
||||||
expect(await hydrateExpandedOccurrences([single])).toEqual([single]);
|
const own = { id: 'd', baseEventId: 'd', start: '2026-03-02T09:00:00' } as any;
|
||||||
|
expect(await hydrateExpandedOccurrences([single, own])).toEqual([single, own]);
|
||||||
expect(mockRequest).not.toHaveBeenCalled();
|
expect(mockRequest).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stalwart 0.16 hands a one-off event out under a synthetic id, with no
|
||||||
|
// recurrenceId and no showWithoutTime, in the query's time zone: the
|
||||||
|
// all-day "Company holiday" read as 00:00 to 00:00.
|
||||||
|
it('gives a one-off all-day event under a synthetic id its all-day flag back', async () => {
|
||||||
|
mockRequest.mockResolvedValue({
|
||||||
|
methodResponses: [['CalendarEvent/get', {
|
||||||
|
list: [
|
||||||
|
{ id: 'c', showWithoutTime: true, duration: 'P1D' },
|
||||||
|
{ id: 'd', timeZone: 'Europe/Berlin', duration: 'PT1H' },
|
||||||
|
],
|
||||||
|
}, '0']],
|
||||||
|
});
|
||||||
|
const holiday = {
|
||||||
|
id: 'h1pceeaaaaac', baseEventId: 'c', '@type': 'Event', title: 'Company holiday',
|
||||||
|
start: '2026-11-01T00:00:00', duration: 'P1D', timeZone: 'Europe/Berlin',
|
||||||
|
utcStart: '2026-10-31T23:00:00Z', utcEnd: '2026-11-01T23:00:00Z',
|
||||||
|
} as any;
|
||||||
|
const review = {
|
||||||
|
id: 'h1pceeaaaaad', baseEventId: 'd', start: '2026-11-01T14:00:00', duration: 'PT1H', timeZone: 'Europe/Berlin',
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const [allDay, timed] = await hydrateExpandedOccurrences([holiday, review]);
|
||||||
|
|
||||||
|
expect(mockRequest.mock.calls[0][0][0][1]).toMatchObject({ ids: ['c', 'd'] });
|
||||||
|
expect(allDay).toMatchObject({ id: 'h1pceeaaaaac', showWithoutTime: true, timeZone: null, start: '2026-11-01T00:00:00' });
|
||||||
|
expect(allDay.recurrenceRules).toBeUndefined();
|
||||||
|
expect(timed).toBe(review);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getEvents for expanded occurrences', () => {
|
describe('getEvents for expanded occurrences', () => {
|
||||||
|
|||||||
@@ -937,6 +937,28 @@ live('live Stalwart calendar', () => {
|
|||||||
}, 60_000);
|
}, 60_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('an all-day event', () => {
|
||||||
|
// 0.16 expands a one-off event to a synthetic id without showWithoutTime;
|
||||||
|
// the detail sheet then said 00:00 - 00:00 and the editor's switch was off.
|
||||||
|
it('stays all-day on the calendar and in the editor', async () => {
|
||||||
|
await connectSingleton();
|
||||||
|
const created = await createEvent({
|
||||||
|
'@type': 'Event',
|
||||||
|
title: `${RUN} all day`,
|
||||||
|
start: '2027-11-10T00:00:00',
|
||||||
|
duration: 'P1D',
|
||||||
|
showWithoutTime: true,
|
||||||
|
} as Partial<CalendarEvent>, calendarId, false);
|
||||||
|
const uid = created.uid ?? (await getEvents([created.id]))[0].uid!;
|
||||||
|
const [event] = await loadOccurrences(uid, '2027-11-08T00:00:00Z', '2027-11-15T00:00:00Z');
|
||||||
|
expect(event, 'the event is on the calendar').toBeTruthy();
|
||||||
|
expect(event.showWithoutTime).toBe(true);
|
||||||
|
expect(event.timeZone ?? null).toBeNull();
|
||||||
|
expect(editorTimesFromEvent(event).allDay).toBe(true);
|
||||||
|
expect(getEventStartDate(event).getDate()).toBe(10);
|
||||||
|
}, 60_000);
|
||||||
|
});
|
||||||
|
|
||||||
describe('a subscribed feed', () => {
|
describe('a subscribed feed', () => {
|
||||||
const FEED_URL = 'https://feeds.invalid/live.ics';
|
const FEED_URL = 'https://feeds.invalid/live.ics';
|
||||||
let feed = '';
|
let feed = '';
|
||||||
|
|||||||
+5
-2
@@ -865,7 +865,10 @@ async function queryExpandedRange(
|
|||||||
* rule / overrides (and all-day ones without showWithoutTime). Fetch those
|
* rule / overrides (and all-day ones without showWithoutTime). Fetch those
|
||||||
* from the base events in one batched get and merge them in, so an
|
* from the base events in one batched get and merge them in, so an
|
||||||
* occurrence carries the same series context the device-side expansion
|
* occurrence carries the same series context the device-side expansion
|
||||||
* copies from its master.
|
* copies from its master. Stalwart 0.16 also hands a one-off event out
|
||||||
|
* under a synthetic id, without a recurrenceId and without
|
||||||
|
* showWithoutTime: an all-day one would read as 00:00 to 00:00, so its base
|
||||||
|
* is fetched as well.
|
||||||
*/
|
*/
|
||||||
export async function hydrateExpandedOccurrences(
|
export async function hydrateExpandedOccurrences(
|
||||||
events: CalendarEvent[],
|
events: CalendarEvent[],
|
||||||
@@ -873,7 +876,7 @@ export async function hydrateExpandedOccurrences(
|
|||||||
): Promise<CalendarEvent[]> {
|
): Promise<CalendarEvent[]> {
|
||||||
const baseIds = Array.from(new Set(
|
const baseIds = Array.from(new Set(
|
||||||
events
|
events
|
||||||
.filter((event) => isServerRecurrenceInstance(event) && event.recurrenceId)
|
.filter((event) => isServerRecurrenceInstance(event))
|
||||||
.map((event) => event.baseEventId as string),
|
.map((event) => event.baseEventId as string),
|
||||||
));
|
));
|
||||||
if (baseIds.length === 0) return events;
|
if (baseIds.length === 0) return events;
|
||||||
|
|||||||
@@ -349,9 +349,16 @@ export function hydrateRecurrenceInstances(
|
|||||||
bases: ReadonlyMap<string, Partial<CalendarEvent>>,
|
bases: ReadonlyMap<string, Partial<CalendarEvent>>,
|
||||||
): CalendarEvent[] {
|
): CalendarEvent[] {
|
||||||
return collapseDuplicateInstances(instances, bases).map((instance) => {
|
return collapseDuplicateInstances(instances, bases).map((instance) => {
|
||||||
if (!isServerRecurrenceInstance(instance) || !instance.recurrenceId) return instance;
|
if (!isServerRecurrenceInstance(instance)) return instance;
|
||||||
const base = instance.baseEventId ? bases.get(instance.baseEventId) : undefined;
|
const base = instance.baseEventId ? bases.get(instance.baseEventId) : undefined;
|
||||||
if (!base) return instance;
|
if (!base) return instance;
|
||||||
|
// Stalwart 0.16 expands a one-off event to a synthetic id too, without
|
||||||
|
// a recurrenceId and without showWithoutTime, in the query's time zone.
|
||||||
|
if (!instance.recurrenceId) {
|
||||||
|
return base.showWithoutTime
|
||||||
|
? { ...instance, showWithoutTime: true, timeZone: base.timeZone ?? null }
|
||||||
|
: instance;
|
||||||
|
}
|
||||||
|
|
||||||
const hydrated: CalendarEvent = {
|
const hydrated: CalendarEvent = {
|
||||||
...instance,
|
...instance,
|
||||||
|
|||||||
Reference in New Issue
Block a user