From 648a4465739e8049bc54ecf332e89eb2577e56d5 Mon Sep 17 00:00:00 2001 From: ivan-lednev Date: Sat, 28 Sep 2024 19:38:50 +0200 Subject: [PATCH] fix: do not show deleted recurrences of events Resolves #418, #530 --- ...-recurring-with-exception-and-location.txt | 41 +++++++++++++++++++ src/constants.ts | 2 +- src/remote-calendars.test.ts | 29 +++++++++++-- src/util/ical.ts | 28 +++++++++---- 4 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 fixtures/google-recurring-with-exception-and-location.txt diff --git a/fixtures/google-recurring-with-exception-and-location.txt b/fixtures/google-recurring-with-exception-and-location.txt new file mode 100644 index 000000000..682fdfa41 --- /dev/null +++ b/fixtures/google-recurring-with-exception-and-location.txt @@ -0,0 +1,41 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:test-ical +X-WR-TIMEZONE:Europe/Warsaw +BEGIN:VTIMEZONE +TZID:Europe/Warsaw +X-LIC-LOCATION:Europe/Warsaw +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:GMT+2 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:GMT+1 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Warsaw:20240927T180000 +DTEND;TZID=Europe/Warsaw:20240927T200000 +RRULE:FREQ=DAILY +EXDATE;TZID=Europe/Warsaw:20240928T180000 +DTSTAMP:20240928T171122Z +UID:2g8p75vbi3ojunbnejha28opic@google.com +CREATED:20240928T170958Z +LAST-MODIFIED:20240928T171047Z +LOCATION:Rynek Główny\, 31-422 Kraków\, Польша +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:recurring +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/src/constants.ts b/src/constants.ts index 01d380ee2..940e8b59c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,7 +7,7 @@ export const editContextKey = "editContext"; export const dateRangeContextKey = "dateRangeContext"; export const errorContextKey = "errorContext"; export const defaultDayFormat = "YYYY-MM-DD"; -export const originalRecurrenceDayKeyFormat = "YYYY-MM-DD"; +export const icalDayKeyFormat = "YYYY-MM-DD"; export const defaultDayFormatForLuxon = "yyyy-MM-dd"; export const clockSeparator = "--"; export const defaultDurationMinutes = 30; diff --git a/src/remote-calendars.test.ts b/src/remote-calendars.test.ts index 1ccd3ffd3..9c8197983 100644 --- a/src/remote-calendars.test.ts +++ b/src/remote-calendars.test.ts @@ -26,12 +26,12 @@ function getMockRequest(): Mock { return jest.requireMock("obsidian").request; } -function createStore() { +function createStore({ visibleDays = [moment("2024-09-26")] } = {}) { const syncTrigger = writable({}); const store = useDayToEventOccurences({ isOnline: writable(true), - visibleDays: writable([moment("2024-09-26")]), + visibleDays: writable(visibleDays), syncTrigger, settings: writable({ ...defaultSettingsForTests, @@ -92,7 +92,30 @@ test("Falls back on previous values if fetching a calendar fails", async () => { }); }); -test.todo("Deleted recurrences don't show up as tasks"); +test("Deleted recurrences don't show up as tasks", async () => { + getMockRequest().mockReturnValue( + getIcalFixture("google-recurring-with-exception-and-location"), + ); + + const { store } = createStore({ + visibleDays: [moment("2024-09-27"), moment("2024-09-28")], + }); + + await waitFor(() => { + expect(get(store)).toEqual({ + "2024-09-27": { + noTime: [], + withTime: [ + expect.objectContaining({ + summary: "recurring", + }), + ], + }, + }); + }); +}); + +test.todo("Location gets passed to an event"); test.todo("Yearly recurrences do not show up every month"); diff --git a/src/util/ical.ts b/src/util/ical.ts index 5d4f34a9a..9858be7fa 100644 --- a/src/util/ical.ts +++ b/src/util/ical.ts @@ -2,11 +2,7 @@ import moment, { type Moment } from "moment"; import { tz } from "moment-timezone"; import ical, { type AttendeePartStat } from "node-ical"; -import { - fallbackPartStat, - noTitle, - originalRecurrenceDayKeyFormat, -} from "../constants"; +import { fallbackPartStat, noTitle, icalDayKeyFormat } from "../constants"; import type { RemoteTask, WithTime } from "../task-types"; import type { WithIcalConfig } from "../types"; @@ -25,14 +21,24 @@ export function canHappenAfter(icalEvent: ical.VEvent, date: Date) { ); } -function hasRecurrenceOverride(icalEvent: ical.VEvent, date: Date) { +function hasRecurrenceOverrideForDate(icalEvent: ical.VEvent, date: Date) { if (!icalEvent.recurrences) { return false; } - const dateKey = moment(date).format(originalRecurrenceDayKeyFormat); + return Object.hasOwn(icalEvent.recurrences, getIcalDayKey(date)); +} + +function getIcalDayKey(date: Date) { + return moment(date).format(icalDayKeyFormat); +} + +function hasExceptionForDate(icalEvent: ical.VEvent, date: Date) { + if (!icalEvent.exdate) { + return false; + } - return Object.hasOwn(icalEvent.recurrences, dateKey); + return Object.keys(icalEvent.exdate).includes(getIcalDayKey(date)); } export function icalEventToTasks( @@ -53,7 +59,11 @@ export function icalEventToTasks( const recurrences = icalEvent.rrule ?.between(startOfDay, endOfDay) - .filter((date) => !hasRecurrenceOverride(icalEvent, date)) + .filter( + (date) => + !hasRecurrenceOverrideForDate(icalEvent, date) && + !hasExceptionForDate(icalEvent, date), + ) .map((date) => icalEventToTask(icalEvent, date)); return [...recurrences, ...recurrenceOverrides];