From 80250d32bd109f8f27c5192a1db15136b703c1de Mon Sep 17 00:00:00 2001 From: Curious George <101997181+curiousgeorgios@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:36:07 +0300 Subject: [PATCH] fix: timezone parsing issues for calendars (#21665) * fix timezone parsing issues for calendars * fix: maintain backward compatibility with other timezone specification methods accepting the cubic-dev-ai as implement backward compatibility with different event ICS event types Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update Cubic comment * Fix timezone handling in ICS feed calendar service - Fix VTIMEZONE selection to find matching timezone instead of using first one - Improve TZID parameter extraction from DTSTART property using jCal array - Fix precedence: DTSTART TZID > standalone TZID > UTC > fallback - Resolves issue where Europe/Brussels events showed wrong times in Europe/Paris * remove comments * remove comments --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> --- .../ics-feedcalendar/lib/CalendarService.ts | 25 ++++++++++++++++--- packages/lib/CalendarService.ts | 24 +++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts index 7a09df9761..a7261cc364 100644 --- a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts +++ b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts @@ -159,11 +159,17 @@ export default class ICSFeedCalendarService implements Calendar { // if (vevent?.getFirstPropertyValue("transp") === "TRANSPARENT") return; const event = new ICAL.Event(vevent); + const dtstartProperty = vevent.getFirstProperty("dtstart"); + const tzidFromDtstart = dtstartProperty ? (dtstartProperty as any).jCal[1].tzid : undefined; + const dtstart: { [key: string]: string } | undefined = vevent?.getFirstPropertyValue("dtstart"); const timezone = dtstart ? dtstart["timezone"] : undefined; // We check if the dtstart timezone is in UTC which is actually represented by Z instead, but not recognized as that in ICAL.js as UTC const isUTC = timezone === "Z"; - const tzid: string | undefined = vevent?.getFirstPropertyValue("tzid") || isUTC ? "UTC" : timezone; + + // Fix precedence: prioritize TZID from DTSTART property, then standalone TZID, then UTC, then fallback + const tzid: string | undefined = + tzidFromDtstart || vevent?.getFirstPropertyValue("tzid") || (isUTC ? "UTC" : timezone); // In case of icalendar, when only tzid is available without vtimezone, we need to add vtimezone explicitly to take care of timezone diff if (!vcalendar.getFirstSubcomponent("vtimezone")) { const timezoneToUse = tzid || userTimeZone; @@ -192,7 +198,16 @@ export default class ICSFeedCalendarService implements Calendar { console.error("No timezone found"); } } - const vtimezone = vcalendar.getFirstSubcomponent("vtimezone"); + + let vtimezone = null; + if (tzid) { + const allVtimezones = vcalendar.getAllSubcomponents("vtimezone"); + vtimezone = allVtimezones.find((vtz) => vtz.getFirstPropertyValue("tzid") === tzid); + } + + if (!vtimezone) { + vtimezone = vcalendar.getFirstSubcomponent("vtimezone"); + } // mutate event to consider travel time applyTravelDuration(event, getTravelDurationInSeconds(vevent)); @@ -263,9 +278,11 @@ export default class ICSFeedCalendarService implements Calendar { event.endDate = event.endDate.convertToZone(zone); } + const finalStartISO = dayjs(event.startDate.toJSDate()).toISOString(); + const finalEndISO = dayjs(event.endDate.toJSDate()).toISOString(); return events.push({ - start: dayjs(event.startDate.toJSDate()).toISOString(), - end: dayjs(event.endDate.toJSDate()).toISOString(), + start: finalStartISO, + end: finalEndISO, }); }); }); diff --git a/packages/lib/CalendarService.ts b/packages/lib/CalendarService.ts index 11dd86d8c7..7b738e69de 100644 --- a/packages/lib/CalendarService.ts +++ b/packages/lib/CalendarService.ts @@ -393,11 +393,16 @@ export default abstract class BaseCalendarService implements Calendar { if (vevent?.getFirstPropertyValue("transp") === "TRANSPARENT") return; const event = new ICAL.Event(vevent); + const dtstartProperty = vevent.getFirstProperty("dtstart"); + const tzidFromDtstart = dtstartProperty ? (dtstartProperty as any).jCal[1].tzid : undefined; const dtstart: { [key: string]: string } | undefined = vevent?.getFirstPropertyValue("dtstart"); const timezone = dtstart ? dtstart["timezone"] : undefined; // We check if the dtstart timezone is in UTC which is actually represented by Z instead, but not recognized as that in ICAL.js as UTC const isUTC = timezone === "Z"; - const tzid: string | undefined = vevent?.getFirstPropertyValue("tzid") || isUTC ? "UTC" : timezone; + + // Fix precedence: prioritize TZID from DTSTART property, then standalone TZID, then UTC, then fallback + const tzid: string | undefined = + tzidFromDtstart || vevent?.getFirstPropertyValue("tzid") || (isUTC ? "UTC" : timezone); // In case of icalendar, when only tzid is available without vtimezone, we need to add vtimezone explicitly to take care of timezone diff if (!vcalendar.getFirstSubcomponent("vtimezone")) { const timezoneToUse = tzid || userTimeZone; @@ -426,7 +431,16 @@ export default abstract class BaseCalendarService implements Calendar { console.error("No timezone found"); } } - const vtimezone = vcalendar.getFirstSubcomponent("vtimezone"); + + let vtimezone = null; + if (tzid) { + const allVtimezones = vcalendar.getAllSubcomponents("vtimezone"); + vtimezone = allVtimezones.find((vtz) => vtz.getFirstPropertyValue("tzid") === tzid); + } + + if (!vtimezone) { + vtimezone = vcalendar.getFirstSubcomponent("vtimezone"); + } // mutate event to consider travel time applyTravelDuration(event, getTravelDurationInSeconds(vevent, this.log)); @@ -498,9 +512,11 @@ export default abstract class BaseCalendarService implements Calendar { event.endDate = event.endDate.convertToZone(zone); } + const finalStartISO = dayjs(event.startDate.toJSDate()).toISOString(); + const finalEndISO = dayjs(event.endDate.toJSDate()).toISOString(); return events.push({ - start: dayjs(event.startDate.toJSDate()).toISOString(), - end: dayjs(event.endDate.toJSDate()).toISOString(), + start: finalStartISO, + end: finalEndISO, }); }); });