Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / containers / EncryptedSearchLibraryProvider.tsx
blob12287d0e6f8f3d5cb6828cf4f1f7f43999352c37
1 import type { ReactNode } from 'react';
2 import { createContext, useContext, useEffect, useMemo, useState } from 'react';
3 import { useHistory } from 'react-router-dom';
5 import { useUser } from '@proton/account/user/hooks';
6 import {
7     useApi,
8     useCalendarModelEventManager,
9     useContactEmailsCache,
10     useEventManager,
11     useGetCalendarEventRaw,
12 } from '@proton/components';
13 import type { IndexingMetrics } from '@proton/encrypted-search';
14 import { defaultESContext, useEncryptedSearch } from '@proton/encrypted-search';
15 import { TelemetryCalendarEvents, TelemetryMeasurementGroups } from '@proton/shared/lib/api/telemetry';
16 import { MINUTE_IN_SECONDS } from '@proton/shared/lib/constants';
17 import { sendTelemetryReport } from '@proton/shared/lib/helpers/metrics';
18 import type { SimpleMap } from '@proton/shared/lib/interfaces';
19 import type {
20     CalendarEventManager,
21     CalendarEventsEventManager,
22 } from '@proton/shared/lib/interfaces/calendar/EventManager';
24 import { getESCallbacks } from '../helpers/encryptedSearch/calendarESCallbacks';
25 import {
26     buildRecurrenceIDsMap,
27     processCalendarEvents,
28     processCoreEvents,
29     updateRecurrenceIDsMap,
30 } from '../helpers/encryptedSearch/esUtils';
31 import type {
32     ESCalendarContent,
33     ESCalendarMetadata,
34     ESCalendarSearchParams,
35     EncryptedSearchFunctionsCalendar,
36 } from '../interfaces/encryptedSearch';
38 interface EncryptedSearchLibrary extends EncryptedSearchFunctionsCalendar {
39     isLibraryInitialized: boolean;
40     recurrenceIDsMap: SimpleMap<number[]>;
43 const EncryptedSearchLibraryContext = createContext<EncryptedSearchLibrary>({
44     ...defaultESContext,
45     isLibraryInitialized: false,
46     recurrenceIDsMap: {},
47 });
49 export const useEncryptedSearchLibrary = () => useContext(EncryptedSearchLibraryContext);
51 interface Props {
52     calendarIDs: string[];
53     children?: ReactNode;
54     hasReactivatedCalendarsRef: React.MutableRefObject<boolean>;
57 const EncryptedSearchLibraryProvider = ({ calendarIDs, hasReactivatedCalendarsRef, children }: Props) => {
58     const api = useApi();
59     const history = useHistory();
60     const [{ ID: userID }] = useUser();
61     const { contactEmailsMap } = useContactEmailsCache();
62     const getCalendarEventRaw = useGetCalendarEventRaw(contactEmailsMap);
63     const { subscribe: coreSubscribe } = useEventManager();
64     const { subscribe: calendarSubscribe } = useCalendarModelEventManager();
66     const [isLibraryInitialized, setIsLibraryInitialized] = useState(false);
67     const [recurrenceIDsMap, setRecurrenceIDsMap] = useState<SimpleMap<number[]>>({});
69     const esCallbacks = useMemo(
70         () =>
71             getESCallbacks({
72                 api,
73                 calendarIDs,
74                 history,
75                 userID,
76                 getCalendarEventRaw,
77             }),
78         [api, calendarIDs, history, userID, getCalendarEventRaw]
79     );
81     const handleIndexingMetrics = (metrics: IndexingMetrics) => {
82         const { numInterruptions, numPauses, indexTime, totalItems } = metrics;
83         const roundedIndexTimeInMinutes = Math.round(indexTime / MINUTE_IN_SECONDS);
85         void sendTelemetryReport({
86             api,
87             measurementGroup: TelemetryMeasurementGroups.calendarEncryptedSearch,
88             event: TelemetryCalendarEvents.enable_encrypted_search,
89             values: { numInterruptions, numPauses, indexTime: roundedIndexTimeInMinutes, totalItems },
90         });
91     };
93     const esLibraryFunctions = useEncryptedSearch<ESCalendarMetadata, ESCalendarSearchParams, ESCalendarContent>({
94         refreshMask: 1,
95         esCallbacks,
96         onMetadataIndexed: handleIndexingMetrics,
97     });
98     const { isConfigFromESDBLoaded, cachedIndexKey, esEnabled } = esLibraryFunctions.esStatus;
100     // Core loop
101     useEffect(() => {
102         return coreSubscribe(
103             async ({
104                 Calendars = [],
105                 Refresh = 0,
106                 EventID,
107             }: {
108                 EventID: string;
109                 Calendars?: CalendarEventManager[];
110                 Refresh?: number;
111             }) => {
112                 /**
113                  * If we have `More` core events to handle, the application itself will take care of the pagination so this handler will automatically get called next.
114                  */
115                 const esEvent = await processCoreEvents({
116                     userID,
117                     Calendars,
118                     Refresh,
119                     EventID,
120                     api,
121                     getCalendarEventRaw,
122                 });
124                 return esLibraryFunctions.handleEvent(esEvent);
125             }
126         );
127     }, [esLibraryFunctions, esCallbacks, getCalendarEventRaw]);
129     // Calendars loop
130     useEffect(() => {
131         return calendarSubscribe(
132             calendarIDs,
133             async ({
134                 CalendarEvents = [],
135                 Refresh = 0,
136                 CalendarModelEventID,
137             }: {
138                 CalendarModelEventID: string;
139                 CalendarEvents?: CalendarEventsEventManager[];
140                 Refresh?: number;
141             }) => {
142                 /**
143                  * If we have `More` calendar events (e.g: in case of a large import) to handle, the application itself will take care of the pagination so this handler will automatically get called next.
144                  */
145                 const esEvent = await processCalendarEvents(
146                     CalendarEvents,
147                     Refresh,
148                     userID,
149                     CalendarModelEventID,
150                     api,
151                     getCalendarEventRaw
152                 );
153                 if (cachedIndexKey) {
154                     await updateRecurrenceIDsMap(userID, cachedIndexKey, CalendarEvents, setRecurrenceIDsMap);
155                 }
157                 return esLibraryFunctions.handleEvent(esEvent);
158             }
159         );
160     }, [calendarIDs, esLibraryFunctions, esCallbacks, cachedIndexKey]);
162     useEffect(() => {
163         if (!isConfigFromESDBLoaded) {
164             return;
165         }
167         const initializeLibrary = async () => {
168             // TODO: error handling
169             await esLibraryFunctions.initializeES();
170             setIsLibraryInitialized(true);
172             if (hasReactivatedCalendarsRef.current) {
173                 await esLibraryFunctions.correctDecryptionErrors();
174                 hasReactivatedCalendarsRef.current = false;
175             }
176         };
178         void initializeLibrary();
179     }, [isConfigFromESDBLoaded]);
181     useEffect(
182         () => {
183             /**
184              * We can't run this logic in the previous useEffect because we have to wait for React to update the
185              * encrypted search state before running cacheIndexedDB.
186              * The library does not offer a way to decouple internal logic from React state logic
187              */
188             if (!isLibraryInitialized || !esEnabled) {
189                 return;
190             }
191             const computeRecurrenceIDsMap = async () => {
192                 // we have to initialize the cache early to build the recurrenceIDs map, to be done in the other useEffect
193                 await esLibraryFunctions.cacheIndexedDB();
194                 setRecurrenceIDsMap(buildRecurrenceIDsMap(esLibraryFunctions.getCache()));
195             };
196             void computeRecurrenceIDsMap();
197         },
198         // getting the cache is a heavy operation; with these dependencies it should be run just once per app lifecycle
199         [isLibraryInitialized, esEnabled]
200     );
202     const value = { ...esLibraryFunctions, isLibraryInitialized, recurrenceIDsMap };
204     return <EncryptedSearchLibraryContext.Provider value={value}>{children}</EncryptedSearchLibraryContext.Provider>;
207 export default EncryptedSearchLibraryProvider;