mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: show all-day events on their own day in the Calendar Agenda for viewers west of the server's time zone
This commit is contained in:
@@ -52,6 +52,7 @@ interface AgendaEvent {
|
||||
accountId: string;
|
||||
uid: string | null;
|
||||
title: string;
|
||||
/** An instant (`...Z`); for an all-day event the date-time without an offset. */
|
||||
start: string;
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
@@ -97,6 +98,20 @@ function eventEnd(event: Partial<CalendarEvent>): Date {
|
||||
return new Date(start.getTime() + parseDurationMinutes(event.duration) * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* An all-day event's start and end as the dates it is stored with
|
||||
* (`2026-10-27T00:00:00`, no offset). As an instant it would be midnight in
|
||||
* this server's time zone, which anyone west of the server reads as the day
|
||||
* before; `new Date()` reads a date-time without an offset as the viewer's
|
||||
* own local time.
|
||||
*/
|
||||
function allDayRange(event: Partial<CalendarEvent>): { start: string; end: string } {
|
||||
const start = (event.start as string).slice(0, 19);
|
||||
const startMs = Date.parse(`${start}Z`);
|
||||
const end = new Date(startMs + parseDurationMinutes(event.duration) * 60000).toISOString().slice(0, 19);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function firstLocationName(event: Partial<CalendarEvent>): string | null {
|
||||
const locations = event.locations;
|
||||
if (!locations || typeof locations !== 'object') return null;
|
||||
@@ -318,8 +333,9 @@ export async function POST(request: NextRequest) {
|
||||
accountId: e.accountId,
|
||||
uid: e.uid ?? null,
|
||||
title: (e.title ?? '').trim() || '(no title)',
|
||||
start: eventStart(e).toISOString(),
|
||||
end: eventEnd(e).toISOString(),
|
||||
...(e.showWithoutTime
|
||||
? allDayRange(e)
|
||||
: { start: eventStart(e).toISOString(), end: eventEnd(e).toISOString() }),
|
||||
allDay: !!e.showWithoutTime,
|
||||
status: e.status ?? null,
|
||||
color: e.color || cal?.color || null,
|
||||
|
||||
@@ -240,6 +240,28 @@ describe.each<StalwartVersion>(['0.16', '1.0'])('calendar-agenda route on Stalwa
|
||||
expect(events[0]).toMatchObject({ start: '2026-09-26T08:00:00.000Z', end: '2026-09-26T09:00:00.000Z' });
|
||||
});
|
||||
|
||||
it(`hands out an all-day event as its own date, not midnight in the server's zone, on Stalwart ${version}`, async () => {
|
||||
// As the server answered live: 0.16 drops showWithoutTime and names
|
||||
// Etc/UTC, 1.0 keeps showWithoutTime and names no zone.
|
||||
const holiday = {
|
||||
id: 'maaaaac', baseEventId: 'c', '@type': 'Event', uid: 'c@example.com', title: 'Holiday',
|
||||
start: '2026-09-27T00:00:00', duration: 'P1D',
|
||||
utcStart: '2026-09-27T00:00:00Z', utcEnd: '2026-09-28T00:00:00Z', calendarIds: { cal: true },
|
||||
...(version === '1.0' ? { showWithoutTime: true } : { timeZone: 'Etc/UTC' }),
|
||||
};
|
||||
answer = ({ method, args }) => method === 'CalendarEvent/get' && (args.ids as string[]).includes('maaaaac')
|
||||
? ['CalendarEvent/get', { list: [OCCURRENCES.maaaaab, holiday] }]
|
||||
: undefined;
|
||||
const { events } = await (await POST(makeReq())).json() as { events: Array<Record<string, unknown>> };
|
||||
|
||||
// The plugin files events under new Date(start) in the viewer's zone. An
|
||||
// instant for midnight in the server's zone put the holiday on 26
|
||||
// September for anyone west of the server; a date-time without an offset
|
||||
// is read as the viewer's own midnight.
|
||||
expect(events[1]).toMatchObject({ allDay: true, start: '2026-09-27T00:00:00', end: '2026-09-28T00:00:00' });
|
||||
expect(new Date(events[1].start as string).getDate()).toBe(27);
|
||||
});
|
||||
|
||||
it(`answers 502 when a get fails on Stalwart ${version}`, async () => {
|
||||
answer = ({ method }) => method === 'CalendarEvent/get' ? ['error', { type: 'serverFail', description: 'Store unavailable' }] : undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user