diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js --- a/keyserver/src/responders/redux-state-responders.js +++ b/keyserver/src/responders/redux-state-responders.js @@ -69,6 +69,7 @@ const excludedDataValidator: TInterface = tShape({ userStore: t.maybe(t.Bool), + messageStore: t.maybe(t.Bool), threadStore: t.maybe(t.Bool), }); @@ -220,6 +221,17 @@ ); 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, @@ -376,7 +388,7 @@ entryStore: entryStorePromise, threadStore: threadStorePromise, userInfos: finalUserInfosPromise, - messageStore: messageStorePromise, + messageStore: finalMessageStorePromise, pushApiPublicKey: pushApiPublicKeyPromise, inviteLinksStore: inviteLinksStorePromise, keyserverInfo: keyserverInfoPromise, diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -5,6 +5,7 @@ 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'; @@ -77,6 +78,7 @@ urlInfo, excludedData: { threadStore: !!clientDBStore.threadStore, + messageStore: !!clientDBStore.messages, userStore: !!clientDBStore.users, }, allUpdatesCurrentAsOf, @@ -126,15 +128,35 @@ })); } + 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: [], diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -15,6 +15,7 @@ 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'; @@ -51,6 +52,7 @@ 'keyserverStore', 'globalThemeInfo', 'customServer', + 'messageStore', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { @@ -416,7 +418,7 @@ 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 --- a/web/shared-worker/types/entities.js +++ b/web/shared-worker/types/entities.js @@ -1,7 +1,10 @@ // @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, @@ -43,6 +46,19 @@ }; } +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 { @@ -99,4 +115,68 @@ 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 --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -15,7 +15,7 @@ type NullableInt, } from './entities.js'; -type WebMessage = { +export type WebMessage = { +id: string, +localID: NullableString, +thread: string, @@ -26,7 +26,7 @@ +time: string, }; -type Media = { +export type Media = { +id: string, +container: string, +thread: string, diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -4,6 +4,7 @@ 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'; @@ -105,6 +106,21 @@ 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; } @@ -122,6 +138,7 @@ syncedMetadataStoreOperations, auxUserStoreOperations, userStoreOperations, + messageStoreOperations, } = storeOperations; const canUseDatabase = canUseDatabaseOnWeb(userID); @@ -145,6 +162,8 @@ auxUserStoreOpsHandlers.convertOpsToClientDBOps(auxUserStoreOperations); const convertedUserStoreOperations = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); + const convertedMessageStoreOperations = + messageStoreOpsHandlers.convertOpsToClientDBOps(messageStoreOperations); if ( convertedThreadStoreOperations.length === 0 && @@ -155,7 +174,8 @@ convertedIntegrityStoreOperations.length === 0 && convertedSyncedMetadataStoreOperations.length === 0 && convertedAuxUserStoreOperations.length === 0 && - convertedUserStoreOperations.length === 0 + convertedUserStoreOperations.length === 0 && + convertedMessageStoreOperations.length === 0 ) { return; } @@ -178,6 +198,7 @@ syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, auxUserStoreOperations: convertedAuxUserStoreOperations, userStoreOperations: convertedUserStoreOperations, + messageStoreOperations: convertedMessageStoreOperations, }, }); } catch (e) { diff --git a/web/shared-worker/worker/process-operations.js b/web/shared-worker/worker/process-operations.js --- a/web/shared-worker/worker/process-operations.js +++ b/web/shared-worker/worker/process-operations.js @@ -4,6 +4,7 @@ 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'; @@ -21,6 +22,8 @@ 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'; @@ -256,6 +259,63 @@ } } +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, @@ -299,6 +359,7 @@ syncedMetadataStoreOperations, auxUserStoreOperations, userStoreOperations, + messageStoreOperations, } = storeOperations; try { @@ -369,6 +430,13 @@ module, ); } + if (messageStoreOperations && messageStoreOperations.length > 0) { + processMessageStoreOperations( + sqliteQueryExecutor, + messageStoreOperations, + module, + ); + } sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -413,11 +481,18 @@ ): 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(), diff --git a/web/types/redux-types.js b/web/types/redux-types.js --- a/web/types/redux-types.js +++ b/web/types/redux-types.js @@ -27,10 +27,12 @@ ...InitialReduxState, +threadStore?: ThreadStore, +userInfos?: UserInfos, + +messageStore?: MessageStore, }>; export type ExcludedData = { +userStore?: boolean, + +messageStore?: boolean, +threadStore?: boolean, };