diff options
author | Marco <marco.martinoli@protonmail.com> | 2022-10-06 11:46:51 +0300 |
---|---|---|
committer | Marco <marco.martinoli@protonmail.com> | 2022-10-06 11:46:51 +0300 |
commit | bf144fb6ebfb4e1090fbbb8317d7d454302482e6 (patch) | |
tree | bd08dbe7ec00a139275753133dc4116017e3b21b | |
parent | 9740ea1050c8dacae9bf1c9012055047951cabe9 (diff) |
Revert "Add partial ES"
This reverts commit 9fcbcc0a6219fd896c16bbbc8ae136ed9db41b74.
18 files changed, 64 insertions, 766 deletions
diff --git a/applications/mail/src/app/MainContainer.tsx b/applications/mail/src/app/MainContainer.tsx index 97419a74ef..e35412aa55 100644 --- a/applications/mail/src/app/MainContainer.tsx +++ b/applications/mail/src/app/MainContainer.tsx @@ -40,7 +40,6 @@ const MainContainer = () => { FeatureCode.UsedMailMobileApp, FeatureCode.SpyTrackerProtection, FeatureCode.ContextFiltering, - FeatureCode.PartialEncryptedSearch, ]); // Service Worker registration diff --git a/applications/mail/src/app/components/header/search/AdvancedSearchFields/EncryptedSearchField.tsx b/applications/mail/src/app/components/header/search/AdvancedSearchFields/EncryptedSearchField.tsx index afb5fa3a41..60b0f1bcda 100644 --- a/applications/mail/src/app/components/header/search/AdvancedSearchFields/EncryptedSearchField.tsx +++ b/applications/mail/src/app/components/header/search/AdvancedSearchFields/EncryptedSearchField.tsx @@ -11,10 +11,8 @@ import { Tooltip, classnames, useModalState, - useUser, } from '@proton/components'; import { ESIndexingState } from '@proton/encrypted-search'; -import { isPaid } from '@proton/shared/lib/user/helpers'; import { useEncryptedSearchContext } from '../../../../containers/EncryptedSearchProvider'; import { formatSimpleDate } from '../../../../helpers/date'; @@ -24,7 +22,6 @@ interface Props { } const EncryptedSearchField = ({ esState }: Props) => { - const [user] = useUser(); const { enableEncryptedSearch, enableContentSearch, @@ -32,7 +29,6 @@ const EncryptedSearchField = ({ esState }: Props) => { pauseIndexing, toggleEncryptedSearch, getProgressRecorderRef, - activateContentSearch, } = useEncryptedSearchContext(); const { isEnablingContentSearch, @@ -42,16 +38,13 @@ const EncryptedSearchField = ({ esState }: Props) => { isEnablingEncryptedSearch, isPaused, contentIndexingDone, - activatingPartialES, } = getESDBStatus(); const { esProgress, oldestTime, totalIndexingItems, estimatedMinutes, currentProgressValue } = esState; const [enableESModalProps, setEnableESModalOpen] = useModalState(); // Switches - const showProgress = - isPaid(user) && - (isEnablingContentSearch || isPaused || (contentIndexingDone && isRefreshing && !activatingPartialES)); + const showProgress = isEnablingContentSearch || isPaused || (contentIndexingDone && isRefreshing); const showSubTitleSection = contentIndexingDone && !isRefreshing && isDBLimited && !isEnablingEncryptedSearch; const isEstimating = estimatedMinutes === 0 && (totalIndexingItems === 0 || esProgress !== totalIndexingItems); const showToggle = isEnablingContentSearch || isPaused || contentIndexingDone; @@ -77,19 +70,8 @@ const EncryptedSearchField = ({ esState }: Props) => { : c('Info').t`Turn on to search the content of your messages`; } - const esExplanation = isPaid(user) - ? c('Info') - .t`This action will download all messages so they can be searched locally. Clearing your browser data will disable this option.` - : c('Info') - .t`This action will download the most recent messages so they can be searched locally. Clearing your browser data will disable this option.`; - - const esActivationTooltip = c('Info').t`The local database is being prepared`; - const esActivationLoading = isEnablingEncryptedSearch || activatingPartialES; - const esActivationButton = ( - <Button onClick={() => setEnableESModalOpen(true)} loading={esActivationLoading}> - {c('Action').t`Activate`} - </Button> - ); + const esExplanation = c('Info') + .t`This action will download all messages so they can be searched locally. Clearing your browser data will disable this option.`; const esCTA = showToggle ? ( <Tooltip title={esToggleTooltip}> @@ -103,12 +85,10 @@ const EncryptedSearchField = ({ esState }: Props) => { /> </span> </Tooltip> - ) : esActivationLoading ? ( - <Tooltip title={esActivationTooltip}> - <span>{esActivationButton}</span> - </Tooltip> ) : ( - esActivationButton + <Button onClick={() => setEnableESModalOpen(true)} loading={isEnablingEncryptedSearch}> + {c('Action').t`Activate`} + </Button> ); const info = <Info questionMark title={esExplanation} />; const esHeader = showToggle ? ( @@ -174,7 +154,7 @@ const EncryptedSearchField = ({ esState }: Props) => { const handleEnableES = async () => { enableESModalProps.onClose(); - void enableEncryptedSearch().then(() => activateContentSearch()); + void enableEncryptedSearch().then(() => enableContentSearch()); }; return ( diff --git a/applications/mail/src/app/components/header/search/MailSearch.tsx b/applications/mail/src/app/components/header/search/MailSearch.tsx index 622de02f2f..5fba53a748 100644 --- a/applications/mail/src/app/components/header/search/MailSearch.tsx +++ b/applications/mail/src/app/components/header/search/MailSearch.tsx @@ -7,7 +7,7 @@ import { TopNavbarListItemSearchButton, generateUID, useAddresses, - useFeatures, + useFeature, useFolders, useLabels, useMailSettings, @@ -47,15 +47,12 @@ const MailSearch = ({ breakpoints, labelID, location }: Props) => { const [, loadingLabels] = useLabels(); const [, loadingFolders] = useFolders(); const [, loadingAddresses] = useAddresses(); - const [{ loading: loadingScheduledFeature }, { feature: partialES }] = useFeatures([ - FeatureCode.ScheduledSend, - FeatureCode.PartialEncryptedSearch, - ]); + const { loading: loadingScheduledFeature } = useFeature(FeatureCode.ScheduledSend); const { getESDBStatus, cacheMailContent, closeDropdown } = useEncryptedSearchContext(); const { dropdownOpened } = getESDBStatus(); const esState = useEncryptedSearchToggleState(isOpen); - const showEncryptedSearch = !isMobile() && (!!isPaid(user) || (!!partialES && partialES.Value)); + const showEncryptedSearch = !isMobile() && !!isPaid(user); // Show more from inside AdvancedSearch to persist the state when the overlay is closed const { state: showMore, toggle: toggleShowMore } = useToggle(false); diff --git a/applications/mail/src/app/components/list/ItemColumnLayout.tsx b/applications/mail/src/app/components/list/ItemColumnLayout.tsx index 2a6f01b6d8..0ea5dbe1bf 100644 --- a/applications/mail/src/app/components/list/ItemColumnLayout.tsx +++ b/applications/mail/src/app/components/list/ItemColumnLayout.tsx @@ -25,7 +25,6 @@ import ItemLabels from './ItemLabels'; import ItemLocation from './ItemLocation'; import ItemStar from './ItemStar'; import ItemUnread from './ItemUnread'; -import useEncryptedSearchItem from './useEncryptedSearchItem'; interface Props { labelID: string; @@ -95,10 +94,6 @@ const ItemColumnLayout = ({ numOccurrences ); - // For free users with limited content search, we want to make obvious that the third - // line in an item is the content - const { showContentLabel, contentLabel } = useEncryptedSearchItem(!!resultJSX); - const hasOnlyIcsAttachments = getHasOnlyIcsAttachments(element?.AttachmentInfo); const hasLabels = useMemo(() => { const allLabelIDs = Object.keys(getLabelIDs(element, labelID)); @@ -214,7 +209,6 @@ const ItemColumnLayout = ({ > <div className="item-subject flex-item-fluid flex flex-nowrap flex-align-items-center"> <span className="inline-block max-w100 text-ellipsis" title={bodyTitle}> - {showContentLabel && contentLabel} {resultJSX} </span> </div> diff --git a/applications/mail/src/app/components/list/ItemRowLayout.tsx b/applications/mail/src/app/components/list/ItemRowLayout.tsx index ceecb65ca7..0f84f86598 100644 --- a/applications/mail/src/app/components/list/ItemRowLayout.tsx +++ b/applications/mail/src/app/components/list/ItemRowLayout.tsx @@ -21,7 +21,6 @@ import ItemLabels from './ItemLabels'; import ItemLocation from './ItemLocation'; import ItemStar from './ItemStar'; import ItemUnread from './ItemUnread'; -import useEncryptedSearchItem from './useEncryptedSearchItem'; interface Props { isCompactView: boolean; @@ -89,10 +88,6 @@ const ItemRowLayout = ({ numOccurrences ); - // For free users with limited content search, we want to make obvious that the third - // line in an item is the content - const { showContentLabel, contentLabel } = useEncryptedSearchItem(!!resultJSX); - const hasOnlyIcsAttachments = getHasOnlyIcsAttachments(element?.AttachmentInfo); return ( @@ -137,7 +132,6 @@ const ItemRowLayout = ({ title={bodyTitle} aria-hidden="true" > - {showContentLabel && contentLabel} {resultJSX} </span> <span className="sr-only">{bodyTitle}</span> diff --git a/applications/mail/src/app/components/list/List.tsx b/applications/mail/src/app/components/list/List.tsx index d2fc80a1e5..1a810334d9 100644 --- a/applications/mail/src/app/components/list/List.tsx +++ b/applications/mail/src/app/components/list/List.tsx @@ -148,20 +148,12 @@ const List = ( // ES options: offer users the option to turn off ES if it's taking too long, and // enable/disable UI elements for incremental partial searches - const { - showESSlowToolbar, - loadingElement, - disableGoToLast, - useLoadingElement, - limitedContentElement, - showESLimitedContent, - limitedContentIndex, - } = useEncryptedSearchList({ + const { showESSlowToolbar, loadingElement, disableGoToLast, useLoadingElement } = useEncryptedSearchList( isSearch, loading, page, - total, - }); + total + ); const { draggedIDs, handleDragStart, handleDragEnd } = useItemsDraggable( elements, @@ -223,32 +215,28 @@ const List = ( <> {/* div needed here for focus management */} <div> - {showESLimitedContent && limitedContentIndex === -1 && limitedContentElement} {elements.map((element, index) => ( - <> - <Item - key={element.ID} - conversationMode={conversationMode} - isCompactView={isCompactView} - labelID={labelID} - loading={loading} - columnLayout={columnLayout} - elementID={elementID} - element={element} - checked={checkedIDs.includes(element.ID || '')} - onCheck={onCheckOne} - onClick={onClick} - onContextMenu={onContextMenu} - onDragStart={handleDragStart} - onDragEnd={handleDragEnd} - dragged={draggedIDs.includes(element.ID || '')} - index={index} - breakpoints={breakpoints} - onFocus={onFocus} - onBack={onBack} - /> - {showESLimitedContent && limitedContentIndex === index && limitedContentElement} - </> + <Item + key={element.ID} + conversationMode={conversationMode} + isCompactView={isCompactView} + labelID={labelID} + loading={loading} + columnLayout={columnLayout} + elementID={elementID} + element={element} + checked={checkedIDs.includes(element.ID || '')} + onCheck={onCheckOne} + onClick={onClick} + onContextMenu={onContextMenu} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + dragged={draggedIDs.includes(element.ID || '')} + index={index} + breakpoints={breakpoints} + onFocus={onFocus} + onBack={onBack} + /> ))} </div> diff --git a/applications/mail/src/app/components/list/useEncryptedSearchItem.tsx b/applications/mail/src/app/components/list/useEncryptedSearchItem.tsx deleted file mode 100644 index 5311cace83..0000000000 --- a/applications/mail/src/app/components/list/useEncryptedSearchItem.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { c } from 'ttag'; - -import { useUser } from '@proton/components'; -import { isPaid } from '@proton/shared/lib/user/helpers'; - -const useEncryptedSearchItem = (bodyContentExists: boolean) => { - const [user] = useUser(); - - const showContentLabel = !isPaid(user) && !!bodyContentExists; - const contentLabel = ( - <span className="text-italic"> - { - // translator: the word "content" refers to the body of messages and is shown to remark that the following text comes from searching it - c('Info').t`Content: ` - } - </span> - ); - - return { showContentLabel, contentLabel }; -}; - -export default useEncryptedSearchItem; diff --git a/applications/mail/src/app/components/list/useEncryptedSearchList.tsx b/applications/mail/src/app/components/list/useEncryptedSearchList.tsx index dafbfde8ef..1ebc0de9f3 100644 --- a/applications/mail/src/app/components/list/useEncryptedSearchList.tsx +++ b/applications/mail/src/app/components/list/useEncryptedSearchList.tsx @@ -1,35 +1,15 @@ import { useEffect, useState } from 'react'; -import { useStore } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { c } from 'ttag'; -import { AppLink, EllipsisLoader, Info, useUser } from '@proton/components'; +import { EllipsisLoader } from '@proton/components'; import { SECOND } from '@proton/shared/lib/constants'; -import { APPS } from '@proton/shared/lib/constants'; -import { isPaid } from '@proton/shared/lib/user/helpers'; -import { ES_BANNER_REF, PAGE_SIZE } from '../../constants'; import { useEncryptedSearchContext } from '../../containers/EncryptedSearchProvider'; -import { sort } from '../../helpers/elements'; -import { parseSearchParams } from '../../helpers/encryptedSearch/esUtils'; -import { RootState } from '../../logic/store'; -import { ESMessage } from '../../models/encryptedSearch'; -interface Props { - isSearch: boolean; - loading: boolean; - page: number; - total: number; -} - -const useEncryptedSearchList = ({ isSearch, loading, page, total }: Props) => { - const history = useHistory(); - const [user] = useUser(); - const store = useStore<RootState>(); +const useEncryptedSearchList = (isSearch: boolean, loading: boolean, page: number, total: number) => { const { getESDBStatus } = useEncryptedSearchContext(); - - const { dbExists, esEnabled, isSearchPartial, isCacheLimited, isSearching, contentIndexingDone } = getESDBStatus(); + const { dbExists, esEnabled, isSearchPartial, isCacheLimited, isSearching } = getESDBStatus(); const [esTimer, setESTimer] = useState<NodeJS.Timeout>(setTimeout(() => {})); const [esTimerExpired, setESTimerExpired] = useState<boolean>(false); @@ -48,94 +28,6 @@ const useEncryptedSearchList = ({ isSearch, loading, page, total }: Props) => { </div> ); - const { esSearchParams } = parseSearchParams(history.location); - let showESLimitedContent = - isSearch && - !isESLoading && - !isPaid(user) && - contentIndexingDone && - esEnabled && - !!esSearchParams && - // Since there are no keywords, a metadata search - // should never show the banner - !!esSearchParams.normalizedKeywords && - // There is no correct place to show the banner - // if the order is by size - esSearchParams.sort.sort === 'Time'; - - // Show an element inside the list of ES results to mark the end of messages - // whose content has been searched for free users with partial ES active - let limitedContentIndex = -1; - if (showESLimitedContent) { - // We find the position where to insert the banner - const elementsArray = sort( - Object.values(store.getState().elements.elements), - esSearchParams!.sort, - esSearchParams!.labelID - ); - - for (let i = 0; i < elementsArray.length; i++) { - if ( - esSearchParams!.sort.desc - ? !!(elementsArray[i] as ESMessage)?.decryptedBody - : !(elementsArray[i] as ESMessage)?.decryptedBody - ) { - limitedContentIndex = i; - } - } - - // If the banner should be placed in another page, we don't show anything - const shouldBeInCurrentPage = - limitedContentIndex === -1 || - (limitedContentIndex >= (page - 1) * PAGE_SIZE && limitedContentIndex < page * PAGE_SIZE); - if (!shouldBeInCurrentPage) { - showESLimitedContent = false; - } - } - - const limitedContentTitle = ( - <div className="mr1 ml1"> - <span className="text-bold mr0-5"> - {limitedContentIndex !== -1 - ? c('Info').t`End of search results from past 30 days` - : c('Info').t`No search results from past 30 days. Searching the rest of your inbox`} - </span> - <Info - questionMark - title={c('Tooltip') - .t`For messages older than 30 days, only the subject line and recipient list is searched`} - /> - </div> - ); - - const paidPlansButton = ( - <AppLink to={`/dashboard?ref=${ES_BANNER_REF}`} toApp={APPS.PROTONACCOUNT} className="text-bold"> - { - // translator: sentence appears when a free user has content search available only for most recent messages. Complete sentence example: "Content search of the entire inbox is available for paid plans." - c('Link').t`paid plan` - } - </AppLink> - ); - - const limitedContentText = ( - <div className="mr1 ml1"> - { - // translator: sentence appears when a free user has content search available only for most recent messages. Complete sentence example: "Content search of the entire inbox is available for paid plans." - c('Info').jt`Search full content of older messages with a ${paidPlansButton}` - } - </div> - ); - - const limitedContentElement = ( - <div - className="flex flex-nowrap flex-column flex-align-items-center flex-justify-center color-weak pt0-5 pb0-5 bg-strong" - key="ESLimitedContentElement" - > - {limitedContentTitle} - {limitedContentText} - </div> - ); - useEffect(() => { if (isESLoading) { setESTimer(() => @@ -149,15 +41,7 @@ const useEncryptedSearchList = ({ isSearch, loading, page, total }: Props) => { } }, [isESLoading]); - return { - showESSlowToolbar, - loadingElement, - disableGoToLast, - useLoadingElement, - limitedContentElement, - limitedContentIndex, - showESLimitedContent, - }; + return { showESSlowToolbar, loadingElement, disableGoToLast, useLoadingElement }; }; export default useEncryptedSearchList; diff --git a/applications/mail/src/app/constants.ts b/applications/mail/src/app/constants.ts index 4bdef9b777..f98ea65cd8 100644 --- a/applications/mail/src/app/constants.ts +++ b/applications/mail/src/app/constants.ts @@ -132,7 +132,6 @@ export const MAX_ELEMENT_LIST_LOAD_RETRIES = 3; export const defaultESMailStatus: ESDBStatusMail = { dropdownOpened: false, temporaryToggleOff: false, - activatingPartialES: false, lastContentTime: 0, }; export const defaultESContextMail: EncryptedSearchFunctionsMail = { @@ -142,10 +141,8 @@ export const defaultESContextMail: EncryptedSearchFunctionsMail = { setTemporaryToggleOff: () => {}, cacheMailContent: async () => {}, getESDBStatus: () => ({ ...defaultESContext.getESDBStatus(), ...defaultESMailStatus }), - activateContentSearch: async () => {}, }; export const MAIL_EVENTLOOP_NAME = 'core'; -export const ES_BANNER_REF = 'content_search_banner'; export const upsellRefLink = 'upsell_mail-banner-'; diff --git a/applications/mail/src/app/containers/EncryptedSearchProvider.tsx b/applications/mail/src/app/containers/EncryptedSearchProvider.tsx index 729946413d..25cd40a490 100644 --- a/applications/mail/src/app/containers/EncryptedSearchProvider.tsx +++ b/applications/mail/src/app/containers/EncryptedSearchProvider.tsx @@ -6,29 +6,14 @@ import { c } from 'ttag'; import { FeatureCode, useApi, - useFeatures, + useFeature, useGetMessageCounts, useGetUserKeys, - useNotifications, useSubscribeEventManager, useUser, useWelcomeFlags, } from '@proton/components'; -import { - ESProgress, - INDEXING_STATUS, - cacheMetadata, - checkRecoveryFormat, - checkVersionedESDB, - defaultESCache, - defaultESProgress, - esSentryReport, - getIndexKey, - getOldestCachedContentTimepoint, - readContentProgress, - useEncryptedSearch, - writeContentProgress, -} from '@proton/encrypted-search'; +import { checkVersionedESDB, getOldestCachedContentTimepoint, useEncryptedSearch } from '@proton/encrypted-search'; import { SECOND } from '@proton/shared/lib/constants'; import { EVENT_ERRORS } from '@proton/shared/lib/errors'; import { isMobile } from '@proton/shared/lib/helpers/browser'; @@ -36,15 +21,9 @@ import { isPaid } from '@proton/shared/lib/user/helpers'; import { defaultESContextMail, defaultESMailStatus } from '../constants'; import { getESHelpers, getItemInfo } from '../helpers/encryptedSearch/encryptedSearchMailHelpers'; -import { convertEventType, getTotal } from '../helpers/encryptedSearch/esSync'; +import { convertEventType } from '../helpers/encryptedSearch/esSync'; import { parseSearchParams } from '../helpers/encryptedSearch/esUtils'; import { migrate } from '../helpers/encryptedSearch/migration'; -import { - activatePartialES, - getWindowStart, - removeOldContent, - revertPartialESActivation, -} from '../helpers/encryptedSearch/partialES'; import { useGetMessageKeys } from '../hooks/message/useGetMessageKeys'; import { ESBaseMessage, @@ -70,11 +49,7 @@ const EncryptedSearchProvider = ({ children }: Props) => { const api = useApi(); const [user] = useUser(); const [welcomeFlags] = useWelcomeFlags(); - const [{ update: updateSpotlightES }, { get: getPartialES }] = useFeatures([ - FeatureCode.SpotlightEncryptedSearch, - FeatureCode.PartialEncryptedSearch, - ]); - const { createNotification } = useNotifications(); + const { update: updateSpotlightES } = useFeature(FeatureCode.SpotlightEncryptedSearch); const { isSearch, page } = parseSearchParams(history.location); const [esMailStatus, setESMailStatus] = useState<ESDBStatusMail>(defaultESMailStatus); @@ -89,12 +64,10 @@ const EncryptedSearchProvider = ({ children }: Props) => { history, }); - const successMessage = c('Success').t`Message content search activated`; - const esLibraryFunctions = useEncryptedSearch<ESBaseMessage, NormalizedSearchParams, ESMessageContent>({ refreshMask: EVENT_ERRORS.MAIL, esHelpers, - successMessage, + successMessage: c('Success').t`Message content search activated`, }); /** @@ -146,12 +119,6 @@ const EncryptedSearchProvider = ({ children }: Props) => { * built at page load) */ const cacheMailContent = async () => { - // Kill switch to control the release of partial ES - const partialES = await getPartialES(); - if (!isPaid(user) && !!partialES && !partialES.Value) { - return esLibraryFunctions.esDelete(); - } - const { dbExists, contentIndexingDone } = getESDBStatus(); // If content indexing is over, we can cache content @@ -172,58 +139,9 @@ const EncryptedSearchProvider = ({ children }: Props) => { }; /** - * Activate the full or partial content search depending - * on whether the user is paid or not - */ - const activateContentSearch = async () => { - if (isPaid(user)) { - return esLibraryFunctions.enableContentSearch(); - } - - setESMailStatus((esMailStatus) => ({ - ...esMailStatus, - activatingPartialES: true, - })); - - const esCacheRef = esLibraryFunctions.getESCache(); - try { - await activatePartialES(api, user, getUserKeys, esHelpers, esCacheRef); - - createNotification({ - text: successMessage, - }); - - // After activating partial ES, initializeES is called to set - // all the appropriate esDBStatus flags - await esLibraryFunctions.initializeES(false); - } catch (error: any) { - // If something goes wrong, we just need to "clean" the content - // part of IDB - await revertPartialESActivation(user.ID, esCacheRef); - - createNotification({ - text: c('Error').t`Something went wrong, please try again`, - type: 'error', - }); - void esSentryReport('activatePartialES', error); - } - - setESMailStatus((esMailStatus) => ({ - ...esMailStatus, - activatingPartialES: false, - })); - }; - - /** * Initialize ES */ const initializeESMail = async () => { - // Kill switch to control the release of partial ES - const partialES = await getPartialES(); - if (!isPaid(user) && !!partialES && !partialES.Value) { - return esLibraryFunctions.esDelete(); - } - // Migrate old IDBs const success = await migrate( user.ID, @@ -235,129 +153,21 @@ const EncryptedSearchProvider = ({ children }: Props) => { await esLibraryFunctions.esDelete(); } - // Enable encrypted search for all new users. For paid users only, - // automatically enable content search too - if (welcomeFlags.isWelcomeFlow && !isMobile()) { - // Prevent showing the spotlight for ES to them - await updateSpotlightES(false); - return esLibraryFunctions.enableEncryptedSearch().then(() => { - if (isPaid(user)) { - return esLibraryFunctions.enableContentSearch({ notify: false }); - } - }); - } - - // Existence of IDB is checked since the following operations interact with it - if (!(await checkVersionedESDB(user.ID))) { - return; - } - - let contentProgress = await readContentProgress(user.ID); - if (!contentProgress) { - return esLibraryFunctions.initializeES(); - } - - // We need to cache the metadata directly, since the library is - // not yet initialised, i.e. the flags in memory are not yet set. - // The reason for not initialising the library just yet is that - // in case an upgrade/downgrade is needed, the flags would be set - // incorrectly due to the way we encode the latter - const indexKey = await getIndexKey(getUserKeys, user.ID); - if (!indexKey) { + // In case of a downgrade from paid to free, remove everything + if ((await checkVersionedESDB(user.ID)) && !isPaid(user)) { return esLibraryFunctions.esDelete(); } - const esCacheRef = { current: defaultESCache }; - await cacheMetadata<ESBaseMessage>(user.ID, indexKey, esHelpers.getItemInfo, esCacheRef); - - if (isPaid(user)) { - // Upgrade comes almost for free since we can turn the previously partial - // content progress into an indexing one and start from where the time window - // left off. This should happen in case a user is paid, has an - // ACTIVE content indexing progress and (which is important to tell apart - // users with full content search) has a recovery point of the form [number, number] - if ( - contentProgress.status === INDEXING_STATUS.ACTIVE && - checkRecoveryFormat(contentProgress.recoveryPoint) - ) { - const newContentProgress: ESProgress = { - ...defaultESProgress, - totalItems: await getTotal(getMessageCounts)(), - recoveryPoint: contentProgress.recoveryPoint, - status: INDEXING_STATUS.INDEXING, - }; - await writeContentProgress( - user.ID, - newContentProgress, - esCacheRef.current.esCache, - esHelpers.getItemInfo - ); - } - } else { - // Downgrade is handled by re-encoding the content progress in the way - // partial ES does (i.e. setting the status to ACTIVE, the recoverPoint to - // the last message with content, which coincides with the last one for a - // previously full index, and leaving all other properties to their default) - // and then let the removeOldContent do its job. - // This should happen in case a user is not paid, has an - // ACTIVE content indexing progress and (which is important to tell apart - // users with only partial ES) has no recovery point in the latter - if ( - (contentProgress.status === INDEXING_STATUS.ACTIVE && !contentProgress.recoveryPoint) || - contentProgress.status === INDEXING_STATUS.INDEXING || - contentProgress.status === INDEXING_STATUS.PAUSED - ) { - // The first message in cache is the oldest - const iter = esCacheRef.current.esCache.values().next(); - const lastTimePoint = iter.done ? undefined : [iter.value.metadata.Time, iter.value.metadata.Order]; - - // A last item must exist. If it doesn't we consider it a corruption - // therefore we remove everything and we re-index first metadata, then - // content only for the last month. Another option is that, in case indexing - // was in progress or paused prior to the downgrade, it didn't cover enough - // ground to include all messages in the time window - if (!lastTimePoint || lastTimePoint[0] > getWindowStart()) { - await esLibraryFunctions.esDelete(); - return esLibraryFunctions.enableEncryptedSearch({ isRefreshed: true }).then(activateContentSearch); - } - - const lastContentTime = lastTimePoint[0] * SECOND; - setESMailStatus((esMailStatus) => ({ - ...esMailStatus, - lastContentTime, - })); - - const newContentProgress = { - ...defaultESProgress, - recoveryPoint: lastTimePoint, - status: INDEXING_STATUS.ACTIVE, - }; - contentProgress = newContentProgress; - - await writeContentProgress( - user.ID, - newContentProgress, - esCacheRef.current.esCache, - esHelpers.getItemInfo - ); - } - // If a free user has a partial content index, trim it to the new time window - if (checkRecoveryFormat(contentProgress.recoveryPoint)) { - try { - await removeOldContent(user.ID, getUserKeys, esCacheRef); - } catch (error: any) { - await revertPartialESActivation(user.ID, esCacheRef); - - createNotification({ - text: c('Error').t`Something went wrong, please try again`, - type: 'error', - }); - void esSentryReport('removeOldContent', error); - } - } + // Enable encrypted search for all new users + if (welcomeFlags.isWelcomeFlow && !isMobile() && isPaid(user)) { + // Prevent showing the spotlight for ES to them + await updateSpotlightES(false); + return esLibraryFunctions + .enableEncryptedSearch() + .then(() => esLibraryFunctions.enableContentSearch({ notify: false })); } - return esLibraryFunctions.initializeES(false); + return esLibraryFunctions.initializeES(); }; useSubscribeEventManager(async (event: Event) => esLibraryFunctions.handleEvent(convertEventType(event))); @@ -415,7 +225,6 @@ const EncryptedSearchProvider = ({ children }: Props) => { closeDropdown, setTemporaryToggleOff, cacheMailContent, - activateContentSearch, }; return <EncryptedSearchContext.Provider value={esFunctions}>{children}</EncryptedSearchContext.Provider>; diff --git a/applications/mail/src/app/helpers/encryptedSearch/partialES.ts b/applications/mail/src/app/helpers/encryptedSearch/partialES.ts deleted file mode 100644 index 7459f5be20..0000000000 --- a/applications/mail/src/app/helpers/encryptedSearch/partialES.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { startOfDay, sub } from 'date-fns'; - -import { serverTime } from '@proton/crypto'; -import { - AesGcmCiphertext, - ESCache, - ESHelpers, - ES_MAX_CACHE, - ES_MAX_CONCURRENT, - GetUserKeys, - INDEXING_STATUS, - defaultESProgress, - encryptItem, - getIndexKey, - openESDB, - removeFromESCache, - roundMilliseconds, - sendIndexingMetrics, - setContentRecoveryPoint, - sizeOfESItem, - updateSize, - writeContentProgress, -} from '@proton/encrypted-search'; -import runInQueue from '@proton/shared/lib/helpers/runInQueue'; -import { Api, User } from '@proton/shared/lib/interfaces'; - -import { ESBaseMessage, ESMessageContent, NormalizedSearchParams } from '../../models/encryptedSearch'; -import { getItemInfo } from './encryptedSearchMailHelpers'; - -/** - * Compute the start date of the time window inside which - * we index emails' content - */ -export const getWindowStart = () => roundMilliseconds(startOfDay(sub(serverTime().getTime(), { days: 30 })).getTime()); - -/** - * Index the content of the most recent specified amount of time worth of emails - * for free users, as a way to try out content search - */ -export const activatePartialES = async ( - api: Api, - user: User, - getUserKeys: GetUserKeys, - esHelpers: ESHelpers<ESBaseMessage, NormalizedSearchParams, ESMessageContent>, - esCacheRef: React.MutableRefObject<ESCache<ESBaseMessage, ESMessageContent>> -) => { - const { ID: userID } = user; - - const esDB = await openESDB(userID); - if (!esDB) { - throw new Error('ESDB cannot be opened'); - } - - const indexKey = await getIndexKey(getUserKeys, userID); - if (!indexKey) { - throw new Error('Index key must exist'); - } - - const { fetchESItem } = esHelpers; - if (!fetchESItem) { - throw new Error('fetchESItem must be defined to download items content'); - } - - let limitedCache = false; - let batchSize = 0; - - const esIteratee = async (itemMetadata: ESBaseMessage) => { - const itemToStore = await fetchESItem(itemMetadata.ID, undefined, esCacheRef); - - let aesGcmCiphertext: AesGcmCiphertext | undefined; - if (itemToStore) { - const size = sizeOfESItem(itemToStore); - batchSize += size; - - if (esCacheRef.current.cacheSize < ES_MAX_CACHE) { - esCacheRef.current.esCache.set(itemMetadata.ID, { - metadata: itemMetadata, - content: itemToStore, - }); - esCacheRef.current.cacheSize += size; - } else { - // In case the limit is reached, the content is not added, the metadata is already - // present, therefore we simply flag that the content in cache is limited - limitedCache = true; - } - - aesGcmCiphertext = await encryptItem(itemToStore, indexKey); - } - - return { itemID: itemMetadata.ID, aesGcmCiphertext }; - }; - - const windowStart = getWindowStart(); - const contentMetadata = Array.from(esCacheRef.current.esCache.values(), ({ metadata }) => metadata).filter( - (metadata) => metadata.Time >= windowStart - ); - - // No messages within the time window has been found. We - // nonetheless have to encode the fact that partial ES is - // active. By setting the current date as recovery point, we are - // ensuring that no messages in the mailbox have content. If new - // messages arrive they will be newer than such a timepoint and - // thus correctly indexed with content - if (!contentMetadata.length) { - await writeContentProgress<ESBaseMessage>( - userID, - { - ...defaultESProgress, - recoveryPoint: [windowStart, 0], - status: INDEXING_STATUS.ACTIVE, - }, - esCacheRef.current.esCache, - getItemInfo - ); - return; - } - - const ciphertexts = await runInQueue( - contentMetadata.map((metadata) => () => esIteratee(metadata)), - ES_MAX_CONCURRENT - ).catch(() => undefined); - - if (!ciphertexts || ciphertexts.length !== contentMetadata.length) { - throw new Error('Failed to load messages'); - } - - const tx = esDB.transaction('content', 'readwrite'); - await Promise.all( - ciphertexts.map(async (ciphertext) => - !!ciphertext.aesGcmCiphertext ? tx.store.put(ciphertext.aesGcmCiphertext, ciphertext.itemID) : undefined - ) - ); - await tx.done; - esDB.close(); - - // We store the last message for which content was indexed in the recoveryPoint - // of the content indexing progress. Note that since cache is in chronological - // order, i.e. oldest message first, so is contentMetadata - const lastMessage = contentMetadata[0]; - await writeContentProgress<ESBaseMessage>( - userID, - { - ...defaultESProgress, - recoveryPoint: [lastMessage.Time, lastMessage.Order], - status: INDEXING_STATUS.ACTIVE, - }, - esCacheRef.current.esCache, - getItemInfo - ); - - await updateSize<ESBaseMessage>(userID, batchSize, esCacheRef.current.esCache, getItemInfo); - - void sendIndexingMetrics(api, user, true); - - esCacheRef.current.isCacheLimited = limitedCache; - esCacheRef.current.isContentCached = true; -}; - -/** - * Remove the content of messages that have fallen out of the - * temporal window - */ -export const removeOldContent = async ( - userID: string, - getUserKeys: GetUserKeys, - esCacheRef: React.MutableRefObject<ESCache<ESBaseMessage, ESMessageContent>> -) => { - const esDB = await openESDB(userID); - if (!esDB) { - throw new Error('ESDB cannot be opened'); - } - - const indexKey = await getIndexKey(getUserKeys, userID); - if (!indexKey) { - throw new Error('Index key not found'); - } - - const windowStart = getWindowStart(); - - const cachedItems = [...esCacheRef.current.esCache.values()]; - // We look for all messages older than the start of the time window - const contentMetadata = Array.from(cachedItems, ({ metadata }) => metadata).filter( - (metadata) => metadata.Time < windowStart - ); - - // We look for the oldest message that sits in the window to set - // it as the new last time. Note that since cache is in chronological - // order, so is cachedItems and therefore the first message within the - // window is also the oldest to be so - let recoveryPoint: [number, number] = [windowStart, 0]; - const newLastItem = cachedItems.find((value) => value.metadata.Time >= windowStart); - if (newLastItem) { - recoveryPoint = [newLastItem.metadata.Time, newLastItem.metadata.Order]; - } - - const sizes = await Promise.all( - contentMetadata.map(async (metadata) => { - await esDB.delete('content', metadata.ID); - return removeFromESCache<ESBaseMessage, ESMessageContent>(metadata.ID, esCacheRef, true); - }) - ); - - esDB.close(); - - await updateSize<ESBaseMessage>( - userID, - -1 * sizes.reduce((p, c) => p + c, 0), - esCacheRef.current.esCache, - getItemInfo - ); - await setContentRecoveryPoint<ESBaseMessage>(userID, recoveryPoint, esCacheRef.current.esCache, getItemInfo); -}; - -/** - * In case of failure of partial ES activation, only remove - * what is stored in the content table and content row of - * the indexingProgress table, as well as any cached content - */ -export const revertPartialESActivation = async ( - userID: string, - esCacheRef: React.MutableRefObject<ESCache<ESBaseMessage, ESMessageContent>> -) => { - const esDB = await openESDB(userID); - if (!esDB) { - return; - } - - const contentIDs = await esDB.getAllKeys('content'); - const sizes = await Promise.all( - contentIDs.map((itemID) => removeFromESCache<ESBaseMessage, ESMessageContent>(itemID, esCacheRef, true)) - ); - await esDB.clear('content'); - await esDB.delete('indexingProgress', 'content'); - esDB.close(); - - await updateSize<ESBaseMessage>( - userID, - -1 * sizes.reduce((p, c) => p + c, 0), - esCacheRef.current.esCache, - getItemInfo - ); - - esCacheRef.current.isCacheLimited = false; - esCacheRef.current.isContentCached = false; -}; diff --git a/applications/mail/src/app/models/encryptedSearch.ts b/applications/mail/src/app/models/encryptedSearch.ts index 7c86e5f733..e4b5d90c09 100644 --- a/applications/mail/src/app/models/encryptedSearch.ts +++ b/applications/mail/src/app/models/encryptedSearch.ts @@ -35,7 +35,6 @@ export interface ESMessageContent { export interface ESDBStatusMail { dropdownOpened: boolean; temporaryToggleOff: boolean; - activatingPartialES: boolean; lastContentTime: number; } @@ -63,7 +62,6 @@ export interface EncryptedSearchFunctionsMail setTemporaryToggleOff: () => void; getESDBStatus: () => ESDBStatusMail & ESDBStatus<ESBaseMessage, ESMessageContent, NormalizedSearchParams>; cacheMailContent: () => Promise<void>; - activateContentSearch: () => Promise<void>; } export interface NormalizedSearchParams extends Omit<SearchParameters, 'wildcard' | 'keyword'> { diff --git a/packages/components/containers/features/FeaturesContext.ts b/packages/components/containers/features/FeaturesContext.ts index 42f822fae4..5a00052f5a 100644 --- a/packages/components/containers/features/FeaturesContext.ts +++ b/packages/components/containers/features/FeaturesContext.ts @@ -89,7 +89,6 @@ export enum FeatureCode { TrustedDeviceRecovery = 'TrustedDeviceRecovery', BulkUserUpload = 'BulkUserUpload', DriveBeta = 'DriveBeta', - PartialEncryptedSearch = 'PartialEncryptedSearch', ConversationHeaderInScroll = 'ConversationHeaderInScroll', } diff --git a/packages/encrypted-search/lib/esHelpers/esAPI.ts b/packages/encrypted-search/lib/esHelpers/esAPI.ts index 92af6079b5..211234d0a1 100644 --- a/packages/encrypted-search/lib/esHelpers/esAPI.ts +++ b/packages/encrypted-search/lib/esHelpers/esAPI.ts @@ -1,16 +1,9 @@ -import { getLocalKey } from '@proton/shared/lib/api/auth'; import { getIsOfflineError, getIsTimeoutError } from '@proton/shared/lib/api/helpers/apiErrorHelper'; -import { getKey } from '@proton/shared/lib/authentication/cryptoHelper'; -import { LocalKeyResponse } from '@proton/shared/lib/authentication/interface'; -import { getActiveSessionByUserID } from '@proton/shared/lib/authentication/persistedSessionHelper'; -import { getDecryptedPersistedSessionBlob } from '@proton/shared/lib/authentication/persistedSessionStorage'; import { METRICS_LOG, SECOND } from '@proton/shared/lib/constants'; -import { withUIDHeaders } from '@proton/shared/lib/fetch/headers'; -import { base64StringToUint8Array, decodeBase64URL, stringToUint8Array } from '@proton/shared/lib/helpers/encoding'; import { randomDelay, sendMetricsReport } from '@proton/shared/lib/helpers/metrics'; import { wait } from '@proton/shared/lib/helpers/promise'; import { captureMessage } from '@proton/shared/lib/helpers/sentry'; -import { Api, User } from '@proton/shared/lib/interfaces'; +import { Api } from '@proton/shared/lib/interfaces'; import { ES_TEMPORARY_ERRORS } from '../constants'; import { readContentProgress, readNumMetadata, readSize } from '../esIDB'; @@ -101,73 +94,17 @@ const sendESMetrics: SendESMetrics = async (api, Title, Data) => sendMetricsReport(api, METRICS_LOG.ENCRYPTED_SEARCH, Title, Data); /** - * Generate a pseudorandom user ID unknown to the API - */ -const generateESID = async (api: Api, user: User) => { - const persistedSession = getActiveSessionByUserID(user.ID, !!user.OrganizationPrivateKey); - const { UID, blob } = persistedSession || {}; - if (!UID || !blob) { - return; - } - - try { - // Extract the local keyPassword - const ClientKey = await api<LocalKeyResponse>(withUIDHeaders(UID, getLocalKey())).then( - ({ ClientKey }) => ClientKey - ); - const localKey = await getKey(base64StringToUint8Array(ClientKey)); - const { keyPassword } = await getDecryptedPersistedSessionBlob(localKey, blob); - - // Extract a fresh key for HMAC - const hkdfKey = await crypto.subtle.importKey( - 'raw', - base64StringToUint8Array(keyPassword).buffer, - 'HKDF', - false, - ['deriveKey'] - ); - const hmacKey = await crypto.subtle.deriveKey( - { - name: 'HKDF', - hash: 'SHA-256', - salt: new Uint8Array(256), - info: new Uint8Array(), - }, - hkdfKey, - { - name: 'HMAC', - hash: 'SHA-256', - }, - false, - ['sign'] - ); - - // HMAC the user ID. The final esID is the first 32-bit of the tag - return new Uint32Array( - await crypto.subtle.sign('HMAC', hmacKey, stringToUint8Array(decodeBase64URL(user.ID))) - )[0]; - } catch (error: any) { - return; - } -}; - -/** * Send metrics about the indexing process */ -export const sendIndexingMetrics = async (api: Api, user: User, isLimited: boolean = false) => { - const progressBlob = await readContentProgress(user.ID); +export const sendIndexingMetrics = async (api: Api, userID: string) => { + const progressBlob = await readContentProgress(userID); if (!progressBlob) { return; } - const esID = await generateESID(api, user); - if (!esID) { - return; - } - const { totalItems, isRefreshed, numPauses, timestamps, originalEstimate } = progressBlob; const { indexTime, totalInterruptions } = estimateIndexingDuration(timestamps); - const indexSize = await readSize(user.ID); + const indexSize = await readSize(userID); return sendESMetrics(api, 'index', { numInterruptions: totalInterruptions - numPauses, @@ -179,8 +116,6 @@ export const sendIndexingMetrics = async (api: Api, user: User, isLimited: boole numMessagesIndexed: totalItems, isRefreshed, numPauses, - esID, - isLimited, }); }; diff --git a/packages/encrypted-search/lib/esIDB/indexedDB.ts b/packages/encrypted-search/lib/esIDB/indexedDB.ts index 2d9ecfc3b4..dae0873e7a 100644 --- a/packages/encrypted-search/lib/esIDB/indexedDB.ts +++ b/packages/encrypted-search/lib/esIDB/indexedDB.ts @@ -57,11 +57,8 @@ export const checkVersionedESDB = async (userID: string) => { /** * Create an up-to-date IDB for the given user */ -export const createESDB = async (userID: string) => { - // Remove the database first, in case there is an old stale version that - // might arise when removing it and creating a new one immediately after - await deleteESDB(userID); - return openDB<EncryptedSearchDB>(getDBName(userID), INDEXEDDB_VERSION, { +export const createESDB = (userID: string) => + openDB<EncryptedSearchDB>(getDBName(userID), INDEXEDDB_VERSION, { upgrade: (esDB) => { // The object store containing the content of items, indexed by their ID esDB.createObjectStore('content'); @@ -85,7 +82,6 @@ export const createESDB = async (userID: string) => { esDB.createObjectStore('indexingProgress'); }, }); -}; /** * Write to the ES IDB and manage the case of running out of disk space. diff --git a/packages/encrypted-search/lib/models/esFunctions.ts b/packages/encrypted-search/lib/models/esFunctions.ts index 91c301d257..53a5d11ff1 100644 --- a/packages/encrypted-search/lib/models/esFunctions.ts +++ b/packages/encrypted-search/lib/models/esFunctions.ts @@ -148,7 +148,7 @@ export interface EncryptedSearchFunctions<ESItemMetadata, ESSearchParameters, ES * the EncryptedSearchProvider runs, as it checks for new events, continues indexing in * case a previous one was started, checks whether the index key is still accessible */ - initializeES: (shouldCache?: boolean) => Promise<void>; + initializeES: () => Promise<void>; /** * Pause the currently ongoing indexing process, if any diff --git a/packages/encrypted-search/lib/models/interfaces.ts b/packages/encrypted-search/lib/models/interfaces.ts index e47f205fc3..49db36301a 100644 --- a/packages/encrypted-search/lib/models/interfaces.ts +++ b/packages/encrypted-search/lib/models/interfaces.ts @@ -177,8 +177,6 @@ export interface ESIndexMetrics extends ESMetrics { numInterruptions: number; isRefreshed: boolean; indexTime: number; - esID: number; - isLimited: boolean; } /** diff --git a/packages/encrypted-search/lib/useEncryptedSearch.tsx b/packages/encrypted-search/lib/useEncryptedSearch.tsx index 4e532adbfe..e2d8de0de9 100644 --- a/packages/encrypted-search/lib/useEncryptedSearch.tsx +++ b/packages/encrypted-search/lib/useEncryptedSearch.tsx @@ -881,7 +881,7 @@ const useEncryptedSearch = <ESItemMetadata, ESSearchParameters, ESItemContent = esHelpers.getItemInfo, TIMESTAMP_TYPE.STOP ); - void sendIndexingMetrics(api, user); + void sendIndexingMetrics(api, userID); if (notify) { createNotification({ @@ -1221,7 +1221,7 @@ const useEncryptedSearch = <ESItemMetadata, ESSearchParameters, ESItemContent = * the EncryptedSearchProvider runs, as it checks for new events, continues indexing in * case a previous one was started, checks whether the index key is still accessible */ - const initializeES = async (shouldCache: boolean = true) => { + const initializeES = async () => { // Check whether the ES IDB exists for the current user. Nothing else is // needed in case it doesn't if (!(await checkVersionedESDB(userID))) { @@ -1243,11 +1243,8 @@ const useEncryptedSearch = <ESItemMetadata, ESSearchParameters, ESItemContent = return dbCorruptError(); } - // Cache is needed at load time, unless otherwise specified because - // it was already built by another process - if (shouldCache) { - await cacheMetadata<ESItemMetadata>(userID, indexKey, esHelpers.getItemInfo, esCacheRef); - } + // Cache is needed at load time + await cacheMetadata<ESItemMetadata>(userID, indexKey, esHelpers.getItemInfo, esCacheRef); if (metadataProgress.status === INDEXING_STATUS.INDEXING) { return enableEncryptedSearch(); |