diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js index 44b584828..be47435e3 100644 --- a/keyserver/src/responders/redux-state-responders.js +++ b/keyserver/src/responders/redux-state-responders.js @@ -1,388 +1,400 @@ // @flow import _keyBy from 'lodash/fp/keyBy.js'; import t, { type TInterface } from 'tcomb'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import { mixedRawThreadInfoValidator } from 'lib/permissions/minimally-encoded-raw-thread-info-validators.js'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; import { freshMessageStore } from 'lib/reducers/message-reducer.js'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; import { threadHasPermission, threadIsPending, parsePendingThreadID, createPendingThread, } from 'lib/shared/thread-utils.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import { entryStoreValidator } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { inviteLinksStoreValidator, type CommunityLinks, type InviteLinkWithHolder, } from 'lib/types/link-types.js'; import { defaultNumberPerThread, messageStoreValidator, type MessageStore, } from 'lib/types/message-types.js'; import { webNavInfoValidator } from 'lib/types/nav-types.js'; import type { WebInitialKeyserverInfo, ServerWebInitialReduxStateResponse, } from 'lib/types/redux-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadStore } from 'lib/types/thread-types.js'; import { currentUserInfoValidator, userInfosValidator, type GlobalAccountUserInfo, } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import { urlInfoValidator } from 'lib/utils/url-utils.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import type { InitialReduxStateRequest, ExcludedData, } from 'web/types/redux-types.js'; import { navInfoFromURL } from 'web/url-utils.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchCurrentUserInfo, fetchKnownUserInfos, fetchUserInfos, } from '../fetchers/user-fetchers.js'; import { getWebPushConfig } from '../push/providers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; import { thisKeyserverID } from '../user/identity.js'; const excludedDataValidator: TInterface = tShape({ userStore: t.maybe(t.Bool), + messageStore: t.maybe(t.Bool), threadStore: t.maybe(t.Bool), }); export const initialReduxStateRequestValidator: TInterface = tShape({ urlInfo: urlInfoValidator, excludedData: excludedDataValidator, clientUpdatesCurrentAsOf: t.Number, }); const initialKeyserverInfoValidator = tShape({ sessionID: t.maybe(t.String), updatesCurrentAsOf: t.Number, }); export const threadStoreValidator: TInterface = tShape({ threadInfos: t.dict(tID, mixedRawThreadInfoValidator), }); export const initialReduxStateValidator: TInterface = tShape({ navInfo: webNavInfoValidator, currentUserInfo: currentUserInfoValidator, entryStore: entryStoreValidator, threadStore: threadStoreValidator, userInfos: userInfosValidator, messageStore: messageStoreValidator, pushApiPublicKey: t.maybe(t.String), inviteLinksStore: inviteLinksStoreValidator, keyserverInfo: initialKeyserverInfoValidator, }); async function getInitialReduxStateResponder( viewer: Viewer, request: InitialReduxStateRequest, ): Promise { const { urlInfo, excludedData, clientUpdatesCurrentAsOf } = request; const useDatabase = viewer.loggedIn && canUseDatabaseOnWeb(viewer.userID); const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); const initialNavInfoPromise = (async () => { try { let backupInfo = { now: currentDateInTimeZone(viewer.timeZone), }; // Some user ids in selectedUserList might not exist in the userInfos // (e.g. they were included in the results of the user search endpoint) // Because of that we keep their userInfos inside the navInfo. if (urlInfo.selectedUserList) { const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); const userInfos: { [string]: GlobalAccountUserInfo } = {}; for (const userID in fetchedUserInfos) { const userInfo = fetchedUserInfos[userID]; if (userInfo.username) { userInfos[userID] = { ...userInfo, username: userInfo.username, }; } } backupInfo = { userInfos, ...backupInfo }; } return navInfoFromURL(urlInfo, backupInfo); } catch (e) { throw new ServerError(e.message); } })(); const calendarQueryPromise = (async () => { const initialNavInfo = await initialNavInfoPromise; return { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; })(); const messageSelectionCriteria = { joinedThreads: true }; const serverUpdatesCurrentAsOf = useDatabase && clientUpdatesCurrentAsOf ? clientUpdatesCurrentAsOf : Date.now(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = (async () => { const calendarQuery = await calendarQueryPromise; return await fetchEntryInfos(viewer, [calendarQuery]); })(); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { const calendarQuery = await calendarQueryPromise; if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, serverUpdatesCurrentAsOf); } return viewer.sessionID; })(); const threadStorePromise = (async () => { if (excludedData.threadStore && useDatabase) { return { threadInfos: {} }; } const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ threadInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; })(); const messageStorePromise: Promise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, hasNotAcknowledgedPolicies, keyserverID, ] = await Promise.all([ threadInfoPromise, messageInfoPromise, hasNotAcknowledgedPoliciesPromise, thisKeyserverID(), ]); if (hasNotAcknowledgedPolicies) { return { messages: {}, threads: {}, local: {}, currentAsOf: { [keyserverID]: 0 }, }; } const { messageStore: freshStore } = freshMessageStore( rawMessageInfos, truncationStatuses, { [keyserverID]: mostRecentMessageTimestamp( rawMessageInfos, serverUpdatesCurrentAsOf, ), }, threadInfos, ); return freshStore; })(); + const finalMessageStorePromise: Promise = (async () => { + if (excludedData.messageStore && useDatabase) { + return { + messages: {}, + threads: {}, + local: {}, + currentAsOf: {}, + }; + } + return await messageStorePromise; + })(); const entryStorePromise = (async () => { const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ entryInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }; } return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: serverUpdatesCurrentAsOf, }; })(); const userInfosPromise = (async () => { const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ userInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return hasNotAcknowledgedPolicies ? {} : userInfos; })(); const finalUserInfosPromise = (async () => { if (excludedData.userStore && useDatabase) { return {}; } return await userInfosPromise; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userInfos, finalNavInfo, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userInfosPromise, initialNavInfoPromise, ]); const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadIsPending(requestedActiveChatThreadID) && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentlyReadThread( messageStore, threadInfos, ); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } const curActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( curActiveChatThreadID && threadIsPending(curActiveChatThreadID) && finalNavInfo.pendingThread?.id !== curActiveChatThreadID ) { const pendingThreadData = parsePendingThreadID(curActiveChatThreadID); if ( pendingThreadData && pendingThreadData.threadType !== threadTypes.SIDEBAR && currentUserInfo.id ) { const members = [...pendingThreadData.memberIDs, currentUserInfo.id] .map(id => { const userInfo = userInfos[id]; if (!userInfo || !userInfo.username) { return undefined; } const { username } = userInfo; return { id, username }; }) .filter(Boolean); const newPendingThread = createPendingThread({ viewerID: currentUserInfo.id, threadType: pendingThreadData.threadType, members, }); finalNavInfo.activeChatThreadID = newPendingThread.id; finalNavInfo.pendingThread = newPendingThread; } } return finalNavInfo; })(); const currentAsOfPromise = (async () => { const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; return hasNotAcknowledgedPolicies ? 0 : serverUpdatesCurrentAsOf; })(); const pushApiPublicKeyPromise: Promise = (async () => { const pushConfig = await getWebPushConfig(); if (!pushConfig) { if (process.env.NODE_ENV !== 'development') { console.warn('keyserver/secrets/web_push_config.json should exist'); } return null; } return pushConfig.publicKey; })(); const inviteLinksStorePromise = (async () => { const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); const links: { [string]: CommunityLinks } = {}; for (const { blobHolder, ...link }: InviteLinkWithHolder of primaryInviteLinks) { if (link.primary) { links[link.communityID] = { primaryLink: link, }; } } return { links, }; })(); const keyserverInfoPromise = (async () => { const { sessionID, updatesCurrentAsOf } = await promiseAll({ sessionID: sessionIDPromise, updatesCurrentAsOf: currentAsOfPromise, }); return { sessionID, updatesCurrentAsOf, }; })(); const initialReduxState: ServerWebInitialReduxStateResponse = await promiseAll({ navInfo: navInfoPromise, currentUserInfo: currentUserInfoPromise, entryStore: entryStorePromise, threadStore: threadStorePromise, userInfos: finalUserInfosPromise, - messageStore: messageStorePromise, + messageStore: finalMessageStorePromise, pushApiPublicKey: pushApiPublicKeyPromise, inviteLinksStore: inviteLinksStorePromise, keyserverInfo: keyserverInfoPromise, }); return initialReduxState; } export { getInitialReduxStateResponder }; diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js index 1dae416ff..05381e2ac 100644 --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -1,178 +1,200 @@ // @flow import * as React from 'react'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import type { Persistor } from 'redux-persist/es/types'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; +import type { MessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import type { UserStoreOperation } from 'lib/ops/user-store-ops.js'; import { allUpdatesCurrentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { LegacyRawThreadInfo } from 'lib/types/thread-types.js'; import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; import { entries, values } from 'lib/utils/objects.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { setInitialReduxState, useGetInitialReduxState, } from './action-types.js'; import { useSelector } from './redux-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import Loading from '../loading.react.js'; import { getClientDBStore, processDBStoreOperations, } from '../shared-worker/utils/store.js'; import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; type Props = { +persistor: Persistor, +children: React.Node, }; function InitialReduxStateGate(props: Props): React.Node { const { children, persistor } = props; const callGetInitialReduxState = useGetInitialReduxState(); const dispatch = useDispatch(); const [initError, setInitError] = React.useState(null); React.useEffect(() => { if (initError) { throw initError; } }, [initError]); const isRehydrated = useSelector(state => !!state._persist?.rehydrated); const allUpdatesCurrentAsOf = useSelector(allUpdatesCurrentAsOfSelector); const prevIsRehydrated = React.useRef(false); React.useEffect(() => { if (prevIsRehydrated.current || !isRehydrated) { return; } prevIsRehydrated.current = isRehydrated; void (async () => { try { let urlInfo = infoFromURL(decodeURI(window.location.href)); // Handle older links if (urlInfo.thread) { urlInfo = { ...urlInfo, thread: convertIDToNewSchema( urlInfo.thread, authoritativeKeyserverID, ), }; } const clientDBStore = await getClientDBStore(); dispatch({ type: setClientDBStoreActionType, payload: clientDBStore, }); const payload = await callGetInitialReduxState({ urlInfo, excludedData: { threadStore: !!clientDBStore.threadStore, + messageStore: !!clientDBStore.messages, userStore: !!clientDBStore.users, }, allUpdatesCurrentAsOf, }); const currentLoggedInUserID = payload.currentUserInfo?.anonymous ? null : payload.currentUserInfo?.id; const useDatabase = canUseDatabaseOnWeb(currentLoggedInUserID); if (!currentLoggedInUserID || !useDatabase) { dispatch({ type: setInitialReduxState, payload }); return; } let initialReduxState: InitialReduxStateActionPayload = payload; let threadStoreOperations: ThreadStoreOperation[] = []; if (clientDBStore.threadStore) { const { threadStore, ...rest } = initialReduxState; initialReduxState = rest; } else { // When there is no data in the DB, it's necessary to migrate data // from the keyserver payload to the DB threadStoreOperations = entries(payload.threadStore.threadInfos).map( ([id, threadInfo]: [ string, LegacyRawThreadInfo | RawThreadInfo, ]) => ({ type: 'replace', payload: { id, threadInfo, }, }), ); } let userStoreOperations: UserStoreOperation[] = []; if (clientDBStore.users) { const { userInfos, ...rest } = initialReduxState; initialReduxState = rest; } else { userStoreOperations = values(payload.userInfos).map(userInfo => ({ type: 'replace_user', payload: userInfo, })); } + let messageStoreOperations: MessageStoreOperation[] = []; + if (clientDBStore.messages) { + const { messageStore, ...rest } = initialReduxState; + initialReduxState = rest; + } else { + const { messages, threads } = payload.messageStore; + + messageStoreOperations = [ + ...entries(messages).map(([id, messageInfo]) => ({ + type: 'replace', + payload: { id, messageInfo }, + })), + { + type: 'replace_threads', + payload: { threads }, + }, + ]; + } + if ( threadStoreOperations.length > 0 || - userStoreOperations.length > 0 + userStoreOperations.length > 0 || + messageStoreOperations.length > 0 ) { await processDBStoreOperations( { threadStoreOperations, draftStoreOperations: [], - messageStoreOperations: [], + messageStoreOperations, reportStoreOperations: [], userStoreOperations, keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], syncedMetadataStoreOperations: [], auxUserStoreOperations: [], }, currentLoggedInUserID, ); } dispatch({ type: setInitialReduxState, payload: initialReduxState, }); } catch (err) { setInitError(err); } })(); }, [callGetInitialReduxState, dispatch, isRehydrated, allUpdatesCurrentAsOf]); const initialStateLoaded = useSelector(state => state.initialStateLoaded); const childFunction = React.useCallback( // This argument is passed from `PersistGate`. It means that the state is // rehydrated and we can start fetching the initial info. (bootstrapped: boolean) => { if (bootstrapped && initialStateLoaded) { return children; } else { return ; } }, [children, initialStateLoaded], ); return {childFunction}; } export default InitialReduxStateGate; diff --git a/web/redux/persist.js b/web/redux/persist.js index 4e55294e1..fd731cc82 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,422 +1,424 @@ // @flow import invariant from 'invariant'; import { getStoredState, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { type ClientDBKeyserverStoreOperation, keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from 'lib/ops/keyserver-store-ops.js'; import { createAsyncMigrate, type StorageMigrationFunction, } from 'lib/shared/create-async-migrate.js'; import { keyserverStoreTransform } from 'lib/shared/transforms/keyserver-store-transform.js'; +import { messageStoreMessagesBlocklistTransform } from 'lib/shared/transforms/message-store-transform.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { cookieTypes } from 'lib/types/session-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import { getConfig } from 'lib/utils/config.js'; import { parseCookies } from 'lib/utils/cookie-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { wipeKeyserverStore } from 'lib/utils/keyserver-store-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import { defaultWebState } from './default-state.js'; import { rootKey, rootKeyPrefix } from './persist-constants.js'; import type { AppState } from './redux-setup.js'; import { nonUserSpecificFieldsWeb } from './redux-setup.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; import { isSQLiteSupported } from '../shared-worker/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var keyserverURL: string; const persistWhitelist = [ 'enabledApps', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'keyserverStore', 'globalThemeInfo', 'customServer', + 'messageStore', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { const persistedNonUserSpecificFields = nonUserSpecificFieldsWeb.filter( field => persistWhitelist.includes(field) || field === '_persist', ); const stateAfterReset = resetUserSpecificState( oldState, defaultWebState, persistedNonUserSpecificFields, ); return { ...stateAfterReset, keyserverStore: wipeKeyserverStore(stateAfterReset.keyserverStore), }; } const migrations = { [1]: async (state: any) => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async (state: AppState) => { return state; }, [3]: async (state: AppState) => { let newState = state; if (state.draftStore) { newState = { ...newState, draftStore: convertDraftStoreToNewIDSchema(state.draftStore), }; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return newState; } const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); invariant(stores?.store, 'Stores should exist'); await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( stores.store.drafts, ), }, }); return newState; }, [4]: async (state: any) => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [5]: async (state: any) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } if (!state.draftStore) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, [6]: async (state: AppState) => ({ ...state, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, }), [7]: async (state: AppState): Promise => { if (!document.cookie) { return state; } const params = parseCookies(document.cookie); let cookie = null; if (params[cookieTypes.USER]) { cookie = `${cookieTypes.USER}=${params[cookieTypes.USER]}`; } else if (params[cookieTypes.ANONYMOUS]) { cookie = `${cookieTypes.ANONYMOUS}=${params[cookieTypes.ANONYMOUS]}`; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], cookie, }, }, }, }; }, [8]: async (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [9]: async (state: AppState) => ({ ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], urlPrefix: keyserverURL, }, }, }, }), [10]: async (state: AppState) => { const { keyserverInfos } = state.keyserverStore; const newKeyserverInfos: { [string]: KeyserverInfo } = {}; for (const key in keyserverInfos) { newKeyserverInfos[key] = { ...keyserverInfos[key], connection: { ...defaultConnectionInfo }, updatesCurrentAsOf: 0, sessionID: null, }; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, }; }, [11]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return state; } catch (e) { console.log(e); return handleReduxMigrationFailure(state); } }, [12]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ) .filter(([, keyserverInfo]) => !keyserverInfo.actualizedCalendarQuery) .map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo: { ...keyserverInfo, actualizedCalendarQuery: defaultCalendarQuery( getConfig().platformDetails.platform, ), }, }, })); if (replaceOps.length === 0) { return state; } const newState = { ...state, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, replaceOps, ), }; const keyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(replaceOps); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return newState; } catch (e) { console.log(e); return handleReduxMigrationFailure(newState); } }, [13]: async (state: any) => { const { cryptoStore, ...rest } = state; const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, olmWasmPath: getOlmWasmPath(), initialCryptoStore: cryptoStore, }); return rest; }, [14]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const keyserversDBInfo = stores?.store?.keyservers; if (!keyserversDBInfo) { return state; } const { translateClientDBData } = keyserverStoreOpsHandlers; const keyservers = translateClientDBData(keyserversDBInfo); // There is no modification of the keyserver data, but the ops handling // should correctly split the data between synced and non-synced tables const replaceOps: $ReadOnlyArray = entries( keyservers, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return state; } catch (e) { console.log(e); return handleReduxMigrationFailure(state); } }, }; const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } const allKeys = Object.keys(oldStorage); const transforms = persistConfig.transforms ?? []; const newStorage = { ...oldStorage }; for (const transform of transforms) { for (const key of allKeys) { const transformedStore = transform.out(newStorage[key], key, newStorage); newStorage[key] = transformedStore; } } return newStorage; }; const persistConfig: PersistConfig = { keyPrefix: rootKeyPrefix, key: rootKey, storage: commReduxStorageEngine, whitelist: isSQLiteSupported() ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( migrations, { debug: isDev }, migrateStorageToSQLite, ): any), version: 14, - transforms: [keyserverStoreTransform], + transforms: [messageStoreMessagesBlocklistTransform, keyserverStoreTransform], }; export { persistConfig }; diff --git a/web/shared-worker/types/entities.js b/web/shared-worker/types/entities.js index 5d917d807..dec4d6e5b 100644 --- a/web/shared-worker/types/entities.js +++ b/web/shared-worker/types/entities.js @@ -1,102 +1,182 @@ // @flow +import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { ClientDBThreadInfo } from 'lib/types/thread-types.js'; +import type { Media, WebMessage } from './sqlite-query-executor.js'; + export type Nullable = { +value: T, +isNull: boolean, }; export type NullableString = Nullable; export type NullableInt = Nullable; export type WebClientDBThreadInfo = { +id: string, +type: number, +name: NullableString, +avatar: NullableString, +description: NullableString, +color: string, +creationTime: string, +parentThreadID: NullableString, +containingThreadID: NullableString, +community: NullableString, +members: string, +roles: string, +currentUser: string, +sourceMessageID: NullableString, +repliesCount: number, +pinnedCount: number, }; function createNullableString(value: ?string): NullableString { if (value === null || value === undefined) { return { value: '', isNull: true, }; } return { value, isNull: false, }; } +function createNullableInt(value: ?string): NullableInt { + if (value === null || value === undefined) { + return { + value: 0, + isNull: true, + }; + } + return { + value: Number(value), + isNull: false, + }; +} + function clientDBThreadInfoToWebThread( info: ClientDBThreadInfo, ): WebClientDBThreadInfo { return { id: info.id, type: info.type, name: createNullableString(info.name), avatar: createNullableString(info.avatar), description: createNullableString(info.description), color: info.color, creationTime: info.creationTime, parentThreadID: createNullableString(info.parentThreadID), containingThreadID: createNullableString(info.containingThreadID), community: createNullableString(info.community), members: info.members, roles: info.roles, currentUser: info.currentUser, sourceMessageID: createNullableString(info.sourceMessageID), repliesCount: info.repliesCount, pinnedCount: info.pinnedCount || 0, }; } function webThreadToClientDBThreadInfo( thread: WebClientDBThreadInfo, ): ClientDBThreadInfo { let result: ClientDBThreadInfo = { id: thread.id, type: thread.type, name: thread.name.isNull ? null : thread.name.value, avatar: thread.avatar.isNull ? null : thread.avatar.value, description: thread.description.isNull ? null : thread.description.value, color: thread.color, creationTime: thread.creationTime, parentThreadID: thread.parentThreadID.isNull ? null : thread.parentThreadID.value, containingThreadID: thread.containingThreadID.isNull ? null : thread.containingThreadID.value, community: thread.community.isNull ? null : thread.community.value, members: thread.members, roles: thread.roles, currentUser: thread.currentUser, repliesCount: thread.repliesCount, pinnedCount: thread.pinnedCount, }; if (!thread.sourceMessageID.isNull) { result = { ...result, sourceMessageID: thread.sourceMessageID.value, }; } return result; } -export { clientDBThreadInfoToWebThread, webThreadToClientDBThreadInfo }; +function clientDBMessageInfoToWebMessage(messageInfo: ClientDBMessageInfo): { + +message: WebMessage, + +medias: $ReadOnlyArray, +} { + return { + message: { + id: messageInfo.id, + localID: createNullableString(messageInfo.local_id), + thread: messageInfo.thread, + user: messageInfo.user, + type: Number(messageInfo.type), + futureType: createNullableInt(messageInfo.future_type), + content: createNullableString(messageInfo.content), + time: messageInfo.time, + }, + medias: + messageInfo.media_infos?.map(({ id, uri, type, extras }) => ({ + id, + uri, + type, + extras, + thread: messageInfo.thread, + container: messageInfo.id, + })) ?? [], + }; +} + +function webMessageToClientDBMessageInfo({ + message, + medias, +}: { + +message: WebMessage, + +medias: $ReadOnlyArray, +}): ClientDBMessageInfo { + let media_infos = null; + if (medias?.length !== 0) { + media_infos = medias.map(({ id, uri, type, extras }) => ({ + id, + uri, + type, + extras, + })); + } + + return { + id: message.id, + local_id: message.localID.isNull ? null : message.localID.value, + thread: message.thread, + user: message.user, + type: message.type.toString(), + future_type: message.futureType.isNull + ? null + : message.futureType.value.toString(), + content: message.content.isNull ? null : message.content.value, + time: message.time, + media_infos, + }; +} + +export { + clientDBThreadInfoToWebThread, + webThreadToClientDBThreadInfo, + clientDBMessageInfoToWebMessage, + webMessageToClientDBMessageInfo, +}; diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js index 4611a9d8c..764b08ed0 100644 --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -1,170 +1,170 @@ // @flow import type { ClientDBAuxUserInfo } from 'lib/ops/aux-user-store-ops.js'; import type { ClientDBCommunityInfo } from 'lib/ops/community-store-ops.js'; import type { ClientDBIntegrityThreadHash } from 'lib/ops/integrity-store-ops.js'; import type { ClientDBKeyserverInfo } from 'lib/ops/keyserver-store-ops.js'; import type { ClientDBReport } from 'lib/ops/report-store-ops.js'; import type { ClientDBSyncedMetadataEntry } from 'lib/ops/synced-metadata-store-ops.js'; import type { ClientDBUserInfo } from 'lib/ops/user-store-ops.js'; import type { ClientDBDraftInfo } from 'lib/types/draft-types.js'; import { type WebClientDBThreadInfo, type NullableString, type NullableInt, } from './entities.js'; -type WebMessage = { +export type WebMessage = { +id: string, +localID: NullableString, +thread: string, +user: string, +type: number, +futureType: NullableInt, +content: NullableString, +time: string, }; -type Media = { +export type Media = { +id: string, +container: string, +thread: string, +uri: string, +type: 'photo' | 'video', +extras: string, }; export type OlmPersistSession = { +targetUserID: string, +sessionData: string, }; export type ClientMessageToDevice = { +messageID: string, +deviceID: string, +userID: string, +timestamp: string, +plaintext: string, +ciphertext: string, }; declare export class SQLiteQueryExecutor { constructor(sqliteFilePath: string): void; updateDraft(key: string, text: string): void; moveDraft(oldKey: string, newKey: string): boolean; getAllDrafts(): ClientDBDraftInfo[]; removeAllDrafts(): void; removeDrafts(ids: $ReadOnlyArray): void; getAllMessagesWeb(): $ReadOnlyArray<{ +message: WebMessage, +medias: $ReadOnlyArray, }>; removeAllMessages(): void; removeMessages(ids: $ReadOnlyArray): void; removeMessagesForThreads(threadIDs: $ReadOnlyArray): void; replaceMessageWeb(message: WebMessage): void; rekeyMessage(from: string, to: string): void; removeAllMedia(): void; removeMediaForThreads(threadIDs: $ReadOnlyArray): void; removeMediaForMessages(msgIDs: $ReadOnlyArray): void; removeMediaForMessage(msgID: string): void; replaceMedia(media: Media): void; rekeyMediaContainers(from: string, to: string): void; replaceMessageStoreThreads( threads: $ReadOnlyArray<{ +id: string, +startReached: number }>, ): void; removeMessageStoreThreads($ReadOnlyArray): void; getAllMessageStoreThreads(): $ReadOnlyArray<{ +id: string, +startReached: number, }>; removeAllMessageStoreThreads(): void; setMetadata(entryName: string, data: string): void; clearMetadata(entryName: string): void; getMetadata(entryName: string): string; replaceReport(report: ClientDBReport): void; removeReports(ids: $ReadOnlyArray): void; removeAllReports(): void; getAllReports(): ClientDBReport[]; setPersistStorageItem(key: string, item: string): void; removePersistStorageItem(key: string): void; getPersistStorageItem(key: string): string; replaceUser(userInfo: ClientDBUserInfo): void; removeUsers(ids: $ReadOnlyArray): void; removeAllUsers(): void; getAllUsers(): ClientDBUserInfo[]; replaceThreadWeb(thread: WebClientDBThreadInfo): void; removeThreads(ids: $ReadOnlyArray): void; removeAllThreads(): void; getAllThreadsWeb(): WebClientDBThreadInfo[]; replaceKeyserver(keyserverInfo: ClientDBKeyserverInfo): void; removeKeyservers(ids: $ReadOnlyArray): void; removeAllKeyservers(): void; getAllKeyservers(): ClientDBKeyserverInfo[]; replaceCommunity(communityInfo: ClientDBCommunityInfo): void; removeCommunities(ids: $ReadOnlyArray): void; removeAllCommunities(): void; getAllCommunities(): ClientDBCommunityInfo[]; replaceIntegrityThreadHashes( threadHashes: $ReadOnlyArray, ): void; removeIntegrityThreadHashes(ids: $ReadOnlyArray): void; removeAllIntegrityThreadHashes(): void; getAllIntegrityThreadHashes(): ClientDBIntegrityThreadHash[]; replaceSyncedMetadataEntry( syncedMetadataEntry: ClientDBSyncedMetadataEntry, ): void; removeSyncedMetadata(names: $ReadOnlyArray): void; removeAllSyncedMetadata(): void; getAllSyncedMetadata(): ClientDBSyncedMetadataEntry[]; replaceAuxUserInfo(auxUserInfo: ClientDBAuxUserInfo): void; removeAuxUserInfos(ids: $ReadOnlyArray): void; removeAllAuxUserInfos(): void; getAllAuxUserInfos(): ClientDBAuxUserInfo[]; beginTransaction(): void; commitTransaction(): void; rollbackTransaction(): void; getContentAccountID(): number; getNotifsAccountID(): number; getOlmPersistAccountDataWeb(accountID: number): NullableString; getOlmPersistSessionsData(): $ReadOnlyArray; storeOlmPersistAccount(accountID: number, accountData: string): void; storeOlmPersistSession(session: OlmPersistSession): void; restoreFromMainCompaction( mainCompactionPath: string, mainCompactionEncryptionKey: string, ): void; restoreFromBackupLog(backupLog: Uint8Array): void; addMessagesToDevice(messages: $ReadOnlyArray): void; removeMessagesToDeviceOlderThan( lastConfirmedMessage: ClientMessageToDevice, ): void; removeAllMessagesForDevice(deviceID: string): void; getAllMessagesToDevice( deviceID: string, ): $ReadOnlyArray; // method is provided to manually signal that a C++ object // is no longer needed and can be deleted delete(): void; } export type SQLiteQueryExecutorType = typeof SQLiteQueryExecutor; diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js index f3e59050e..7026ce20f 100644 --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -1,195 +1,216 @@ // @flow import { auxUserStoreOpsHandlers } from 'lib/ops/aux-user-store-ops.js'; import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; +import { messageStoreOpsHandlers } from 'lib/ops/message-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { ClientStore, StoreOperations, } from 'lib/types/store-ops-types.js'; import { defaultWebState } from '../../redux/default-state.js'; import { workerRequestMessageTypes } from '../../types/worker-types.js'; import { getCommSharedWorker } from '../shared-worker-provider.js'; async function getClientDBStore(): Promise { const sharedWorker = await getCommSharedWorker(); let result: ClientStore = { currentUserID: null, drafts: [], messages: null, threadStore: null, messageStoreThreads: null, reports: null, users: null, keyserverInfos: defaultWebState.keyserverStore.keyserverInfos, communityInfos: null, threadHashes: null, syncedMetadata: null, auxUserInfos: null, }; const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); if (data?.store?.drafts) { result = { ...result, drafts: data.store.drafts, }; } if (data?.store?.reports) { result = { ...result, reports: reportStoreOpsHandlers.translateClientDBData(data.store.reports), }; } if (data?.store?.threads && data.store.threads.length > 0) { result = { ...result, threadStore: { threadInfos: threadStoreOpsHandlers.translateClientDBData( data.store.threads, ), }, }; } if (data?.store?.keyservers?.length) { result = { ...result, keyserverInfos: keyserverStoreOpsHandlers.translateClientDBData( data.store.keyservers, ), }; } if (data?.store?.communities) { result = { ...result, communityInfos: communityStoreOpsHandlers.translateClientDBData( data.store.communities, ), }; } if (data?.store?.integrityThreadHashes) { result = { ...result, threadHashes: integrityStoreOpsHandlers.translateClientDBData( data.store.integrityThreadHashes, ), }; } if (data?.store?.syncedMetadata) { result = { ...result, syncedMetadata: syncedMetadataStoreOpsHandlers.translateClientDBData( data.store.syncedMetadata, ), }; } if (data?.store?.auxUserInfos) { result = { ...result, auxUserInfos: auxUserStoreOpsHandlers.translateClientDBData( data.store.auxUserInfos, ), }; } if (data?.store?.users && data.store.users.length > 0) { result = { ...result, users: userStoreOpsHandlers.translateClientDBData(data.store.users), }; } + if (data?.store?.messages && data.store.messages.length > 0) { + result = { + ...result, + messages: data.store.messages, + }; + } + if ( + data?.store?.messageStoreThreads && + data.store.messageStoreThreads.length > 0 + ) { + result = { + ...result, + messageStoreThreads: data.store.messageStoreThreads, + }; + } return result; } async function processDBStoreOperations( storeOperations: StoreOperations, userID: null | string, ): Promise { const { draftStoreOperations, threadStoreOperations, reportStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, userStoreOperations, + messageStoreOperations, } = storeOperations; const canUseDatabase = canUseDatabaseOnWeb(userID); const convertedThreadStoreOperations = canUseDatabase ? threadStoreOpsHandlers.convertOpsToClientDBOps(threadStoreOperations) : []; const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); const convertedKeyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations); const convertedCommunityStoreOperations = communityStoreOpsHandlers.convertOpsToClientDBOps(communityStoreOperations); const convertedIntegrityStoreOperations = integrityStoreOpsHandlers.convertOpsToClientDBOps(integrityStoreOperations); const convertedSyncedMetadataStoreOperations = syncedMetadataStoreOpsHandlers.convertOpsToClientDBOps( syncedMetadataStoreOperations, ); const convertedAuxUserStoreOperations = auxUserStoreOpsHandlers.convertOpsToClientDBOps(auxUserStoreOperations); const convertedUserStoreOperations = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); + const convertedMessageStoreOperations = + messageStoreOpsHandlers.convertOpsToClientDBOps(messageStoreOperations); if ( convertedThreadStoreOperations.length === 0 && convertedReportStoreOperations.length === 0 && draftStoreOperations.length === 0 && convertedKeyserverStoreOperations.length === 0 && convertedCommunityStoreOperations.length === 0 && convertedIntegrityStoreOperations.length === 0 && convertedSyncedMetadataStoreOperations.length === 0 && convertedAuxUserStoreOperations.length === 0 && - convertedUserStoreOperations.length === 0 + convertedUserStoreOperations.length === 0 && + convertedMessageStoreOperations.length === 0 ) { return; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return; } try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations, reportStoreOperations: convertedReportStoreOperations, threadStoreOperations: convertedThreadStoreOperations, keyserverStoreOperations: convertedKeyserverStoreOperations, communityStoreOperations: convertedCommunityStoreOperations, integrityStoreOperations: convertedIntegrityStoreOperations, syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, auxUserStoreOperations: convertedAuxUserStoreOperations, userStoreOperations: convertedUserStoreOperations, + messageStoreOperations: convertedMessageStoreOperations, }, }); } catch (e) { console.log(e); if (canUseDatabase) { window.alert(e.message); if (threadStoreOperations.length > 0) { await sharedWorker.init({ clearDatabase: true }); location.reload(); } } } } export { getClientDBStore, processDBStoreOperations }; diff --git a/web/shared-worker/worker/process-operations.js b/web/shared-worker/worker/process-operations.js index 3f8495d8f..e89bbdf1a 100644 --- a/web/shared-worker/worker/process-operations.js +++ b/web/shared-worker/worker/process-operations.js @@ -1,435 +1,510 @@ // @flow import type { ClientDBAuxUserStoreOperation } from 'lib/ops/aux-user-store-ops.js'; import type { ClientDBCommunityStoreOperation } from 'lib/ops/community-store-ops.js'; import type { ClientDBIntegrityStoreOperation } from 'lib/ops/integrity-store-ops.js'; import type { ClientDBKeyserverStoreOperation } from 'lib/ops/keyserver-store-ops.js'; +import type { ClientDBMessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.js'; import type { ClientDBSyncedMetadataStoreOperation } from 'lib/ops/synced-metadata-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import type { ClientDBUserStoreOperation } from 'lib/ops/user-store-ops.js'; import type { ClientDBDraftStoreOperation, DraftStoreOperation, } from 'lib/types/draft-types.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { clientDBThreadInfoToWebThread, webThreadToClientDBThreadInfo, + webMessageToClientDBMessageInfo, + clientDBMessageInfoToWebMessage, } from '../types/entities.js'; import type { EmscriptenModule } from '../types/module.js'; import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; function getProcessingStoreOpsExceptionMessage( e: mixed, module: EmscriptenModule, ): string { if (typeof e === 'number') { return module.getExceptionMessage(e); } return getMessageForException(e) ?? 'unknown error'; } function processDraftStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: DraftStoreOperation of operations) { try { if (operation.type === 'remove_all') { sqliteQueryExecutor.removeAllDrafts(); } else if (operation.type === 'remove') { const { ids } = operation.payload; sqliteQueryExecutor.removeDrafts(ids); } else if (operation.type === 'update') { const { key, text } = operation.payload; sqliteQueryExecutor.updateDraft(key, text); } else if (operation.type === 'move') { const { oldKey, newKey } = operation.payload; sqliteQueryExecutor.moveDraft(oldKey, newKey); } else { throw new Error('Unsupported draft operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } draft operation: ${getProcessingStoreOpsExceptionMessage(e, module)}`, ); } } } function processReportStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBReportStoreOperation of operations) { try { if (operation.type === 'remove_all_reports') { sqliteQueryExecutor.removeAllReports(); } else if (operation.type === 'remove_reports') { const { ids } = operation.payload; sqliteQueryExecutor.removeReports(ids); } else if (operation.type === 'replace_report') { const { id, report } = operation.payload; sqliteQueryExecutor.replaceReport({ id, report }); } else { throw new Error('Unsupported report operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } report operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processThreadStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBThreadStoreOperation of operations) { try { if (operation.type === 'remove_all') { sqliteQueryExecutor.removeAllThreads(); } else if (operation.type === 'remove') { const { ids } = operation.payload; sqliteQueryExecutor.removeThreads(ids); } else if (operation.type === 'replace') { sqliteQueryExecutor.replaceThreadWeb( clientDBThreadInfoToWebThread(operation.payload), ); } else { throw new Error('Unsupported thread operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } thread operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processKeyserverStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBKeyserverStoreOperation of operations) { try { if (operation.type === 'remove_all_keyservers') { sqliteQueryExecutor.removeAllKeyservers(); } else if (operation.type === 'remove_keyservers') { const { ids } = operation.payload; sqliteQueryExecutor.removeKeyservers(ids); } else if (operation.type === 'replace_keyserver') { const { id, keyserverInfo, syncedKeyserverInfo } = operation.payload; sqliteQueryExecutor.replaceKeyserver({ id, keyserverInfo, syncedKeyserverInfo, }); } else { throw new Error('Unsupported keyserver operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } keyserver operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processCommunityStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBCommunityStoreOperation of operations) { try { if (operation.type === 'remove_all_communities') { sqliteQueryExecutor.removeAllCommunities(); } else if (operation.type === 'remove_communities') { const { ids } = operation.payload; sqliteQueryExecutor.removeCommunities(ids); } else if (operation.type === 'replace_community') { const { id, communityInfo } = operation.payload; sqliteQueryExecutor.replaceCommunity({ id, communityInfo }); } else { throw new Error('Unsupported community operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } community operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processIntegrityStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBIntegrityStoreOperation of operations) { try { if (operation.type === 'remove_all_integrity_thread_hashes') { sqliteQueryExecutor.removeAllIntegrityThreadHashes(); } else if (operation.type === 'remove_integrity_thread_hashes') { const { ids } = operation.payload; sqliteQueryExecutor.removeIntegrityThreadHashes(ids); } else if (operation.type === 'replace_integrity_thread_hashes') { const { threadHashes } = operation.payload; sqliteQueryExecutor.replaceIntegrityThreadHashes(threadHashes); } else { throw new Error('Unsupported integrity operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } integrity operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function processSyncedMetadataStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBSyncedMetadataStoreOperation of operations) { try { if (operation.type === 'remove_all_synced_metadata') { sqliteQueryExecutor.removeAllSyncedMetadata(); } else if (operation.type === 'remove_synced_metadata') { const { names } = operation.payload; sqliteQueryExecutor.removeSyncedMetadata(names); } else if (operation.type === 'replace_synced_metadata_entry') { const { name, data } = operation.payload; sqliteQueryExecutor.replaceSyncedMetadataEntry({ name, data }); } else { throw new Error('Unsupported synced metadata operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } synced metadata operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } +function processMessageStoreOperations( + sqliteQueryExecutor: SQLiteQueryExecutor, + operations: $ReadOnlyArray, + module: EmscriptenModule, +) { + for (const operation of operations) { + try { + if (operation.type === 'rekey') { + const { from, to } = operation.payload; + sqliteQueryExecutor.rekeyMessage(from, to); + } else if (operation.type === 'remove') { + const { ids } = operation.payload; + sqliteQueryExecutor.removeMessages(ids); + } else if (operation.type === 'replace') { + const { message, medias } = clientDBMessageInfoToWebMessage( + operation.payload, + ); + sqliteQueryExecutor.replaceMessageWeb(message); + for (const media of medias) { + sqliteQueryExecutor.replaceMedia(media); + } + } else if (operation.type === 'remove_all') { + sqliteQueryExecutor.removeAllMessages(); + sqliteQueryExecutor.removeAllMedia(); + } else if (operation.type === 'remove_threads') { + const { ids } = operation.payload; + sqliteQueryExecutor.removeMessageStoreThreads(ids); + } else if (operation.type === 'replace_threads') { + const { threads } = operation.payload; + + sqliteQueryExecutor.replaceMessageStoreThreads( + threads.map(({ id, start_reached }) => ({ + id, + startReached: Number(start_reached), + })), + ); + } else if (operation.type === 'remove_all_threads') { + sqliteQueryExecutor.removeAllMessageStoreThreads(); + } else if (operation.type === 'remove_messages_for_threads') { + const { threadIDs } = operation.payload; + sqliteQueryExecutor.removeMessagesForThreads(threadIDs); + } else { + throw new Error('Unsupported message operation'); + } + } catch (e) { + throw new Error( + `Error while processing ${ + operation.type + } message operation: ${getProcessingStoreOpsExceptionMessage( + e, + module, + )}`, + ); + } + } +} + function processUserStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation of operations) { try { if (operation.type === 'remove_users') { const { ids } = operation.payload; sqliteQueryExecutor.removeUsers(ids); } else if (operation.type === 'replace_user') { const user = operation.payload; sqliteQueryExecutor.replaceUser(user); } else if (operation.type === 'remove_all_users') { sqliteQueryExecutor.removeAllUsers(); } else { throw new Error('Unsupported user operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } user operation: ${getProcessingStoreOpsExceptionMessage(e, module)}`, ); } } } function processDBStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, storeOperations: ClientDBStoreOperations, module: EmscriptenModule, ) { const { draftStoreOperations, reportStoreOperations, threadStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, userStoreOperations, + messageStoreOperations, } = storeOperations; try { sqliteQueryExecutor.beginTransaction(); if (draftStoreOperations && draftStoreOperations.length > 0) { processDraftStoreOperations( sqliteQueryExecutor, draftStoreOperations, module, ); } if (reportStoreOperations && reportStoreOperations.length > 0) { processReportStoreOperations( sqliteQueryExecutor, reportStoreOperations, module, ); } if (threadStoreOperations && threadStoreOperations.length > 0) { processThreadStoreOperations( sqliteQueryExecutor, threadStoreOperations, module, ); } if (keyserverStoreOperations && keyserverStoreOperations.length > 0) { processKeyserverStoreOperations( sqliteQueryExecutor, keyserverStoreOperations, module, ); } if (communityStoreOperations && communityStoreOperations.length > 0) { processCommunityStoreOperations( sqliteQueryExecutor, communityStoreOperations, module, ); } if (integrityStoreOperations && integrityStoreOperations.length > 0) { processIntegrityStoreOperations( sqliteQueryExecutor, integrityStoreOperations, module, ); } if ( syncedMetadataStoreOperations && syncedMetadataStoreOperations.length > 0 ) { processSyncedMetadataStoreOperations( sqliteQueryExecutor, syncedMetadataStoreOperations, module, ); } if (auxUserStoreOperations && auxUserStoreOperations.length > 0) { processAuxUserStoreOperations( sqliteQueryExecutor, auxUserStoreOperations, module, ); } if (userStoreOperations && userStoreOperations.length > 0) { processUserStoreOperations( sqliteQueryExecutor, userStoreOperations, module, ); } + if (messageStoreOperations && messageStoreOperations.length > 0) { + processMessageStoreOperations( + sqliteQueryExecutor, + messageStoreOperations, + module, + ); + } sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); console.log('Error while processing store ops: ', e); throw e; } } function processAuxUserStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, operations: $ReadOnlyArray, module: EmscriptenModule, ) { for (const operation: ClientDBAuxUserStoreOperation of operations) { try { if (operation.type === 'remove_all_aux_user_infos') { sqliteQueryExecutor.removeAllAuxUserInfos(); } else if (operation.type === 'remove_aux_user_infos') { const { ids } = operation.payload; sqliteQueryExecutor.removeAuxUserInfos(ids); } else if (operation.type === 'replace_aux_user_info') { const { id, auxUserInfo } = operation.payload; sqliteQueryExecutor.replaceAuxUserInfo({ id, auxUserInfo }); } else { throw new Error('Unsupported aux user operation'); } } catch (e) { throw new Error( `Error while processing ${ operation.type } aux user operation: ${getProcessingStoreOpsExceptionMessage( e, module, )}`, ); } } } function getClientStoreFromQueryExecutor( sqliteQueryExecutor: SQLiteQueryExecutor, ): ClientDBStore { return { drafts: sqliteQueryExecutor.getAllDrafts(), - messages: [], + messages: sqliteQueryExecutor + .getAllMessagesWeb() + .map(webMessageToClientDBMessageInfo), threads: sqliteQueryExecutor .getAllThreadsWeb() - .map(t => webThreadToClientDBThreadInfo(t)), - messageStoreThreads: [], + .map(webThreadToClientDBThreadInfo), + messageStoreThreads: sqliteQueryExecutor + .getAllMessageStoreThreads() + .map(({ id, startReached }) => ({ + id, + start_reached: startReached.toString(), + })), reports: sqliteQueryExecutor.getAllReports(), users: sqliteQueryExecutor.getAllUsers(), keyservers: sqliteQueryExecutor.getAllKeyservers(), communities: sqliteQueryExecutor.getAllCommunities(), integrityThreadHashes: sqliteQueryExecutor.getAllIntegrityThreadHashes(), syncedMetadata: sqliteQueryExecutor.getAllSyncedMetadata(), auxUserInfos: sqliteQueryExecutor.getAllAuxUserInfos(), }; } export { processDBStoreOperations, getProcessingStoreOpsExceptionMessage, getClientStoreFromQueryExecutor, }; diff --git a/web/types/redux-types.js b/web/types/redux-types.js index 1592394c3..1fd8a7931 100644 --- a/web/types/redux-types.js +++ b/web/types/redux-types.js @@ -1,41 +1,43 @@ // @flow import type { EntryStore, CalendarQuery } from 'lib/types/entry-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { WebInitialKeyserverInfo } from 'lib/types/redux-types.js'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserInfos } from 'lib/types/user-types.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; export type InitialReduxState = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +dataLoaded: boolean, +actualizedCalendarQuery: CalendarQuery, +keyserverInfos: { +[keyserverID: string]: WebInitialKeyserverInfo }, }; export type InitialReduxStateActionPayload = $ReadOnly<{ ...InitialReduxState, +threadStore?: ThreadStore, +userInfos?: UserInfos, + +messageStore?: MessageStore, }>; export type ExcludedData = { +userStore?: boolean, + +messageStore?: boolean, +threadStore?: boolean, }; export type InitialReduxStateRequest = { +urlInfo: URLInfo, +excludedData: ExcludedData, +clientUpdatesCurrentAsOf: number, };