diff --git a/static/js/upcoming.js b/static/js/upcoming.js index f031b4d..b172d83 100644 --- a/static/js/upcoming.js +++ b/static/js/upcoming.js @@ -1,5 +1,17 @@ import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js"; +/** + * Read the URL of an event. + * + * ICAL.Event does not expose the URL property, so read it from the component. + * + * @param {ICAL.Event} event The event to read the URL of + * @returns {string} The URL, empty when the event has none + */ +function eventUrl(event) { + return event.component.getFirstPropertyValue("url") ?? ""; +} + /** * Parse an ICS calendar and return upcoming event occurrences. * @@ -21,12 +33,15 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) { for (const component of calendar.getAllSubcomponents("vevent")) { const event = new ICAL.Event(component); - if (!event.startDate) { + // Occurrences modified via RECURRENCE-ID are reached through the event they + // belong to, listing them here as well would show them twice. + if (event.isRecurrenceException()) { continue; } - // ICAL.Event does not expose the URL property, so read it from the component. - const url = component.getFirstPropertyValue("url") ?? ""; + if (!event.startDate) { + continue; + } if (event.isRecurring()) { const iterator = event.iterator(); @@ -44,15 +59,16 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) { break; } - // Details resolve the times of occurrences overridden by RECURRENCE-ID. + // Details resolve time, name and URL of an occurrence that was + // modified via RECURRENCE-ID. const details = event.getOccurrenceDetails(occurrence); // A running event stays listed until it is over, so filter on its end. if (details.endDate.toJSDate() > now) { events.push({ start: details.startDate.toJSDate(), - name: event.summary ?? "", - url, + name: details.item.summary ?? "", + url: eventUrl(details.item), }); } } @@ -63,7 +79,7 @@ function getUpcomingEvents(icsText, now, maxEvents, maxDays) { events.push({ start, name: event.summary ?? "", - url, + url: eventUrl(event), }); } }