diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 94e8befb2..638a12113 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,364 +1,360 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import { type CreateMessageInfoParams, type MessageSpec, pushTypes, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo, MessageInfo, } from '../../types/message-types.js'; import { type RawLegacyUpdateRelationshipMessageInfo, rawLegacyUpdateRelationshipMessageInfoValidator, type LegacyUpdateRelationshipMessageData, type LegacyUpdateRelationshipMessageInfo, } from '../../types/messages/legacy-update-relationship.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, UpdateRelationshipMessageInfo, } from '../../types/messages/update-relationship.js'; import { rawUpdateRelationshipMessageInfoValidator } from '../../types/messages/update-relationship.js'; import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { type EntityText, ET } from '../../utils/entity-text.js'; -import { - NEXT_CODE_VERSION, - FUTURE_CODE_VERSION, - hasMinCodeVersion, -} from '../version-utils.js'; +import { NEXT_CODE_VERSION, hasMinCodeVersion } from '../version-utils.js'; type UpdateRelationshipMessageSpec = MessageSpec< LegacyUpdateRelationshipMessageData | UpdateRelationshipMessageData, RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo, LegacyUpdateRelationshipMessageInfo | UpdateRelationshipMessageInfo, > & { // We need to explicitly type this as non-optional so that // it can be referenced from messageContentForClientDB below +messageContentForServerDB: ( data: | LegacyUpdateRelationshipMessageData | RawLegacyUpdateRelationshipMessageInfo | UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo, ) => string, ... }; export const updateRelationshipMessageSpec: UpdateRelationshipMessageSpec = Object.freeze({ messageContentForServerDB( data: | LegacyUpdateRelationshipMessageData | RawLegacyUpdateRelationshipMessageInfo | UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo, ): string { if ( data.type === messageTypes.UPDATE_RELATIONSHIP && data.operation === 'farcaster_mutual' ) { return JSON.stringify({ operation: data.operation, targetID: data.targetID, creatorFID: data.creatorFID, targetFID: data.targetFID, }); } return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, messageContentForClientDB( data: | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo, ): string { return updateRelationshipMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow( row: Object, ): | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo { const content = JSON.parse(row.content); if (content.operation === 'farcaster_mutual') { return { type: row.type, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetID: content.targetID, operation: content.operation, creatorFID: content.creatorFID, targetFID: content.targetFID, }; } return { type: row.type, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetID: content.targetID, operation: content.operation, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for UpdateRelationship', ); const content = JSON.parse(clientDBMessageInfo.content); const messageType = parseInt(clientDBMessageInfo.type); if (messageType === messageTypes.LEGACY_UPDATE_RELATIONSHIP) { return { type: messageTypes.LEGACY_UPDATE_RELATIONSHIP, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetID: content.targetID, operation: content.operation, }; } else if (content.operation === 'farcaster_mutual') { return { type: messageTypes.UPDATE_RELATIONSHIP, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetID: content.targetID, operation: content.operation, creatorFID: content.creatorFID, targetFID: content.targetFID, }; } else { return { type: messageTypes.UPDATE_RELATIONSHIP, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, targetID: content.targetID, operation: content.operation, }; } }, createMessageInfo( rawMessageInfo: | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?LegacyUpdateRelationshipMessageInfo | ?UpdateRelationshipMessageInfo { const target = params.createRelativeUserInfos([ rawMessageInfo.targetID, ])[0]; if (!target) { return null; } if (rawMessageInfo.type === messageTypes.LEGACY_UPDATE_RELATIONSHIP) { return { type: messageTypes.LEGACY_UPDATE_RELATIONSHIP, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, target, time: rawMessageInfo.time, operation: rawMessageInfo.operation, }; } else if (rawMessageInfo.operation === 'farcaster_mutual') { return { type: messageTypes.UPDATE_RELATIONSHIP, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, creatorFID: rawMessageInfo.creatorFID, target, targetFID: rawMessageInfo.targetFID, time: rawMessageInfo.time, operation: rawMessageInfo.operation, }; } else { return { type: messageTypes.UPDATE_RELATIONSHIP, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, target, time: rawMessageInfo.time, operation: rawMessageInfo.operation, }; } }, rawMessageInfoFromMessageData( messageData: | LegacyUpdateRelationshipMessageData | UpdateRelationshipMessageData, id: ?string, ): | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo { invariant(id, 'RawUpdateRelationshipMessageInfo needs id'); return { ...messageData, id }; }, // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return robotext( messageInfo: | LegacyUpdateRelationshipMessageInfo | UpdateRelationshipMessageInfo, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); if (messageInfo.operation === 'request_sent') { const target = ET.user({ userInfo: messageInfo.target }); return ET`${creator} sent ${target} a friend request`; } else if (messageInfo.operation === 'request_accepted') { const targetPossessive = ET.user({ userInfo: messageInfo.target, possessive: true, }); return ET`${creator} accepted ${targetPossessive} friend request`; } else if (messageInfo.operation === 'farcaster_mutual') { const viewerIsCreator = messageInfo.creator.isViewer; if (viewerIsCreator) { const otherUser = ET.user({ userInfo: messageInfo.target }); return ET`${otherUser} is ${ET.fcUser({ fid: messageInfo.targetFID, })} on Farcaster`; } else { return ET`${creator} is ${ET.fcUser({ fid: messageInfo.creatorFID, })} on Farcaster`; } } invariant( false, `Invalid operation ${messageInfo.operation} ` + `of message with type ${messageInfo.type}`, ); }, shimUnsupportedMessageInfo( rawMessageInfo: | RawUpdateRelationshipMessageInfo | RawLegacyUpdateRelationshipMessageInfo, platformDetails: ?PlatformDetails, ): | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo | RawUnsupportedMessageInfo { if (rawMessageInfo.type === messageTypes.LEGACY_UPDATE_RELATIONSHIP) { return rawMessageInfo; } else if ( rawMessageInfo.type === messageTypes.UPDATE_RELATIONSHIP && hasMinCodeVersion(platformDetails, { native: NEXT_CODE_VERSION, - web: FUTURE_CODE_VERSION, + web: NEXT_CODE_VERSION, }) ) { return rawMessageInfo; } else if ( rawMessageInfo.type === messageTypes.UPDATE_RELATIONSHIP && rawMessageInfo.operation !== 'farcaster_mutual' ) { return { ...rawMessageInfo, type: messageTypes.LEGACY_UPDATE_RELATIONSHIP, operation: rawMessageInfo.operation, }; } else { return { type: messageTypes.UNSUPPORTED, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'Your connection on Farcaster is on Comm.', unsupportedMessageInfo: rawMessageInfo, dontPrefixCreator: true, }; } }, unshimMessageInfo( unwrapped: | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo, ): | RawLegacyUpdateRelationshipMessageInfo | RawUpdateRelationshipMessageInfo { return unwrapped; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); if ( messageInfo.type === messageTypes.UPDATE_RELATIONSHIP && messageInfo.operation === 'farcaster_mutual' ) { const title = threadInfo.uiName; const prefix = ET`${ET.fcUser({ fid: messageInfo.creatorFID })}`; const body = 'from Farcaster is on Comm'; const merged = ET`${prefix} ${body}`; return { merged, body, title, prefix }; } const creator = ET.user({ userInfo: messageInfo.creator }); const prefix = ET`${creator}`; const title = threadInfo.uiName; let body; if (messageInfo.operation === 'request_sent') { body = 'sent you a friend request'; } else if (messageInfo.operation === 'request_accepted') { body = 'accepted your friend request'; } else { invariant(false, 'unknown messageInfo.operation in notificationTexts'); } const merged = ET`${prefix} ${body}`; return { merged, body, title, prefix }; }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: t.union([ rawLegacyUpdateRelationshipMessageInfoValidator, rawUpdateRelationshipMessageInfoValidator, ]), }); diff --git a/web/redux/persist.js b/web/redux/persist.js index 79b105986..148318dc6 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,519 +1,522 @@ // @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 type { ClientDBMessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { patchRawThreadInfoWithSpecialRole } from 'lib/permissions/special-roles.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 { defaultAlertInfos } from 'lib/types/alert-types.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import type { ClientDBMessageInfo } from 'lib/types/message-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 type { ClientDBThreadInfo } from 'lib/types/thread-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 { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from 'lib/utils/thread-ops-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import { handleReduxMigrationFailure, persistWhitelist, } from './handle-redux-migration-failure.js'; import { rootKey, rootKeyPrefix } from './persist-constants.js'; import type { AppState } from './redux-setup.js'; +import { unshimClientDB } from './unshim-utils.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 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); } }, [15]: (state: any) => { const { notifPermissionAlertInfo, ...rest } = state; const newState = { ...rest, alertStore: { alertInfos: defaultAlertInfos, }, }; return newState; }, [16]: async (state: AppState) => { // 1. Check if `databaseModule` is supported and early-exit if not. const sharedWorker = await getCommSharedWorker(); const isDatabaseSupported = await sharedWorker.isSupported(); if (!isDatabaseSupported) { return state; } // 2. Get existing `stores` from SQLite. const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const messages: ?$ReadOnlyArray = stores?.store?.messages; if (messages === null || messages === undefined || messages.length === 0) { return state; } // 3. Filter out `UNSUPPORTED.UPDATE_RELATIONSHIP` `ClientDBMessageInfo`s. const unsupportedMessageIDsToRemove = messages .filter((message: ClientDBMessageInfo) => { if (parseInt(message.type) !== messageTypes.UPDATE_RELATIONSHIP) { return false; } if (message.content === null || message.content === undefined) { return false; } const { operation } = JSON.parse(message.content); return operation === 'farcaster_mutual'; }) .map(message => message.id); // 4. Construct `ClientDBMessageStoreOperation`s const messageStoreOperations: $ReadOnlyArray = [ { type: 'remove', payload: { ids: unsupportedMessageIDsToRemove }, }, ]; // 5. Process the constructed `messageStoreOperations`. await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { messageStoreOperations }, }); return state; }, [17]: async (state: AppState) => { // 1. Check if `databaseModule` is supported and early-exit if not. const sharedWorker = await getCommSharedWorker(); const isDatabaseSupported = await sharedWorker.isSupported(); if (!isDatabaseSupported) { return state; } // 2. Get existing `stores` from SQLite. const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const threads: ?$ReadOnlyArray = stores?.store?.threads; if (threads === null || threads === undefined || threads.length === 0) { return state; } // 3. Convert to `RawThreadInfo`, patch in `specialRole`, and convert back. const patchedClientDBThreadInfos: $ReadOnlyArray = threads .map(convertClientDBThreadInfoToRawThreadInfo) .map(patchRawThreadInfoWithSpecialRole) .map(convertRawThreadInfoToClientDBThreadInfo); // 4. Construct operations to remove existing threads and replace them // with threads that have the `specialRole` field patched in. const threadStoreOperations: ClientDBThreadStoreOperation[] = []; threadStoreOperations.push({ type: 'remove_all' }); for (const clientDBThreadInfo: ClientDBThreadInfo of patchedClientDBThreadInfos) { threadStoreOperations.push({ type: 'replace', payload: clientDBThreadInfo, }); } // 5. Process the constructed `threadStoreOperations`. await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { threadStoreOperations }, }); return state; }, + [18]: (state: AppState) => + unshimClientDB(state, [messageTypes.UPDATE_RELATIONSHIP]), }; 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: 17, transforms: [messageStoreMessagesBlocklistTransform, keyserverStoreTransform], }; export { persistConfig }; diff --git a/web/redux/unshim-utils.js b/web/redux/unshim-utils.js new file mode 100644 index 000000000..5d12ef4c6 --- /dev/null +++ b/web/redux/unshim-utils.js @@ -0,0 +1,96 @@ +// @flow + +import { + type MessageStoreOperation, + messageStoreOpsHandlers, +} from 'lib/ops/message-store-ops.js'; +import { messageID } from 'lib/shared/message-utils.js'; +import { unshimFunc } from 'lib/shared/unshim-utils.js'; +import { type MessageType } from 'lib/types/message-types-enum.js'; +import type { + RawMessageInfo, + ClientDBMessageInfo, +} from 'lib/types/message-types.js'; +import { translateClientDBMessageInfoToRawMessageInfo } from 'lib/utils/message-ops-utils.js'; + +import { handleReduxMigrationFailure } from './handle-redux-migration-failure.js'; +import type { AppState } from './redux-setup.js'; +import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; +import { workerRequestMessageTypes } from '../types/worker-types.js'; + +const { + processStoreOperations: processMessageStoreOperations, + convertOpsToClientDBOps: convertMessageOpsToClientDBOps, +} = messageStoreOpsHandlers; + +async function unshimClientDB( + state: AppState, + unshimTypes: $ReadOnlyArray, +): Promise { + // 1. Check if `databaseModule` is supported and early-exit if not. + const sharedWorker = await getCommSharedWorker(); + const isDatabaseSupported = await sharedWorker.isSupported(); + + if (!isDatabaseSupported) { + return state; + } + + // 2. Get existing `stores` from SQLite. + const stores = await sharedWorker.schedule({ + type: workerRequestMessageTypes.GET_CLIENT_STORE, + }); + + const messages: ?$ReadOnlyArray = + stores?.store?.messages; + + if (messages === null || messages === undefined || messages.length === 0) { + return state; + } + + // 3. Translate `ClientDBMessageInfo`s to `RawMessageInfo`s. + const rawMessageInfos = messages.map( + translateClientDBMessageInfoToRawMessageInfo, + ); + + // 4. "Unshim" translated `RawMessageInfo`s. + const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => + unshimFunc(messageInfo, new Set(unshimTypes)), + ); + + // 5. Construct `MessageStoreOperation`s to clear SQLite `messages` table and + // repopulate with unshimmed `RawMessageInfo`s. + const operations: $ReadOnlyArray = [ + { + type: 'remove_all', + }, + ...unshimmedRawMessageInfos.map((message: RawMessageInfo) => ({ + type: 'replace', + payload: { id: messageID(message), messageInfo: message }, + })), + ]; + + // 6. Process the constructed `messageStoreOperations`. + try { + const convertedMessageStoreOperations = + convertMessageOpsToClientDBOps(operations); + await sharedWorker.schedule({ + type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, + storeOperations: { + messageStoreOperations: convertedMessageStoreOperations, + }, + }); + const processedMessageStore = processMessageStoreOperations( + state.messageStore, + operations, + ); + return { + ...state, + messageStore: processedMessageStore, + }; + } catch (e) { + console.log(e); + return handleReduxMigrationFailure(state); + } +} + +export { unshimClientDB };