Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / hooks / useOpenEvent.ts
blobca3fe1382934e17e7f2caf5a74ee3f18e58ae128
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[];
25     addresses: Address[];
26     calendarID: string | null;
27     eventID: string | null;
28     recurrenceId: string | null;
29     onGoToEvent: (eventData: CalendarEvent, eventComponent: VcalVeventComponent) => void;
30     onGoToOccurrence: (
31         eventData: CalendarEvent,
32         eventComponent: VcalVeventComponent,
33         occurrence: { localStart: Date; occurrenceNumber: number }
34     ) => void;
35     onEventNotFoundError: () => void;
36     onOtherError?: () => void;
39 export const useOpenEvent = () => {
40     const api = useApi();
41     const getCalendars = useGetCalendars();
43     return useCallback(
44         async ({
45             calendars,
46             addresses,
47             calendarID,
48             eventID,
49             recurrenceId,
50             onGoToEvent,
51             onGoToOccurrence,
52             onEventNotFoundError,
53             onOtherError,
54         }: HandlerProps) => {
55             if (!calendarID || !eventID) {
56                 return onEventNotFoundError();
57             }
58             const calendar = calendars.find(({ ID }) => ID === calendarID);
59             if (!calendar) {
60                 return onEventNotFoundError();
61             }
62             if (!calendar.Display) {
63                 const [{ ID: memberID }] = getMemberAndAddress(addresses, calendar.Members);
64                 await api({
65                     ...updateMember(calendarID, memberID, { Display: 1 }),
66                     silence: true,
67                 }).catch(() => {});
68                 await getCalendars({ cache: CacheType.None });
69             }
70             try {
71                 const result = await api<{ Event: CalendarEvent }>({
72                     ...getEvent(calendarID, eventID),
73                     silence: true,
74                 });
75                 const parsedEvent = parseMainEventData(result.Event);
77                 if (!parsedEvent) {
78                     throw new Error('Missing parsed event');
79                 }
81                 if (!parsedEvent.rrule) {
82                     return onGoToEvent(result.Event, parsedEvent);
83                 }
85                 if (!recurrenceId) {
86                     const occurrences = getOccurrences({ component: parsedEvent, maxCount: 1 });
87                     if (!occurrences.length) {
88                         return onEventNotFoundError();
89                     }
90                     const [firstOccurrence] = occurrences;
91                     return onGoToOccurrence(result.Event, parsedEvent, firstOccurrence);
92                 }
94                 const parsedRecurrenceID = parseInt(recurrenceId || '', 10);
96                 if (
97                     !Number.isInteger(parsedRecurrenceID) ||
98                     parsedRecurrenceID < getUnixTime(MINIMUM_DATE) ||
99                     parsedRecurrenceID > getUnixTime(MAXIMUM_DATE)
100                 ) {
101                     return onEventNotFoundError();
102                 }
104                 const eventsByUID = await getAllEventsByUID(api, calendarID, parsedEvent.uid.value);
106                 const { dtstart } = parsedEvent;
107                 const localRecurrenceID = toUTCDate(
108                     getRecurrenceIdValueFromTimestamp(
109                         parsedRecurrenceID,
110                         getIsPropertyAllDay(dtstart),
111                         getPropertyTzid(dtstart) || 'UTC'
112                     ).value
113                 );
115                 const isTargetOccurrenceEdited = eventsByUID.some((recurrence) => {
116                     const parsedEvent = parseMainEventData(recurrence);
117                     if (!parsedEvent) {
118                         return false;
119                     }
120                     const recurrenceID = getRecurrenceIdDate(parsedEvent);
121                     if (!recurrenceID) {
122                         return false;
123                     }
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);
127                 });
129                 const maxStart = addMilliseconds(localRecurrenceID, 1);
130                 const untilTargetOccurrences = getOccurrences({
131                     component: parsedEvent,
132                     maxCount: 10000000,
133                     maxStart,
134                 });
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();
140                     }
141                     const [firstOccurrence] = initialOccurrences;
142                     return onGoToOccurrence(result.Event, parsedEvent, firstOccurrence);
143                 }
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);
149                 }
150                 return onGoToOccurrence(result.Event, parsedEvent, targetOccurrence);
151             } catch (e: any) {
152                 if (e.status >= 400 && e.status <= 499) {
153                     return onEventNotFoundError();
154                 }
155                 return onOtherError?.();
156             }
157         },
158         [api, getCalendars]
159     );