import { createContext , ReactNode , useContext , useState , useEffect , useRef , useMemo } from 'react' ; import { useLocation , useHistory } from 'react-router-dom' ; import { c } from 'ttag' ; import { FeatureCode , useApi , useFeatures , useGetMessageCounts , useGetUserKeys , useNotifications , useOnLogout , useSubscribeEventManager , useUser , useWelcomeFlags , } from '@proton/components' ; import { wait } from '@proton/shared/lib/helpers/promise' ; import { EVENT_ACTIONS , SECOND } from '@proton/shared/lib/constants' ; import { EVENT_ERRORS } from '@proton/shared/lib/errors' ; import { hasBit } from '@proton/shared/lib/helpers/bitset' ; import { isMobile } from '@proton/shared/lib/helpers/browser' ; import { isPaid } from '@proton/shared/lib/user/helpers' ; import { useGetMessageKeys } from '../hooks/message/useGetMessageKeys' ; import { Event } from '../models/event' ; import { Element } from '../models/element' ; import { EncryptedSearch , EncryptedSearchFunctions , ESDBStatus , ESStatus , IncrementSearch , HighlightMetadata , HighlightString , LastEmail , ESMessage , IsSearchResult , ESCache , ResumeIndexing , } from '../models/encryptedSearch' ; import { defaultESCache , defaultESStatus , PAGE_SIZE } from '../constants' ; import { indexKeyExists , getNumMessagesDB , wasIndexingDone , getESTotal , setESCurrent , canUseES , deleteESDB , removeESFlags , removeES , setES , esSentryReport , checkNewUserID , parseSearchParams , resetSort , increaseNumPauses , addESTimestamp , getES , isDBReadyAfterBuilding , } from '../helpers/encryptedSearch/esUtils' ; import { buildDB , getIndexKey , initialiseDB , sendIndexingMetrics } from '../helpers/encryptedSearch/esBuild' ; import { hybridSearch , normaliseSearchParams , sendSearchingMetrics , shouldOnlySortResults , uncachedSearch , } from '../helpers/encryptedSearch/esSearch' ; import { cacheDB , refreshESCache } from '../helpers/encryptedSearch/esCache' ; import { checkIsDBLimited , correctDecryptionErrors , getEventFromLS , syncMessageEvents , } from '../helpers/encryptedSearch/esSync' ; import { queryEvents } from '../helpers/encryptedSearch/esAPI' ; import { highlightJSX , insertMarks } from '../helpers/encryptedSearch/esHighlight' ; const EncryptedSearchContext = createContext < EncryptedSearchFunctions > ( null as any ) ; export const useEncryptedSearchContext = ( ) => useContext ( EncryptedSearchContext ) ; interface Props { children ? : ReactNode ; } const EncryptedSearchProvider = ( { children } ) => { const location = useLocation ( ) ; const history = useHistory ( ) ; const getUserKeys = useGetUserKeys ( ) ; const getMessageKeys = useGetMessageKeys ( ) ; const api = useApi ( ) ; const [ user ] = useUser ( ) ; const { ID : userID } = user ; const { createNotification } = useNotifications ( ) ; const getMessageCounts = useGetMessageCounts ( ) ; const { isSearch , page } = parseSearchParams ( location ) ; const [ { get : getESFeature } , { update : updateSpotlightES } ] = useFeatures ( [ FeatureCode . EnabledEncryptedSearch , FeatureCode . SpotlightEncryptedSearch , ] ) ; const [ welcomeFlags ] = useWelcomeFlags ( ) ; const [ esStatus , setESStatus ] = useState < ESStatus > ( defaultESStatus ) ; const esCacheRef = useRef < ESCache > ( defaultESCache ) ; const abortIndexingRef = useRef < AbortController > ( new AbortController ( ) ) ; const abortSearchingRef = useRef < AbortController > ( new AbortController ( ) ) ; const progressRecorderRef = useRef < [ number , number ] > ( [ 0 , 0 ] ) ; const syncingEventsRef = useRef < Promise < void > > ( Promise . resolve ( ) ) ; const pageRef = useRef < number > ( 0 ) ; /**
     * Chain several synchronisations to account for events being fired when
     * previous ones are still being processed
     */ const addSyncing = async ( callback ) => { syncingEventsRef . current = syncingEventsRef . current . then ( ( ) => callback ( ) ) ; } ; /**
     * Delete localStorage blobs and IDB
     */ const esDelete = async ( inputUserID ? ) => { abortIndexingRef . current . abort ( ) ; abortSearchingRef . current . abort ( ) ; const uID = inputUserID || userID ; removeESFlags ( uID ) ; setESStatus ( ( ) => defaultESStatus ) ; return deleteESDB ( uID ) ; } ; /**
     * Abort ongoing operations if the user logs out
     */ useOnLogout ( async ( ) => { abortIndexingRef . current . abort ( ) ; abortSearchingRef . current . abort ( ) ; } ) ; /**
     * Notify the user the DB is deleted. Typically this is needed if the key is no
     * longer usable to decrypt it
     */ const dbCorruptError = async ( inputUserID ? ) => { await esDelete ( inputUserID ) ; createNotification ( { text : c ( 'Error' ) . t `Please activate your content search again` , type : 'error' , } ) ; } ; /**
     * Store progress of operations
     */ const recordProgress = ( progress , total ) => { progressRecorderRef . current = [ progress , total ] ; } ; /**
     * Give access to progress made during operations
     */ const getProgressRecorderRef = ( ) => { return progressRecorderRef ; } ; /**
     * Report the status of IndexedDB
     */ const getESDBStatus = ( ) => { const { dbExists , isBuilding , isDBLimited , esEnabled , isRefreshing , isSearchPartial , isSearching , isCaching , dropdownOpened , } = esStatus ; const { isCacheLimited } = esCacheRef . current ; const esDBStatus = { dbExists , isBuilding , isDBLimited , esEnabled , isCacheLimited , isRefreshing , isSearchPartial , isSearching , isCaching , dropdownOpened , } ; return esDBStatus ; } ; /**
     * Once encrypted search is active, allow to switch back to server-side metadata search
     */ const toggleEncryptedSearch = ( ) => { const currentOption = esStatus . esEnabled ; setESStatus ( ( esStatus ) => { return { ... esStatus , esEnabled : ! currentOption , } ; } ) ; if ( currentOption ) { abortSearchingRef . current . abort ( ) ; removeES . Enabled ( userID ) ; } else { const { isSearch } = parseSearchParams ( history . location ) ; if ( isSearch ) { resetSort ( history ) ; } setES . Enabled ( userID ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , temporaryToggleOff : false , } ; } ) ; } void canUseES ( userID ) . then ( ( isIDBIntact ) => { if ( ! isIDBIntact ) { void dbCorruptError ( ) ; } } ) ; } ; /**
     * Temporarily disable ES for times when search is too slow and a server-side
     * search is needed. The toggle is set automatically back on upon exiting search mode
     */ const setTemporaryToggleOff = ( ) => { setESStatus ( ( esStatus ) => { return { ... esStatus , temporaryToggleOff : true , } ; } ) ; toggleEncryptedSearch ( ) ; } ; /**
     * Cache IndexedDB
     */ const cacheIndexedDB = async ( ) => { const { esEnabled , dbExists , cachedIndexKey , isCaching } = esStatus ; if ( dbExists && esEnabled ) { const indexKey = cachedIndexKey || ( await getIndexKey ( getUserKeys , userID ) ) ; const isIDBIntact = await canUseES ( userID ) ; if ( ! indexKey || ! isIDBIntact ) { return dbCorruptError ( ) ; } const { isCacheReady } = esCacheRef . current ; if ( isCaching || isCacheReady ) { return ; } setESStatus ( ( esStatus ) => { return { ... esStatus , isCaching : true , cachedIndexKey : indexKey , } ; } ) ; await cacheDB ( indexKey , userID , esCacheRef ) ; esCacheRef . current . isCacheReady = true ; setESStatus ( ( esStatus ) => { return { ... esStatus , isCaching : false , } ; } ) ; } } ; /**
     * Keep IndexedDB in sync with new events
     */ const syncIndexedDB = async ( event , indexKey , recordProgressLocal ? ) => { const { Messages , Addresses } = event ; const isUpdatingMessageContent = typeof recordProgressLocal !== 'undefined' ; const attemptReDecryption = Addresses && Addresses . some ( ( AddressEvent ) => AddressEvent . Action === EVENT_ACTIONS . UPDATE ) ; if ( attemptReDecryption ) { recordProgress ( 0 , 0 ) ; if ( ! isUpdatingMessageContent ) { setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : true , } ; } ) ; } await correctDecryptionErrors ( userID , indexKey , api , getMessageKeys , recordProgress , esCacheRef ) ; if ( ! isUpdatingMessageContent ) { setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : false , } ; } ) ; } } if ( ! Messages || ! Messages . length ) { return ; } if ( attemptReDecryption ) { recordProgress ( 0 , 0 ) ; } const { labelID , permanentResults , setElementsCache } = esStatus ; const { isSearch , searchParameters , filterParameter , sortParameter } = parseSearchParams ( history . location ) ; const normalisedSearchParams = normaliseSearchParams ( searchParameters , labelID , filterParameter , sortParameter ) ; const searchChanged = await syncMessageEvents ( Messages , userID , esCacheRef , permanentResults , isSearch , api , getMessageKeys , indexKey , normalisedSearchParams , recordProgressLocal ) ; if ( searchChanged ) { setElementsCache ( permanentResults , pageRef . current ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , permanentResults , } ; } ) ; } } ; /**
     * Check whether a refresh is needed. Temporarily disabled to assess impact
     */ /**
     * Conclude any type of syncing routine
     */ const finaliseSyncing = async ( event , indexKey ) => { if ( event . EventID ) { setES . Event ( userID , event . EventID ) ; } await refreshESCache ( userID , indexKey , esCacheRef ) ; const messageCounts = await getMessageCounts ( ) ; const isDBLimited = await checkIsDBLimited ( userID , messageCounts ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : false , isDBLimited , } ; } ) ; } ; /**
     * Catch up with all changes contained in the given event
     */ const catchUpFromEvent = async ( indexKey , currentEvent ) : Promise < void > => { try { await syncIndexedDB ( currentEvent , indexKey ) ; } catch ( error ) { esSentryReport ( 'catchUpFromEvent: syncIndexedDB' , { error } ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : false , } ; } ) ; return catchUpFromEvent ( indexKey , currentEvent ) ; } return finaliseSyncing ( currentEvent , indexKey ) ; } ; /**
     * Fetch all events since a previously stored one
     */ const catchUpFromLS = async ( indexKey , initialEvent ) : Promise < void > => { const isIDBIntact = await canUseES ( userID ) ; if ( ! isIDBIntact ) { return dbCorruptError ( ) ; } setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : true , } ; } ) ; recordProgress ( 0 , 0 ) ; recordProgress ( 0 , 0 ) ; let keepSyncing = true ; let index = 0 ; const newEventsToCheck = [ initialEvent ] ; while ( keepSyncing ) { try { const nextEventToCheck = newEventsToCheck [ index ++ ] ; const newEventToCheck = await queryEvents ( api , nextEventToCheck . EventID ) ; if ( ! newEventToCheck || ! newEventToCheck . EventID ) { throw new Error ( 'No event found' ) ; } keepSyncing = nextEventToCheck . More === 1 || newEventToCheck . EventID !== nextEventToCheck . EventID ; if ( keepSyncing ) { newEventsToCheck . push ( newEventToCheck ) ; } } catch ( error ) { esSentryReport ( 'catchUpFromLS: queryEvents' , { error } ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : false , } ; } ) ; return catchUpFromLS ( indexKey , initialEvent ) ; } } const Total = newEventsToCheck . reduce ( ( accumulator , event ) => { const addressEvents = ( event . Addresses || [ ] ) . filter ( ( AddressEvent ) => AddressEvent . Action === EVENT_ACTIONS . UPDATE ) . length ; return accumulator + ( event . Messages ?. length || 0 ) + addressEvents ; } , 0 ) ; const recordProgressLocal = ( ) => { const [ current ] = progressRecorderRef . current ; recordProgress ( current + 1 , Total ) ; } ; try { for ( const eventToCheck of newEventsToCheck ) { await syncIndexedDB ( eventToCheck , indexKey , recordProgressLocal ) ; } } catch ( error ) { esSentryReport ( 'catchUpFromLS: syncIndexedDB' , { error } ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isRefreshing : false , } ; } ) ; return catchUpFromLS ( indexKey , initialEvent ) ; } return finaliseSyncing ( newEventsToCheck [ newEventsToCheck . length - 1 ] , indexKey ) ; } ; /**
     * Pause a running indexig
     */ const pauseIndexing = async ( ) => { abortIndexingRef . current . abort ( ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isBuilding : false , } ; } ) ; setES . Pause ( userID ) ; increaseNumPauses ( userID ) ; addESTimestamp ( userID , 'stop' ) ; const isIDBIntact = await canUseES ( userID ) ; if ( ! isIDBIntact ) { return dbCorruptError ( ) ; } } ; /**
     * Resume an existing indexing operation or start one anew
     */ const resumeIndexing = async ( { notify , isRefreshed } = { notify : true , isRefreshed : false } ) => { const isResumed = getES . Pause ( userID ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , esEnabled : true , } ; } ) ; setES . Enabled ( userID ) ; const showError = ( notSupported ? ) => { if ( notify ) { createNotification ( { text : notSupported ? c ( 'Error' ) . t `Content search cannot be enabled in this browser. Please quit private browsing mode or use another browser.` : c ( 'Error' ) . t `A problem occurred, please try again` , type : 'error' , } ) ; } setESStatus ( ( ) => defaultESStatus ) ; } ; removeES . Pause ( userID ) ; abortIndexingRef . current = new AbortController ( ) ; let indexKey ; if ( ! indexKeyExists ( userID ) && ! isResumed ) { const { notSupported , indexKey : newIndexKey } = await initialiseDB ( userID , getUserKeys , api , isRefreshed || false ) ; if ( ! newIndexKey ) { return showError ( notSupported ) ; } indexKey = newIndexKey ; } else { const existingIndexKey = await getIndexKey ( getUserKeys , userID ) ; if ( ! existingIndexKey ) { return dbCorruptError ( ) ; } indexKey = existingIndexKey ; } const totalMessages = getESTotal ( userID ) ; const mailboxEmpty = totalMessages === 0 ; recordProgress ( await getNumMessagesDB ( userID ) , totalMessages ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isBuilding : true , } ; } ) ; let success = mailboxEmpty ; while ( ! success ) { const currentMessages = await getNumMessagesDB ( userID ) ; recordProgress ( currentMessages , totalMessages ) ; const recordProgressLocal = ( progress ) => { const newProgress = currentMessages + progress ; setESCurrent ( userID , newProgress ) ; recordProgress ( newProgress , totalMessages ) ; } ; success = await buildDB ( userID , indexKey , getMessageKeys , api , abortIndexingRef , recordProgressLocal ) ; if ( abortIndexingRef . current . signal . aborted || getES . Pause ( userID ) ) { return ; } const isIDBIntact = await canUseES ( userID ) ; if ( ! isIDBIntact ) { return dbCorruptError ( ) ; } await wait ( 2 * SECOND ) ; } await sendIndexingMetrics ( api , userID ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isBuilding : false , } ; } ) ; const currentEvent = await getEventFromLS ( userID , api ) ; if ( ! currentEvent || hasBit ( currentEvent . Refresh , EVENT_ERRORS . MAIL ) ) { return dbCorruptError ( ) ; } const catchUpPromise = catchUpFromLS ( indexKey , currentEvent ) ; void addSyncing ( ( ) => catchUpPromise ) ; await catchUpPromise ; removeES . Progress ( userID ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , dbExists : true , } ; } ) ; if ( notify ) { createNotification ( { text : c ( 'Success' ) . t `Message content search activated` , } ) ; } } ; /**
     * Execute an encrypted search
     */ const encryptedSearch = async ( labelID , setCache ) => { abortSearchingRef . current . abort ( ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , isSearching : false , isSearchPartial : false , } ; } ) ; const t1 = performance . now ( ) ; const { dbExists , esEnabled , previousNormSearchParams , permanentResults , isSearchPartial : wasSearchPartial , cachedIndexKey , isCaching , isFirstSearch , } = esStatus ; if ( ! dbExists || ! esEnabled ) { return false ; } const isIDBIntact = await canUseES ( userID ) ; if ( ! isIDBIntact ) { return dbCorruptError ( ) . then ( ( ) => false ) ; } abortSearchingRef . current = new AbortController ( ) ; if ( ! isCaching && ! esCacheRef . current . isCacheReady ) { void cacheIndexedDB ( ) ; } const { searchParameters , filterParameter , sortParameter } = parseSearchParams ( history . location ) ; const normalisedSearchParams = normaliseSearchParams ( searchParameters , labelID , filterParameter , sortParameter ) ; if ( ! wasSearchPartial && previousNormSearchParams ) { const shouldSortOnly = shouldOnlySortResults ( normalisedSearchParams , previousNormSearchParams ) ; if ( shouldSortOnly ) { setCache ( permanentResults , pageRef . current ) ; return true ; } } setESStatus ( ( esStatus ) => { return { ... esStatus , isSearching : true , isSearchPartial : true , isFirstSearch : false , } ; } ) ; const controlledSetCache = ( Elements ) => { if ( ! abortSearchingRef . current . signal . aborted ) { setCache ( Elements , pageRef . current ) ; } } ; let searchResults = [ ] ; let isSearchPartial = false ; let lastEmail ; try { ( { searchResults , isSearchPartial , lastEmail } = await hybridSearch ( esCacheRef , normalisedSearchParams , cachedIndexKey , getUserKeys , userID , controlledSetCache , abortSearchingRef ) ) ; } catch ( error ) { esSentryReport ( 'encryptedSearch: hybridSearch' , { error } ) ; if ( error . message === 'Key not found' ) { return dbCorruptError ( ) . then ( ( ) => false ) ; } throw error ; } if ( ! abortSearchingRef . current . signal . aborted ) { setESStatus ( ( esStatus ) => { return { ... esStatus , permanentResults : searchResults , labelID , setElementsCache : setCache , lastEmail , previousNormSearchParams : normalisedSearchParams , page : 0 , isSearchPartial , isSearching : false , } ; } ) ; setCache ( searchResults , pageRef . current ) ; const t2 = performance . now ( ) ; void sendSearchingMetrics ( api , userID , esCacheRef . current . cacheSize , Math . ceil ( t2 - t1 ) , isFirstSearch , esCacheRef . current . isCacheLimited ) ; } return true ; } ; /**
     * Increase the number of results in case the cache is limited as the user changes page
     */ const incrementSearch = async ( page , setElementsCache , shouldLoadMore ) => { const { isCacheLimited } = esCacheRef . current ; const { dbExists , esEnabled , labelID , permanentResults , lastEmail , isSearchPartial , cachedIndexKey , isSearching , } = esStatus ; if ( ! dbExists || ! esEnabled || ! isCacheLimited || abortSearchingRef . current . signal . aborted || isSearching ) { return ; } const lastFilledPage = Math . ceil ( permanentResults . length / PAGE_SIZE ) - 1 ; if ( page <= pageRef . current || page < lastFilledPage - 1 ) { return ; } const { isSearch , searchParameters , filterParameter , sortParameter } = parseSearchParams ( history . location ) ; if ( ! isSearch || ! isSearchPartial ) { return ; } const neededResults = PAGE_SIZE * ( lastFilledPage + 2 ) ; let messageLimit = 0 ; if ( permanentResults . length < neededResults ) { messageLimit = neededResults - permanentResults . length ; } else { return ; } if ( shouldLoadMore ) { messageLimit += PAGE_SIZE ; } setESStatus ( ( esStatus ) => { return { ... esStatus , isSearching : true , } ; } ) ; const normalisedSearchParams = normaliseSearchParams ( searchParameters , labelID , filterParameter , sortParameter ) ; const indexKey = cachedIndexKey || ( await getIndexKey ( getUserKeys , userID ) ) ; if ( ! indexKey ) { return dbCorruptError ( ) ; } const searchOutput = await uncachedSearch ( userID , indexKey , normalisedSearchParams , { messageLimit , beginOrder : lastEmail ?. Order , lastEmailTime : lastEmail ?. Time , abortSearchingRef , } ) ; if ( ! abortSearchingRef . current . signal . aborted ) { permanentResults . push ( ... searchOutput . resultsArray ) ; const newIsSearchPartial = ! ! searchOutput . lastEmail ; setESStatus ( ( esStatus ) => { return { ... esStatus , permanentResults , lastEmail : searchOutput . lastEmail , isSearchPartial : newIsSearchPartial , isSearching : false , } ; } ) ; setElementsCache ( permanentResults , page ) ; } } ; /**
     * Check whether to highlight keywords upon opening a result of any search (both server-side and encrypted)
     */ const shouldHighlight = ( ) => { const { isSearch , searchParameters } = parseSearchParams ( history . location ) ; if ( ! isSearch ) { return false ; } const { labelID } = esStatus ; const { normalisedKeywords } = normaliseSearchParams ( searchParameters , labelID ) ; if ( ! normalisedKeywords ) { return false ; } return true ; } ; /**
     * Highlight keywords in body. Return a string with the new body
     */ const highlightString = ( content , setAutoScroll ) => { const { searchParameters } = parseSearchParams ( history . location ) ; const { labelID } = esStatus ; const { normalisedKeywords } = normaliseSearchParams ( searchParameters , labelID ) ; if ( ! normalisedKeywords ) { return content ; } return insertMarks ( content , normalisedKeywords , setAutoScroll ) ; } ; /**
     * Highlight keywords in metadata. Return the JSX element to be rendered
     */ const highlightMetadata = ( metadata , isBold , trim ) => { const { searchParameters } = parseSearchParams ( history . location ) ; const { labelID } = esStatus ; const { normalisedKeywords } = normaliseSearchParams ( searchParameters , labelID ) ; if ( ! normalisedKeywords ) { return { numOccurrences : 0 , resultJSX : < span > { metadata } < /span>, } ; } return highlightJSX ( metadata , normalisedKeywords , isBold , trim ) ; } ; /**
     * Check whether a message is part of the current search results
     */ const isSearchResult = ( ID ) => { const { dbExists , esEnabled , permanentResults } = esStatus ; const { isSearch } = parseSearchParams ( history . location ) ; if ( ! ( dbExists && esEnabled && isSearch ) ) { return false ; } return permanentResults . findIndex ( ( result ) => result . ID === ID ) !== - 1 ; } ; /**
     * Remove the index and restart ES by creating a new one from scratch
     */ const restartIndexing = async ( ) => { await esDelete ( ) ; return resumeIndexing ( { isRefreshed : true } ) ; } ; /**
     * Open the advanced search dropdown
     */ const openDropdown = ( ) => { setESStatus ( ( esStatus ) => { return { ... esStatus , dropdownOpened : true , } ; } ) ; } ; /**
     * Close the advanced search dropdown
     */ const closeDropdown = ( ) => { setESStatus ( ( esStatus ) => { return { ... esStatus , dropdownOpened : false , } ; } ) ; } ; useSubscribeEventManager ( async ( event ) => { const { dbExists , cachedIndexKey } = esStatus ; if ( ! dbExists ) { return ; } const indexKey = cachedIndexKey || ( await getIndexKey ( getUserKeys , userID ) ) ; const isIDBIntact = await canUseES ( userID ) ; if ( ! indexKey || ! isIDBIntact ) { return dbCorruptError ( ) ; } if ( hasBit ( event . Refresh , EVENT_ERRORS . MAIL ) ) { return restartIndexing ( ) ; } void addSyncing ( ( ) => catchUpFromEvent ( indexKey , event ) ) ; } ) ; useEffect ( ( ) => { pageRef . current = page ; } , [ page ] ) ; useEffect ( ( ) => { if ( ! isSearch ) { const { temporaryToggleOff } = esStatus ; abortSearchingRef . current . abort ( ) ; setESStatus ( ( esStatus ) => { return { ... esStatus , permanentResults : defaultESStatus . permanentResults , setElementsCache : defaultESStatus . setElementsCache , labelID : defaultESStatus . labelID , lastEmail : defaultESStatus . lastEmail , previousNormSearchParams : defaultESStatus . previousNormSearchParams , isSearchPartial : defaultESStatus . isSearchPartial , isSearching : defaultESStatus . isSearching , temporaryToggleOff : false , } ; } ) ; if ( temporaryToggleOff ) { toggleEncryptedSearch ( ) ; } } } , [ isSearch ] ) ; useEffect ( ( ) => { const run = async ( ) => { try { const esFeature = await getESFeature < boolean > ( ) ; if ( welcomeFlags . isWelcomeFlow && ! isMobile ( ) && ! ! esFeature . Value && ! indexKeyExists ( userID ) && isPaid ( user ) ) { await updateSpotlightES ( false ) ; return await resumeIndexing ( { notify : false } ) ; } const indexKeys = await checkNewUserID ( getUserKeys ) ; if ( indexKeys . length === 0 ) { return ; } if ( indexKeys . length > 1 ) { await Promise . all ( indexKeys . map ( async ( { userID } ) => { await esDelete ( userID ) ; } ) ) ; return await restartIndexing ( ) ; } const { userID : storedUserID , indexKey } = indexKeys [ 0 ] ; if ( storedUserID !== userID ) { await esDelete ( storedUserID ) ; return await restartIndexing ( ) ; } const isIDBIntact = await canUseES ( userID ) ; if ( ! indexKey || ! isIDBIntact ) { return await dbCorruptError ( ) ; } setESStatus ( ( esStatus ) => { return { ... esStatus , dbExists : wasIndexingDone ( userID ) , esEnabled : getES . Enabled ( userID ) , } ; } ) ; if ( ! isDBReadyAfterBuilding ( userID ) ) { if ( ! getES . Pause ( userID ) ) { await resumeIndexing ( ) ; } return ; } const currentEvent = await getEventFromLS ( userID , api ) ; if ( ! currentEvent ) { return await dbCorruptError ( ) ; } if ( hasBit ( currentEvent . Refresh , EVENT_ERRORS . MAIL ) ) { return await restartIndexing ( ) ; } void addSyncing ( ( ) => catchUpFromLS ( indexKey , currentEvent ) ) ; } catch ( error ) { console . log ( 'ES effect error' , error ) ; } } ; void run ( ) ; } , [ ] ) ; const esFunctions = useMemo ( ( ) => ( { encryptedSearch , cacheIndexedDB , getESDBStatus , toggleEncryptedSearch , resumeIndexing , pauseIndexing , getProgressRecorderRef , incrementSearch , highlightString , highlightMetadata , shouldHighlight , isSearchResult , esDelete , openDropdown , closeDropdown , setTemporaryToggleOff , } ) , [ userID , esStatus ] ) ; return < EncryptedSearchContext . Provider value = { esFunctions } > { children } < /EncryptedSearchContext.Provider>; } ; export default EncryptedSearchProvider ;