1 import { useCallback } from 'react';
3 import { getUnixTime } from 'date-fns';
5 import { useGetCalendars } from '@proton/calendar/calendars/hooks';
6 import { useApi } from '@proton/components';
7 import { CacheType } from '@proton/redux-utilities';
8 import { getEvent, updateMember } from '@proton/shared/lib/api/calendars';
9 import { MAXIMUM_DATE, MINIMUM_DATE } from '@proton/shared/lib/calendar/constants';
10 import { getMemberAndAddress } from '@proton/shared/lib/calendar/members';
11 import getRecurrenceIdValueFromTimestamp from '@proton/shared/lib/calendar/recurrence/getRecurrenceIdValueFromTimestamp';
12 import { getOccurrences } from '@proton/shared/lib/calendar/recurrence/recurring';
13 import { getIsPropertyAllDay, getPropertyTzid } from '@proton/shared/lib/calendar/vcalHelper';
14 import { getRecurrenceIdDate } from '@proton/shared/lib/calendar/veventHelper';
15 import { addMilliseconds, isSameDay } from '@proton/shared/lib/date-fns-utc';
16 import { toUTCDate } from '@proton/shared/lib/date/timezone';
17 import type { Address } from '@proton/shared/lib/interfaces';
18 import type { CalendarEvent, VcalVeventComponent, VisualCalendar } from '@proton/shared/lib/interfaces/calendar';
20 import parseMainEventData from '../containers/calendar/event/parseMainEventData';
21 import getAllEventsByUID from '../containers/calendar/getAllEventsByUID';
23 interface HandlerProps {
24 calendars: VisualCalendar[];
26 calendarID: string | null;
27 eventID: string | null;
28 recurrenceId: string | null;
29 onGoToEvent: (eventData: CalendarEvent, eventComponent: VcalVeventComponent) => void;
31 eventData: CalendarEvent,
32 eventComponent: VcalVeventComponent,
33 occurrence: { localStart: Date; occurrenceNumber: number }
35 onEventNotFoundError: () => void;
36 onOtherError?: () => void;
39 export const useOpenEvent = () => {
41 const getCalendars = useGetCalendars();
55 if (!calendarID || !eventID) {
56 return onEventNotFoundError();
58 const calendar = calendars.find(({ ID }) => ID === calendarID);
60 return onEventNotFoundError();
62 if (!calendar.Display) {
63 const [{ ID: memberID }] = getMemberAndAddress(addresses, calendar.Members);
65 ...updateMember(calendarID, memberID, { Display: 1 }),
68 await getCalendars({ cache: CacheType.None });
71 const result = await api<{ Event: CalendarEvent }>({
72 ...getEvent(calendarID, eventID),
75 const parsedEvent = parseMainEventData(result.Event);
78 throw new Error('Missing parsed event');
81 if (!parsedEvent.rrule) {
82 return onGoToEvent(result.Event, parsedEvent);
86 const occurrences = getOccurrences({ component: parsedEvent, maxCount: 1 });
87 if (!occurrences.length) {
88 return onEventNotFoundError();
90 const [firstOccurrence] = occurrences;
91 return onGoToOccurrence(result.Event, parsedEvent, firstOccurrence);
94 const parsedRecurrenceID = parseInt(recurrenceId || '', 10);
97 !Number.isInteger(parsedRecurrenceID) ||
98 parsedRecurrenceID < getUnixTime(MINIMUM_DATE) ||
99 parsedRecurrenceID > getUnixTime(MAXIMUM_DATE)
101 return onEventNotFoundError();
104 const eventsByUID = await getAllEventsByUID(api, calendarID, parsedEvent.uid.value);
106 const { dtstart } = parsedEvent;
107 const localRecurrenceID = toUTCDate(
108 getRecurrenceIdValueFromTimestamp(
110 getIsPropertyAllDay(dtstart),
111 getPropertyTzid(dtstart) || 'UTC'
115 const isTargetOccurrenceEdited = eventsByUID.some((recurrence) => {
116 const parsedEvent = parseMainEventData(recurrence);
120 const recurrenceID = getRecurrenceIdDate(parsedEvent);
124 // Here a date-time comparison could be used instead, but since the recurrence id parameter can be easily tweaked to change
125 // e.g. the seconds and since a recurring granularity less than daily is not allowed, just compare the day
126 return isSameDay(localRecurrenceID, recurrenceID);
129 const maxStart = addMilliseconds(localRecurrenceID, 1);
130 const untilTargetOccurrences = getOccurrences({
131 component: parsedEvent,
135 if (!untilTargetOccurrences.length || isTargetOccurrenceEdited) {
136 // Target occurrence could not be found, fall back to the first generated occurrence
137 const initialOccurrences = getOccurrences({ component: parsedEvent, maxCount: 1 });
138 if (!initialOccurrences.length) {
139 return onEventNotFoundError();
141 const [firstOccurrence] = initialOccurrences;
142 return onGoToOccurrence(result.Event, parsedEvent, firstOccurrence);
144 const [firstOccurrence] = untilTargetOccurrences;
145 const targetOccurrence = untilTargetOccurrences[untilTargetOccurrences.length - 1];
146 // Target recurrence could not be expanded to
147 if (!isSameDay(localRecurrenceID, targetOccurrence.localStart)) {
148 return onGoToOccurrence(result.Event, parsedEvent, firstOccurrence);
150 return onGoToOccurrence(result.Event, parsedEvent, targetOccurrence);
152 if (e.status >= 400 && e.status <= 499) {
153 return onEventNotFoundError();
155 return onOtherError?.();