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';
8 useCalendarModelEventManager,
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';
21 CalendarEventsEventManager,
22 } from '@proton/shared/lib/interfaces/calendar/EventManager';
24 import { getESCallbacks } from '../helpers/encryptedSearch/calendarESCallbacks';
26 buildRecurrenceIDsMap,
27 processCalendarEvents,
29 updateRecurrenceIDsMap,
30 } from '../helpers/encryptedSearch/esUtils';
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>({
45 isLibraryInitialized: false,
49 export const useEncryptedSearchLibrary = () => useContext(EncryptedSearchLibraryContext);
52 calendarIDs: string[];
54 hasReactivatedCalendarsRef: React.MutableRefObject<boolean>;
57 const EncryptedSearchLibraryProvider = ({ calendarIDs, hasReactivatedCalendarsRef, children }: Props) => {
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(
78 [api, calendarIDs, history, userID, getCalendarEventRaw]
81 const handleIndexingMetrics = (metrics: IndexingMetrics) => {
82 const { numInterruptions, numPauses, indexTime, totalItems } = metrics;
83 const roundedIndexTimeInMinutes = Math.round(indexTime / MINUTE_IN_SECONDS);
85 void sendTelemetryReport({
87 measurementGroup: TelemetryMeasurementGroups.calendarEncryptedSearch,
88 event: TelemetryCalendarEvents.enable_encrypted_search,
89 values: { numInterruptions, numPauses, indexTime: roundedIndexTimeInMinutes, totalItems },
93 const esLibraryFunctions = useEncryptedSearch<ESCalendarMetadata, ESCalendarSearchParams, ESCalendarContent>({
96 onMetadataIndexed: handleIndexingMetrics,
98 const { isConfigFromESDBLoaded, cachedIndexKey, esEnabled } = esLibraryFunctions.esStatus;
102 return coreSubscribe(
109 Calendars?: CalendarEventManager[];
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.
115 const esEvent = await processCoreEvents({
124 return esLibraryFunctions.handleEvent(esEvent);
127 }, [esLibraryFunctions, esCallbacks, getCalendarEventRaw]);
131 return calendarSubscribe(
136 CalendarModelEventID,
138 CalendarModelEventID: string;
139 CalendarEvents?: CalendarEventsEventManager[];
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.
145 const esEvent = await processCalendarEvents(
149 CalendarModelEventID,
153 if (cachedIndexKey) {
154 await updateRecurrenceIDsMap(userID, cachedIndexKey, CalendarEvents, setRecurrenceIDsMap);
157 return esLibraryFunctions.handleEvent(esEvent);
160 }, [calendarIDs, esLibraryFunctions, esCallbacks, cachedIndexKey]);
163 if (!isConfigFromESDBLoaded) {
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;
178 void initializeLibrary();
179 }, [isConfigFromESDBLoaded]);
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
188 if (!isLibraryInitialized || !esEnabled) {
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()));
196 void computeRecurrenceIDsMap();
198 // getting the cache is a heavy operation; with these dependencies it should be run just once per app lifecycle
199 [isLibraryInitialized, esEnabled]
202 const value = { ...esLibraryFunctions, isLibraryInitialized, recurrenceIDsMap };
204 return <EncryptedSearchLibraryContext.Provider value={value}>{children}</EncryptedSearchLibraryContext.Provider>;
207 export default EncryptedSearchLibraryProvider;