diff --git a/lib/ops/aux-user-store-ops.js b/lib/ops/aux-user-store-ops.js index e4485b01c..9b10b7fc2 100644 --- a/lib/ops/aux-user-store-ops.js +++ b/lib/ops/aux-user-store-ops.js @@ -1,123 +1,126 @@ // @flow import type { BaseStoreOpsHandlers } from './base-ops.js'; import type { AuxUserInfo, AuxUserInfos, AuxUserStore, } from '../types/aux-user-types.js'; // client types export type ReplaceAuxUserInfoOperation = { +type: 'replace_aux_user_info', +payload: { +id: string, +auxUserInfo: AuxUserInfo }, }; export type RemoveAuxUserInfosOperation = { +type: 'remove_aux_user_infos', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllAuxUserInfosOperation = { +type: 'remove_all_aux_user_infos', }; export type AuxUserStoreOperation = | ReplaceAuxUserInfoOperation | RemoveAuxUserInfosOperation | RemoveAllAuxUserInfosOperation; // SQLite types export type ClientDBAuxUserInfo = { +id: string, +auxUserInfo: string, }; export type ClientDBReplaceAuxUserOperation = { +type: 'replace_aux_user_info', +payload: ClientDBAuxUserInfo, }; export type ClientDBAuxUserStoreOperation = | ClientDBReplaceAuxUserOperation | RemoveAuxUserInfosOperation | RemoveAllAuxUserInfosOperation; function convertAuxUserInfoToClientDBAuxUserInfo({ id, auxUserInfo, }: { +id: string, +auxUserInfo: AuxUserInfo, }): ClientDBAuxUserInfo { return { id, auxUserInfo: JSON.stringify(auxUserInfo), }; } const auxUserStoreOpsHandlers: BaseStoreOpsHandlers< AuxUserStore, AuxUserStoreOperation, ClientDBAuxUserStoreOperation, AuxUserInfos, ClientDBAuxUserInfo, > = { processStoreOperations( auxUserStore: AuxUserStore, ops: $ReadOnlyArray, ): AuxUserStore { if (ops.length === 0) { return auxUserStore; } let processedAuxUserInfos = { ...auxUserStore.auxUserInfos }; for (const operation of ops) { if (operation.type === 'replace_aux_user_info') { processedAuxUserInfos[operation.payload.id] = operation.payload.auxUserInfo; } else if (operation.type === 'remove_aux_user_infos') { for (const id of operation.payload.ids) { delete processedAuxUserInfos[id]; } } else if (operation.type === 'remove_all_aux_user_infos') { processedAuxUserInfos = {}; } } return { ...auxUserStore, auxUserInfos: processedAuxUserInfos }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(operation => { if ( operation.type === 'remove_aux_user_infos' || operation.type === 'remove_all_aux_user_infos' ) { return operation; } return { type: 'replace_aux_user_info', payload: convertAuxUserInfoToClientDBAuxUserInfo(operation.payload), }; }); }, translateClientDBData( clientDBAuxUserInfos: $ReadOnlyArray, ): AuxUserInfos { const auxUserInfos: { [id: string]: AuxUserInfo } = {}; clientDBAuxUserInfos.forEach(dbAuxUserInfo => { auxUserInfos[dbAuxUserInfo.id] = JSON.parse(dbAuxUserInfo.auxUserInfo); }); return auxUserInfos; }, }; export { auxUserStoreOpsHandlers, convertAuxUserInfoToClientDBAuxUserInfo }; diff --git a/lib/ops/base-ops.js b/lib/ops/base-ops.js index 297dcf4b1..146263ad5 100644 --- a/lib/ops/base-ops.js +++ b/lib/ops/base-ops.js @@ -1,18 +1,18 @@ // @flow export type BaseStoreOpsHandlers< Store, Operation, ClientDBOperation, DataType, ClientDBDataType, > = { processStoreOperations: ( store: Store, ops: $ReadOnlyArray, ) => Store, convertOpsToClientDBOps: ( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ) => $ReadOnlyArray, translateClientDBData: (data: $ReadOnlyArray) => DataType, }; diff --git a/lib/ops/community-store-ops.js b/lib/ops/community-store-ops.js index ce567f6cb..950f4f8cd 100644 --- a/lib/ops/community-store-ops.js +++ b/lib/ops/community-store-ops.js @@ -1,126 +1,129 @@ // @flow import type { BaseStoreOpsHandlers } from './base-ops.js'; import type { CommunityInfo, CommunityInfos, CommunityStore, } from '../types/community-types.js'; // client types export type ReplaceCommunityOperation = { +type: 'replace_community', +payload: { +id: string, +communityInfo: CommunityInfo }, }; export type RemoveCommunitiesOperation = { +type: 'remove_communities', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllCommunitiesOperation = { +type: 'remove_all_communities', }; export type CommunityStoreOperation = | ReplaceCommunityOperation | RemoveCommunitiesOperation | RemoveAllCommunitiesOperation; // SQLite types export type ClientDBCommunityInfo = { +id: string, +communityInfo: string, }; export type ClientDBReplaceCommunityOperation = { +type: 'replace_community', +payload: ClientDBCommunityInfo, }; export type ClientDBCommunityStoreOperation = | ClientDBReplaceCommunityOperation | RemoveCommunitiesOperation | RemoveAllCommunitiesOperation; function convertCommunityInfoToClientDBCommunityInfo({ id, communityInfo, }: { +id: string, +communityInfo: CommunityInfo, }): ClientDBCommunityInfo { return { id, communityInfo: JSON.stringify(communityInfo), }; } const communityStoreOpsHandlers: BaseStoreOpsHandlers< CommunityStore, CommunityStoreOperation, ClientDBCommunityStoreOperation, CommunityInfos, ClientDBCommunityInfo, > = { processStoreOperations( communityStore: CommunityStore, ops: $ReadOnlyArray, ): CommunityStore { if (ops.length === 0) { return communityStore; } let processedCommunityInfos = { ...communityStore.communityInfos }; for (const operation of ops) { if (operation.type === 'replace_community') { processedCommunityInfos[operation.payload.id] = operation.payload.communityInfo; } else if (operation.type === 'remove_communities') { for (const id of operation.payload.ids) { delete processedCommunityInfos[id]; } } else if (operation.type === 'remove_all_communities') { processedCommunityInfos = {}; } } return { ...communityStore, communityInfos: processedCommunityInfos }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(operation => { if ( operation.type === 'remove_communities' || operation.type === 'remove_all_communities' ) { return operation; } return { type: 'replace_community', payload: convertCommunityInfoToClientDBCommunityInfo(operation.payload), }; }); }, translateClientDBData( communites: $ReadOnlyArray, ): CommunityInfos { const communityInfos: { [id: string]: CommunityInfo } = {}; communites.forEach(dbCommunity => { communityInfos[dbCommunity.id] = JSON.parse(dbCommunity.communityInfo); }); return communityInfos; }, }; export { communityStoreOpsHandlers, convertCommunityInfoToClientDBCommunityInfo, }; diff --git a/lib/ops/integrity-store-ops.js b/lib/ops/integrity-store-ops.js index 8699fc57e..1f1b8e0ee 100644 --- a/lib/ops/integrity-store-ops.js +++ b/lib/ops/integrity-store-ops.js @@ -1,128 +1,131 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { ThreadHashes, IntegrityStore } from '../types/integrity-types.js'; import { entries } from '../utils/objects.js'; // client types export type ReplaceIntegrityThreadHashesOperation = { +type: 'replace_integrity_thread_hashes', +payload: { +threadHashes: ThreadHashes }, }; export type RemoveIntegrityThreadHashesOperation = { +type: 'remove_integrity_thread_hashes', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllIntegrityThreadHashesOperation = { +type: 'remove_all_integrity_thread_hashes', }; export type IntegrityStoreOperation = | ReplaceIntegrityThreadHashesOperation | RemoveIntegrityThreadHashesOperation | RemoveAllIntegrityThreadHashesOperation; // SQLite types export type ClientDBIntegrityThreadHash = { +id: string, +threadHash: string, }; export type ClientDBReplaceIntegrityThreadHashOperation = { +type: 'replace_integrity_thread_hashes', +payload: { +threadHashes: $ReadOnlyArray }, }; export type ClientDBIntegrityStoreOperation = | ClientDBReplaceIntegrityThreadHashOperation | RemoveIntegrityThreadHashesOperation | RemoveAllIntegrityThreadHashesOperation; function convertIntegrityThreadHashesToClientDBIntegrityThreadHashes( threadHashes: ThreadHashes, ): $ReadOnlyArray { return entries(threadHashes).map(([id, threadHash]) => ({ id: id, threadHash: threadHash.toString(), })); } const integrityStoreOpsHandlers: BaseStoreOpsHandlers< IntegrityStore, IntegrityStoreOperation, ClientDBIntegrityStoreOperation, ThreadHashes, ClientDBIntegrityThreadHash, > = { processStoreOperations( integrityStore: IntegrityStore, ops: $ReadOnlyArray, ): IntegrityStore { if (ops.length === 0) { return integrityStore; } let processedThreadHashes = { ...integrityStore.threadHashes }; for (const operation: IntegrityStoreOperation of ops) { if (operation.type === 'replace_integrity_thread_hashes') { for (const id in operation.payload.threadHashes) { processedThreadHashes[id] = operation.payload.threadHashes[id]; } } else if (operation.type === 'remove_integrity_thread_hashes') { for (const id of operation.payload.ids) { delete processedThreadHashes[id]; } } else if (operation.type === 'remove_all_integrity_thread_hashes') { processedThreadHashes = {}; } } return { ...integrityStore, threadHashes: processedThreadHashes }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } const convertedOperations = ops.map(integrityStoreOperation => { if ( integrityStoreOperation.type === 'remove_all_integrity_thread_hashes' || integrityStoreOperation.type === 'remove_integrity_thread_hashes' ) { return integrityStoreOperation; } const { threadHashes } = integrityStoreOperation.payload; const dbIntegrityThreadHashes: $ReadOnlyArray = convertIntegrityThreadHashesToClientDBIntegrityThreadHashes( threadHashes, ); if (dbIntegrityThreadHashes.length === 0) { return undefined; } return { type: 'replace_integrity_thread_hashes', payload: { threadHashes: dbIntegrityThreadHashes }, }; }); return convertedOperations.filter(Boolean); }, translateClientDBData( data: $ReadOnlyArray, ): ThreadHashes { return Object.fromEntries( data.map((dbThreadHash: ClientDBIntegrityThreadHash) => [ dbThreadHash.id, Number(dbThreadHash.threadHash), ]), ); }, }; export { integrityStoreOpsHandlers, convertIntegrityThreadHashesToClientDBIntegrityThreadHashes, }; diff --git a/lib/ops/keyserver-store-ops.js b/lib/ops/keyserver-store-ops.js index cc0ae6470..349427d7b 100644 --- a/lib/ops/keyserver-store-ops.js +++ b/lib/ops/keyserver-store-ops.js @@ -1,191 +1,194 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { PersistedKeyserverInfo } from '../shared/transforms/keyserver-store-transform.js'; import { transformKeyserverInfoToPersistedKeyserverInfo, transformPersistedKeyserverInfoToKeyserverInfo, } from '../shared/transforms/keyserver-store-transform.js'; import { type KeyserverInfo, type KeyserverInfos, type KeyserverStore, defaultKeyserverInfo, } from '../types/keyserver-types.js'; import { getConfig } from '../utils/config.js'; // client types export type ReplaceKeyserverOperation = { +type: 'replace_keyserver', +payload: { +id: string, +keyserverInfo: KeyserverInfo }, }; export type RemoveKeyserversOperation = { +type: 'remove_keyservers', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllKeyserversOperation = { +type: 'remove_all_keyservers', }; export type KeyserverStoreOperation = | ReplaceKeyserverOperation | RemoveKeyserversOperation | RemoveAllKeyserversOperation; // SQLite types export type ClientDBKeyserverInfo = { +id: string, +keyserverInfo: string, +syncedKeyserverInfo: string, }; export type ClientDBReplaceKeyserverOperation = { +type: 'replace_keyserver', +payload: ClientDBKeyserverInfo, }; export type ClientDBKeyserverStoreOperation = | ClientDBReplaceKeyserverOperation | RemoveKeyserversOperation | RemoveAllKeyserversOperation; type SyncedKeyserverInfoData = { +urlPrefix: string }; function convertKeyserverInfoToClientDBKeyserverInfo({ id, keyserverInfo, }: { +id: string, +keyserverInfo: KeyserverInfo, }): ClientDBKeyserverInfo { const persistedKeyserverInfo = transformKeyserverInfoToPersistedKeyserverInfo(keyserverInfo); const { urlPrefix, ...nonSyncedData } = persistedKeyserverInfo; const syncedData: SyncedKeyserverInfoData = { urlPrefix }; return { id, keyserverInfo: JSON.stringify(nonSyncedData), syncedKeyserverInfo: JSON.stringify(syncedData), }; } function convertClientDBKeyserverInfoToKeyserverInfo( dbKeyserverInfo: ClientDBKeyserverInfo, ): KeyserverInfo { const persistedSyncedKeyserverInfo: SyncedKeyserverInfoData = JSON.parse( dbKeyserverInfo.syncedKeyserverInfo, ); let persistedKeyserverInfo: PersistedKeyserverInfo; if (dbKeyserverInfo.keyserverInfo.length > 0) { const persistedNonSyncedKeyserverInfo: $Diff< PersistedKeyserverInfo, SyncedKeyserverInfoData, > = JSON.parse(dbKeyserverInfo.keyserverInfo); persistedKeyserverInfo = { ...persistedNonSyncedKeyserverInfo, ...persistedSyncedKeyserverInfo, }; } else { const defaultPersistedNonSyncedKeyserverInfo = transformKeyserverInfoToPersistedKeyserverInfo( defaultKeyserverInfo( persistedSyncedKeyserverInfo.urlPrefix, getConfig().platformDetails.platform, ), ); persistedKeyserverInfo = { ...defaultPersistedNonSyncedKeyserverInfo, ...persistedSyncedKeyserverInfo, }; } return transformPersistedKeyserverInfoToKeyserverInfo(persistedKeyserverInfo); } const keyserverStoreOpsHandlers: BaseStoreOpsHandlers< KeyserverStore, KeyserverStoreOperation, ClientDBKeyserverStoreOperation, KeyserverInfos, ClientDBKeyserverInfo, > = { processStoreOperations( keyserverStore: KeyserverStore, ops: $ReadOnlyArray, ): KeyserverStore { if (ops.length === 0) { return keyserverStore; } let processedKeyserverInfos = { ...keyserverStore.keyserverInfos }; for (const operation: KeyserverStoreOperation of ops) { if (operation.type === 'replace_keyserver') { processedKeyserverInfos[operation.payload.id] = operation.payload.keyserverInfo; } else if (operation.type === 'remove_keyservers') { for (const id of operation.payload.ids) { delete processedKeyserverInfos[id]; } } else if (operation.type === 'remove_all_keyservers') { processedKeyserverInfos = {}; } } return { ...keyserverStore, keyserverInfos: processedKeyserverInfos }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(operation => { if ( operation.type === 'remove_keyservers' || operation.type === 'remove_all_keyservers' ) { return operation; } return { type: 'replace_keyserver', payload: convertKeyserverInfoToClientDBKeyserverInfo(operation.payload), }; }); }, translateClientDBData( keyservers: $ReadOnlyArray, ): KeyserverInfos { const keyserverInfos: { [id: string]: KeyserverInfo } = {}; keyservers.forEach(dbKeyserver => { keyserverInfos[dbKeyserver.id] = convertClientDBKeyserverInfoToKeyserverInfo(dbKeyserver); }); return keyserverInfos; }, }; function getKeyserversToRemoveFromNotifsStore( ops: $ReadOnlyArray, ): $ReadOnlyArray { const idsToRemove: Set = new Set(); for (const op of ops) { if (op.type !== 'remove_keyservers') { continue; } for (const id of op.payload.ids) { idsToRemove.add(id); } } return [...idsToRemove]; } export { keyserverStoreOpsHandlers, convertKeyserverInfoToClientDBKeyserverInfo, getKeyserversToRemoveFromNotifsStore, }; diff --git a/lib/ops/message-store-ops.js b/lib/ops/message-store-ops.js index 64317e437..a7865ab64 100644 --- a/lib/ops/message-store-ops.js +++ b/lib/ops/message-store-ops.js @@ -1,196 +1,199 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { ClientDBMessageInfo, ClientDBThreadMessageInfo, MessageStore, MessageStoreThreads, RawMessageInfo, } from '../types/message-types.js'; import { translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, translateThreadMessageInfoToClientDBThreadMessageInfo, } from '../utils/message-ops-utils.js'; // MessageStore messages ops export type RemoveMessageOperation = { +type: 'remove', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveMessagesForThreadsOperation = { +type: 'remove_messages_for_threads', +payload: { +threadIDs: $ReadOnlyArray }, }; export type ReplaceMessageOperation = { +type: 'replace', +payload: { +id: string, +messageInfo: RawMessageInfo }, }; export type RekeyMessageOperation = { +type: 'rekey', +payload: { +from: string, +to: string }, }; export type RemoveAllMessagesOperation = { +type: 'remove_all', }; // MessageStore threads ops export type ReplaceMessageStoreThreadsOperation = { +type: 'replace_threads', +payload: { +threads: MessageStoreThreads }, }; export type RemoveMessageStoreThreadsOperation = { +type: 'remove_threads', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveMessageStoreAllThreadsOperation = { +type: 'remove_all_threads', }; export type ClientDBReplaceMessageOperation = { +type: 'replace', +payload: ClientDBMessageInfo, }; export type ClientDBReplaceThreadsOperation = { +type: 'replace_threads', +payload: { +threads: $ReadOnlyArray }, }; export type MessageStoreOperation = | RemoveMessageOperation | ReplaceMessageOperation | RekeyMessageOperation | RemoveMessagesForThreadsOperation | RemoveAllMessagesOperation | ReplaceMessageStoreThreadsOperation | RemoveMessageStoreThreadsOperation | RemoveMessageStoreAllThreadsOperation; export type ClientDBMessageStoreOperation = | RemoveMessageOperation | ClientDBReplaceMessageOperation | RekeyMessageOperation | RemoveMessagesForThreadsOperation | RemoveAllMessagesOperation | ClientDBReplaceThreadsOperation | RemoveMessageStoreThreadsOperation | RemoveMessageStoreAllThreadsOperation; export const messageStoreOpsHandlers: BaseStoreOpsHandlers< MessageStore, MessageStoreOperation, ClientDBMessageStoreOperation, { +[id: string]: RawMessageInfo }, ClientDBMessageInfo, > = { processStoreOperations( store: MessageStore, ops: $ReadOnlyArray, ): MessageStore { if (ops.length === 0) { return store; } let processedMessages = { ...store.messages }; let processedThreads = { ...store.threads }; for (const operation of ops) { if (operation.type === 'replace') { processedMessages[operation.payload.id] = operation.payload.messageInfo; } else if (operation.type === 'remove') { for (const id of operation.payload.ids) { delete processedMessages[id]; } } else if (operation.type === 'remove_messages_for_threads') { for (const msgID in processedMessages) { if ( operation.payload.threadIDs.includes( processedMessages[msgID].threadID, ) ) { delete processedMessages[msgID]; } } } else if (operation.type === 'rekey') { processedMessages[operation.payload.to] = processedMessages[operation.payload.from]; delete processedMessages[operation.payload.from]; } else if (operation.type === 'remove_all') { processedMessages = {}; } else if (operation.type === 'replace_threads') { for (const threadID in operation.payload.threads) { processedThreads[threadID] = operation.payload.threads[threadID]; } } else if (operation.type === 'remove_threads') { for (const id of operation.payload.ids) { delete processedThreads[id]; } } else if (operation.type === 'remove_all_threads') { processedThreads = {}; } } return { ...store, threads: processedThreads, messages: processedMessages, }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } const convertedOperations = ops.map(messageStoreOperation => { if (messageStoreOperation.type === 'replace') { return { type: 'replace', payload: translateRawMessageInfoToClientDBMessageInfo( messageStoreOperation.payload.messageInfo, ), }; } if (messageStoreOperation.type !== 'replace_threads') { return messageStoreOperation; } const threadMessageInfo: MessageStoreThreads = messageStoreOperation.payload.threads; const dbThreadMessageInfos: ClientDBThreadMessageInfo[] = []; for (const threadID in threadMessageInfo) { dbThreadMessageInfos.push( translateThreadMessageInfoToClientDBThreadMessageInfo( threadID, threadMessageInfo[threadID], ), ); } if (dbThreadMessageInfos.length === 0) { return undefined; } return { type: 'replace_threads', payload: { threads: dbThreadMessageInfos, }, }; }); return convertedOperations.filter(Boolean); }, translateClientDBData(data: $ReadOnlyArray): { +[id: string]: RawMessageInfo, } { return Object.fromEntries( data.map((dbMessageInfo: ClientDBMessageInfo) => [ dbMessageInfo.id, translateClientDBMessageInfoToRawMessageInfo(dbMessageInfo), ]), ); }, }; diff --git a/lib/ops/report-store-ops.js b/lib/ops/report-store-ops.js index 8bc727daf..4b65ada83 100644 --- a/lib/ops/report-store-ops.js +++ b/lib/ops/report-store-ops.js @@ -1,119 +1,122 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; export type ReplaceQueuedReportOperation = { +type: 'replace_report', +payload: { +report: ClientReportCreationRequest }, }; export type RemoveQueuedReportsOperation = { +type: 'remove_reports', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllQueuedReportsOperation = { +type: 'remove_all_reports', }; export type ReportStoreOperation = | ReplaceQueuedReportOperation | RemoveQueuedReportsOperation | RemoveAllQueuedReportsOperation; export type ClientDBReplaceQueuedReportOperation = { +type: 'replace_report', +payload: ClientDBReport, }; export type ClientDBReportStoreOperation = | ClientDBReplaceQueuedReportOperation | RemoveQueuedReportsOperation | RemoveAllQueuedReportsOperation; export type ClientDBReport = { +id: string, +report: string }; function convertReportsToReplaceReportOps( reports: $ReadOnlyArray, ): $ReadOnlyArray { return reports.map(report => ({ type: 'replace_report', payload: { report }, })); } function convertReportsToRemoveReportsOperation( reports: $ReadOnlyArray, ): RemoveQueuedReportsOperation { return { type: 'remove_reports', payload: { ids: reports.map(report => report.id) }, }; } export const reportStoreOpsHandlers: BaseStoreOpsHandlers< $ReadOnlyArray, ReportStoreOperation, ClientDBReportStoreOperation, $ReadOnlyArray, ClientDBReport, > = { processStoreOperations( queuedReports: $ReadOnlyArray, ops: $ReadOnlyArray, ): $ReadOnlyArray { if (ops.length === 0) { return queuedReports; } let processedReports = [...queuedReports]; for (const operation of ops) { if (operation.type === 'replace_report') { const filteredReports = processedReports.filter( report => report.id !== operation.payload.report.id, ); processedReports = [ ...filteredReports, { ...operation.payload.report }, ]; } else if (operation.type === 'remove_reports') { processedReports = processedReports.filter( report => !operation.payload.ids.includes(report.id), ); } else if (operation.type === 'remove_all_reports') { processedReports = []; } } return processedReports; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(operation => { if ( operation.type === 'remove_reports' || operation.type === 'remove_all_reports' ) { return operation; } return { type: 'replace_report', payload: { id: operation.payload.report.id, report: JSON.stringify(operation.payload.report), }, }; }); }, translateClientDBData( data: $ReadOnlyArray, ): $ReadOnlyArray { return data.map(reportRecord => JSON.parse(reportRecord.report)); }, }; export { convertReportsToReplaceReportOps, convertReportsToRemoveReportsOperation, }; diff --git a/lib/ops/synced-metadata-store-ops.js b/lib/ops/synced-metadata-store-ops.js index e674bb334..a608526eb 100644 --- a/lib/ops/synced-metadata-store-ops.js +++ b/lib/ops/synced-metadata-store-ops.js @@ -1,98 +1,101 @@ // @flow import type { BaseStoreOpsHandlers } from './base-ops.js'; import type { SyncedMetadata, SyncedMetadataStore, } from '../types/synced-metadata-types.js'; // client types export type ReplaceSyncedMetadataEntryOperation = { +type: 'replace_synced_metadata_entry', +payload: { +name: string, +data: string, }, }; export type RemoveSyncedMetadataOperation = { +type: 'remove_synced_metadata', +payload: { +names: $ReadOnlyArray }, }; export type RemoveAllSyncedMetadataOperation = { +type: 'remove_all_synced_metadata', }; export type SyncedMetadataStoreOperation = | ReplaceSyncedMetadataEntryOperation | RemoveSyncedMetadataOperation | RemoveAllSyncedMetadataOperation; // SQLite types export type ClientDBSyncedMetadataEntry = { +name: string, +data: string, }; export type ClientDBSyncedMetadataStoreOperation = | ReplaceSyncedMetadataEntryOperation | RemoveSyncedMetadataOperation | RemoveAllSyncedMetadataOperation; const syncedMetadataStoreOpsHandlers: BaseStoreOpsHandlers< SyncedMetadataStore, SyncedMetadataStoreOperation, ClientDBSyncedMetadataStoreOperation, SyncedMetadata, ClientDBSyncedMetadataEntry, > = { processStoreOperations( syncedMetadataStore: SyncedMetadataStore, ops: $ReadOnlyArray, ): SyncedMetadataStore { if (ops.length === 0) { return syncedMetadataStore; } let processedSyncedMetadata = { ...syncedMetadataStore.syncedMetadata }; for (const operation of ops) { if (operation.type === 'replace_synced_metadata_entry') { processedSyncedMetadata[operation.payload.name] = operation.payload.data; } else if (operation.type === 'remove_synced_metadata') { for (const name of operation.payload.names) { delete processedSyncedMetadata[name]; } } else if (operation.type === 'remove_all_synced_metadata') { processedSyncedMetadata = {}; } } return { ...syncedMetadataStore, syncedMetadata: processedSyncedMetadata, }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops; }, translateClientDBData( communites: $ReadOnlyArray, ): SyncedMetadata { const syncedMetadata: { [name: string]: string } = {}; communites.forEach(dbSyncedMetadata => { syncedMetadata[dbSyncedMetadata.name] = dbSyncedMetadata.data; }); return syncedMetadata; }, }; export { syncedMetadataStoreOpsHandlers }; diff --git a/lib/ops/thread-activity-store-ops.js b/lib/ops/thread-activity-store-ops.js index 331aa4349..693785592 100644 --- a/lib/ops/thread-activity-store-ops.js +++ b/lib/ops/thread-activity-store-ops.js @@ -1,136 +1,139 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { ThreadActivityStore, ThreadActivityStoreEntry, } from '../types/thread-activity-types.js'; // client types export type ReplaceThreadActivityEntryOperation = { +type: 'replace_thread_activity_entry', +payload: { +id: string, +threadActivityStoreEntry: ThreadActivityStoreEntry, }, }; export type RemoveThreadActivityEntriesOperation = { +type: 'remove_thread_activity_entries', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllThreadActivityEntriesOperation = { +type: 'remove_all_thread_activity_entries', }; export type ThreadActivityStoreOperation = | ReplaceThreadActivityEntryOperation | RemoveThreadActivityEntriesOperation | RemoveAllThreadActivityEntriesOperation; // SQLite types export type ClientDBThreadActivityEntry = { +id: string, +threadActivityStoreEntry: string, }; export type ClientDBReplaceThreadActivityEntryOperation = { +type: 'replace_thread_activity_entry', +payload: ClientDBThreadActivityEntry, }; export type ClientDBThreadActivityStoreOperation = | ClientDBReplaceThreadActivityEntryOperation | RemoveThreadActivityEntriesOperation | RemoveAllThreadActivityEntriesOperation; function convertThreadActivityEntryToClientDBThreadActivityEntry({ id, threadActivityStoreEntry, }: { +id: string, +threadActivityStoreEntry: ThreadActivityStoreEntry, }): ClientDBThreadActivityEntry { return { id, threadActivityStoreEntry: JSON.stringify(threadActivityStoreEntry), }; } const threadActivityStoreOpsHandlers: BaseStoreOpsHandlers< ThreadActivityStore, ThreadActivityStoreOperation, ClientDBThreadActivityStoreOperation, ThreadActivityStore, ClientDBThreadActivityEntry, > = { processStoreOperations( threadActivityStore: ThreadActivityStore, ops: $ReadOnlyArray, ): ThreadActivityStore { if (ops.length === 0) { return threadActivityStore; } let processedThreadActivityStore = threadActivityStore; for (const operation: ThreadActivityStoreOperation of ops) { if (operation.type === 'replace_thread_activity_entry') { const { id, threadActivityStoreEntry } = operation.payload; processedThreadActivityStore = { ...processedThreadActivityStore, [id]: threadActivityStoreEntry, }; } else if (operation.type === 'remove_thread_activity_entries') { for (const id of operation.payload.ids) { const { [id]: _, ...rest } = processedThreadActivityStore; processedThreadActivityStore = rest; } } else if (operation.type === 'remove_all_thread_activity_entries') { processedThreadActivityStore = {}; } } return processedThreadActivityStore; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(threadActivityStoreOperation => { if ( threadActivityStoreOperation.type === 'remove_all_thread_activity_entries' || threadActivityStoreOperation.type === 'remove_thread_activity_entries' ) { return threadActivityStoreOperation; } const { id, threadActivityStoreEntry } = threadActivityStoreOperation.payload; return { type: 'replace_thread_activity_entry', payload: convertThreadActivityEntryToClientDBThreadActivityEntry({ id, threadActivityStoreEntry, }), }; }); }, translateClientDBData( data: $ReadOnlyArray, ): ThreadActivityStore { return Object.fromEntries( data.map((dbThreadActivityEntry: ClientDBThreadActivityEntry) => [ dbThreadActivityEntry.id, JSON.parse(dbThreadActivityEntry.threadActivityStoreEntry), ]), ); }, }; export { threadActivityStoreOpsHandlers, convertThreadActivityEntryToClientDBThreadActivityEntry, }; diff --git a/lib/ops/thread-store-ops.js b/lib/ops/thread-store-ops.js index c8316a518..e6eacd9f4 100644 --- a/lib/ops/thread-store-ops.js +++ b/lib/ops/thread-store-ops.js @@ -1,99 +1,102 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { ClientDBThreadInfo, RawThreadInfos, ThreadStore, } from '../types/thread-types.js'; import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from '../utils/thread-ops-utils.js'; export type RemoveThreadOperation = { +type: 'remove', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllThreadsOperation = { +type: 'remove_all', }; export type ReplaceThreadOperation = { +type: 'replace', +payload: { +id: string, +threadInfo: RawThreadInfo }, }; export type ThreadStoreOperation = | RemoveThreadOperation | RemoveAllThreadsOperation | ReplaceThreadOperation; export type ClientDBReplaceThreadOperation = { +type: 'replace', +payload: ClientDBThreadInfo, }; export type ClientDBThreadStoreOperation = | RemoveThreadOperation | RemoveAllThreadsOperation | ClientDBReplaceThreadOperation; export const threadStoreOpsHandlers: BaseStoreOpsHandlers< ThreadStore, ThreadStoreOperation, ClientDBThreadStoreOperation, RawThreadInfos, ClientDBThreadInfo, > = { processStoreOperations( store: ThreadStore, ops: $ReadOnlyArray, ): ThreadStore { if (ops.length === 0) { return store; } let processedThreads = { ...store.threadInfos }; for (const operation of ops) { if (operation.type === 'replace') { processedThreads[operation.payload.id] = operation.payload.threadInfo; } else if (operation.type === 'remove') { for (const id of operation.payload.ids) { delete processedThreads[id]; } } else if (operation.type === 'remove_all') { processedThreads = {}; } } return { ...store, threadInfos: processedThreads }; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(threadStoreOperation => { if (threadStoreOperation.type === 'replace') { return { type: 'replace', payload: convertRawThreadInfoToClientDBThreadInfo( threadStoreOperation.payload.threadInfo, ), }; } return threadStoreOperation; }); }, translateClientDBData(data: $ReadOnlyArray): { +[id: string]: RawThreadInfo, } { return Object.fromEntries( data.map((dbThreadInfo: ClientDBThreadInfo) => [ dbThreadInfo.id, convertClientDBThreadInfoToRawThreadInfo(dbThreadInfo), ]), ); }, }; diff --git a/lib/ops/user-store-ops.js b/lib/ops/user-store-ops.js index 5353cdcaf..e245f90bb 100644 --- a/lib/ops/user-store-ops.js +++ b/lib/ops/user-store-ops.js @@ -1,119 +1,122 @@ // @flow import { type BaseStoreOpsHandlers } from './base-ops.js'; import type { UserInfo, UserInfos } from '../types/user-types.js'; import { values } from '../utils/objects.js'; // client types export type ReplaceUserOperation = { +type: 'replace_user', +payload: UserInfo, }; export type RemoveUsersOperation = { +type: 'remove_users', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllUsersOperation = { +type: 'remove_all_users', }; export type UserStoreOperation = | ReplaceUserOperation | RemoveUsersOperation | RemoveAllUsersOperation; // SQLite types export type ClientDBUserInfo = { +id: string, +userInfo: string, }; export type ClientDBReplaceUserOperation = { +type: 'replace_user', +payload: ClientDBUserInfo, }; export type ClientDBUserStoreOperation = | ClientDBReplaceUserOperation | RemoveUsersOperation | RemoveAllUsersOperation; function convertUserInfosToReplaceUserOps( userInfos: UserInfos, ): $ReadOnlyArray { return values(userInfos).map(userInfo => ({ type: 'replace_user', payload: userInfo, })); } function convertUserInfoToClientDBUserInfo(user: UserInfo): ClientDBUserInfo { return { id: user.id, userInfo: JSON.stringify(user), }; } const userStoreOpsHandlers: BaseStoreOpsHandlers< UserInfos, UserStoreOperation, ClientDBUserStoreOperation, UserInfos, ClientDBUserInfo, > = { processStoreOperations( userInfos: UserInfos, ops: $ReadOnlyArray, ): UserInfos { if (ops.length === 0) { return userInfos; } let processedUserInfos = { ...userInfos }; for (const operation: UserStoreOperation of ops) { if (operation.type === 'replace_user') { processedUserInfos[operation.payload.id] = operation.payload; } else if (operation.type === 'remove_users') { for (const id of operation.payload.ids) { delete processedUserInfos[id]; } } else if (operation.type === 'remove_all_users') { processedUserInfos = {}; } } return processedUserInfos; }, convertOpsToClientDBOps( - ops: $ReadOnlyArray, + ops: ?$ReadOnlyArray, ): $ReadOnlyArray { + if (!ops) { + return []; + } return ops.map(operation => { if ( operation.type === 'remove_users' || operation.type === 'remove_all_users' ) { return operation; } return { type: 'replace_user', payload: convertUserInfoToClientDBUserInfo(operation.payload), }; }); }, translateClientDBData(users: $ReadOnlyArray): UserInfos { const userInfos: { [id: string]: UserInfo } = {}; users.forEach(dbUser => { userInfos[dbUser.id] = JSON.parse(dbUser.userInfo); }); return userInfos; }, }; export { userStoreOpsHandlers, convertUserInfosToReplaceUserOps, convertUserInfoToClientDBUserInfo, }; diff --git a/lib/types/store-ops-types.js b/lib/types/store-ops-types.js index 3ba82c32b..ef9d68c82 100644 --- a/lib/types/store-ops-types.js +++ b/lib/types/store-ops-types.js @@ -1,126 +1,126 @@ // @flow import type { AuxUserInfos } from './aux-user-types.js'; import type { CommunityInfos } from './community-types.js'; import type { DraftStoreOperation, ClientDBDraftStoreOperation, ClientDBDraftInfo, } from './draft-types.js'; import type { ThreadHashes } from './integrity-types.js'; import type { KeyserverInfos } from './keyserver-types.js'; import type { ClientDBMessageInfo, ClientDBThreadMessageInfo, } from './message-types.js'; import type { ClientReportCreationRequest } from './report-types.js'; import type { SyncedMetadata } from './synced-metadata-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ClientDBThreadInfo, ThreadStore } from './thread-types.js'; import type { UserInfos } from './user-types.js'; import type { ClientDBAuxUserInfo, ClientDBAuxUserStoreOperation, AuxUserStoreOperation, } from '../ops/aux-user-store-ops.js'; import type { ClientDBCommunityInfo, ClientDBCommunityStoreOperation, CommunityStoreOperation, } from '../ops/community-store-ops.js'; import type { ClientDBIntegrityThreadHash, ClientDBIntegrityStoreOperation, IntegrityStoreOperation, } from '../ops/integrity-store-ops.js'; import type { ClientDBKeyserverInfo, ClientDBKeyserverStoreOperation, KeyserverStoreOperation, } from '../ops/keyserver-store-ops.js'; import type { ClientDBMessageStoreOperation, MessageStoreOperation, } from '../ops/message-store-ops.js'; import type { ReportStoreOperation, ClientDBReport, ClientDBReportStoreOperation, } from '../ops/report-store-ops.js'; import type { ClientDBSyncedMetadataEntry, ClientDBSyncedMetadataStoreOperation, } from '../ops/synced-metadata-store-ops.js'; import type { ThreadActivityStoreOperation, ClientDBThreadActivityEntry, ClientDBThreadActivityStoreOperation, } from '../ops/thread-activity-store-ops.js'; import type { ClientDBThreadStoreOperation, ThreadStoreOperation, } from '../ops/thread-store-ops.js'; import type { ClientDBUserStoreOperation, UserStoreOperation, ClientDBUserInfo, } from '../ops/user-store-ops.js'; export type StoreOperations = { - +draftStoreOperations: $ReadOnlyArray, - +threadStoreOperations: $ReadOnlyArray, - +messageStoreOperations: $ReadOnlyArray, - +reportStoreOperations: $ReadOnlyArray, - +userStoreOperations: $ReadOnlyArray, - +keyserverStoreOperations: $ReadOnlyArray, - +communityStoreOperations: $ReadOnlyArray, - +integrityStoreOperations: $ReadOnlyArray, - +syncedMetadataStoreOperations: $ReadOnlyArray, - +auxUserStoreOperations: $ReadOnlyArray, - +threadActivityStoreOperations: $ReadOnlyArray, + +draftStoreOperations?: $ReadOnlyArray, + +threadStoreOperations?: $ReadOnlyArray, + +messageStoreOperations?: $ReadOnlyArray, + +reportStoreOperations?: $ReadOnlyArray, + +userStoreOperations?: $ReadOnlyArray, + +keyserverStoreOperations?: $ReadOnlyArray, + +communityStoreOperations?: $ReadOnlyArray, + +integrityStoreOperations?: $ReadOnlyArray, + +syncedMetadataStoreOperations?: $ReadOnlyArray, + +auxUserStoreOperations?: $ReadOnlyArray, + +threadActivityStoreOperations?: $ReadOnlyArray, }; export type ClientDBStoreOperations = { +draftStoreOperations?: $ReadOnlyArray, +threadStoreOperations?: $ReadOnlyArray, +messageStoreOperations?: $ReadOnlyArray, +reportStoreOperations?: $ReadOnlyArray, +userStoreOperations?: $ReadOnlyArray, +keyserverStoreOperations?: $ReadOnlyArray, +communityStoreOperations?: $ReadOnlyArray, +integrityStoreOperations?: $ReadOnlyArray, +syncedMetadataStoreOperations?: $ReadOnlyArray, +auxUserStoreOperations?: $ReadOnlyArray, +threadActivityStoreOperations?: $ReadOnlyArray, }; export type ClientDBStore = { +messages: $ReadOnlyArray, +drafts: $ReadOnlyArray, +threads: $ReadOnlyArray, +messageStoreThreads: $ReadOnlyArray, +reports: $ReadOnlyArray, +users: $ReadOnlyArray, +keyservers: $ReadOnlyArray, +communities: $ReadOnlyArray, +integrityThreadHashes: $ReadOnlyArray, +syncedMetadata: $ReadOnlyArray, +auxUserInfos: $ReadOnlyArray, +threadActivityEntries: $ReadOnlyArray, }; export type ClientStore = { +currentUserID: ?string, +drafts: $ReadOnlyArray, +messages: ?$ReadOnlyArray, +threadStore: ?ThreadStore, +messageStoreThreads: ?$ReadOnlyArray, +reports: ?$ReadOnlyArray, +users: ?UserInfos, +keyserverInfos: ?KeyserverInfos, +communityInfos: ?CommunityInfos, +threadHashes: ?ThreadHashes, +syncedMetadata: ?SyncedMetadata, +auxUserInfos: ?AuxUserInfos, +threadActivityStore: ?ThreadActivityStore, }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index db29195a9..7a2e4b0fd 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,472 +1,450 @@ // @flow import { AppState as NativeAppState, Alert } from 'react-native'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { legacySiweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, legacyLogInActionTypes, keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, invalidSessionRecovery, identityInvalidSessionDowngrade, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, setLocalSettingsActionType, } from './action-types.js'; import { setAccessTokenActionType } from './action-types.js'; import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; import { onStateDifference } from './redux-debug-utils.js'; import type { AppState } from './state-types.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { getDevServerHostname } from '../utils/url-utils.js'; function reducer(state: AppState = defaultState, inputAction: Action) { let action = inputAction; if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys: $ReadOnlyArray = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys: $ReadOnlyArray = Object.keys( action.payload ?? {}, ); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( (action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if ( action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, action.payload.keyserverID, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || ((action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setLocalSettingsActionType) { return { ...state, localSettings: { ...state.localSettings, ...action.payload }, }; } else if (action.type === setAccessTokenActionType) { return { ...state, commServicesAccessToken: action.payload }; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo the state // won't be cleared and the current user info determined in baseReducer will // be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return a different // result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we never // keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } const baseReducerResult = baseReducer( state, (action: BaseAction), onStateDifference, ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; - const { - draftStoreOperations, - threadStoreOperations, - messageStoreOperations, - reportStoreOperations, - userStoreOperations, - keyserverStoreOperations, - communityStoreOperations, - integrityStoreOperations, - syncedMetadataStoreOperations, - auxUserStoreOperations, - threadActivityStoreOperations, - } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ - ...threadStoreOperations, + ...(storeOperations.threadStoreOperations ?? []), ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const ops = { - draftStoreOperations, - messageStoreOperations, + ...storeOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, - reportStoreOperations, - userStoreOperations, - keyserverStoreOperations, - communityStoreOperations, - integrityStoreOperations, - syncedMetadataStoreOperations, - auxUserStoreOperations, - threadActivityStoreOperations, }; state = { ...state, dbOpsStore: queueDBOps(state.dbOpsStore, action.messageSourceMetadata, ops), }; return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const activeThreadInfo = state.threadStore.threadInfos[activeThread]; const updatedActiveThreadInfo = { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/redux/redux-utils.js b/native/redux/redux-utils.js index fcbe4fdc6..325ad40d2 100644 --- a/native/redux/redux-utils.js +++ b/native/redux/redux-utils.js @@ -1,114 +1,114 @@ // @flow import { useSelector as reactReduxUseSelector } from 'react-redux'; 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, getKeyserversToRemoveFromNotifsStore, } 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 { threadActivityStoreOpsHandlers } from 'lib/ops/thread-activity-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import { values } from 'lib/utils/objects.js'; import type { AppState } from './state-types.js'; import { commCoreModule } from '../native-modules.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; function useSelector( selector: (state: AppState) => SS, equalityFn?: (a: SS, b: SS) => boolean, ): SS { return reactReduxUseSelector(selector, equalityFn); } async function processDBStoreOperations( storeOperations: StoreOperations, ): Promise { const { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, integrityStoreOperations, communityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, threadActivityStoreOperations, } = storeOperations; const convertedThreadStoreOperations = threadStoreOpsHandlers.convertOpsToClientDBOps(threadStoreOperations); const convertedMessageStoreOperations = messageStoreOpsHandlers.convertOpsToClientDBOps(messageStoreOperations); const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); const convertedUserStoreOperations = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); const convertedKeyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations); const convertedCommunityStoreOperations = communityStoreOpsHandlers.convertOpsToClientDBOps(communityStoreOperations); const convertedSyncedMetadataStoreOperations = syncedMetadataStoreOpsHandlers.convertOpsToClientDBOps( syncedMetadataStoreOperations, ); const keyserversToRemoveFromNotifsStore = - getKeyserversToRemoveFromNotifsStore(keyserverStoreOperations); + getKeyserversToRemoveFromNotifsStore(keyserverStoreOperations ?? []); const convertedIntegrityStoreOperations = integrityStoreOpsHandlers.convertOpsToClientDBOps(integrityStoreOperations); const convertedAuxUserStoreOperations = auxUserStoreOpsHandlers.convertOpsToClientDBOps(auxUserStoreOperations); const convertedThreadActivityStoreOperations = threadActivityStoreOpsHandlers.convertOpsToClientDBOps( threadActivityStoreOperations, ); try { const promises = []; if (keyserversToRemoveFromNotifsStore.length > 0) { promises.push( commCoreModule.removeKeyserverDataFromNotifStorage( keyserversToRemoveFromNotifsStore, ), ); } const dbOps = { draftStoreOperations, threadStoreOperations: convertedThreadStoreOperations, messageStoreOperations: convertedMessageStoreOperations, reportStoreOperations: convertedReportStoreOperations, userStoreOperations: convertedUserStoreOperations, keyserverStoreOperations: convertedKeyserverStoreOperations, communityStoreOperations: convertedCommunityStoreOperations, integrityStoreOperations: convertedIntegrityStoreOperations, syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, auxUserStoreOperations: convertedAuxUserStoreOperations, threadActivityStoreOperations: convertedThreadActivityStoreOperations, }; - if (values(dbOps).some(ops => ops.length > 0)) { + if (values(dbOps).some(ops => ops && ops.length > 0)) { promises.push(commCoreModule.processDBStoreOperations(dbOps)); } await Promise.all(promises); } catch (e) { if (isTaskCancelledError(e)) { return; } // this code will make an entry in SecureStore and cause re-creating // database when user will open app again commCoreModule.reportDBOperationsFailure(); commCoreModule.terminate(); } } export { useSelector, processDBStoreOperations }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index 4bcf1c860..b56939740 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,563 +1,551 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteKeyserverAccountActionTypes, deleteAccountActionTypes, keyserverAuthActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { type ReplaceKeyserverOperation, keyserverStoreOpsHandlers, } from 'lib/ops/keyserver-store-ops.js'; import { type ReplaceThreadActivityEntryOperation, threadActivityStoreOpsHandlers, } from 'lib/ops/thread-activity-store-ops.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, identityInvalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/session-utils.js'; import type { AlertStore } from 'lib/types/alert-types.js'; import type { AuxUserStore } from 'lib/types/aux-user-types.js'; import type { CommunityStore } from 'lib/types/community-types.js'; import type { MessageSourceMetadata, DBOpsStore, } from 'lib/types/db-ops-types.js'; import type { DraftStore } from 'lib/types/draft-types.js'; import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore } from 'lib/types/entry-types.js'; import { type CalendarFilter } from 'lib/types/filter-types.js'; import type { IntegrityStore } from 'lib/types/integrity-types.js'; import type { KeyserverStore } from 'lib/types/keyserver-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { BaseAction } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import type { SyncedMetadataStore } from 'lib/types/synced-metadata-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateWindowActiveActionType, updateNavInfoActionType, updateWindowDimensionsActionType, setInitialReduxState, } from './action-types.js'; import { reduceCommunityPickerStore } from './community-picker-reducer.js'; import { defaultWebState } from './default-state.js'; import reduceNavInfo from './nav-reducer.js'; import { onStateDifference } from './redux-debug-utils.js'; import { reduceServicesAccessToken } from './services-access-token-reducer.js'; import { getVisibility } from './visibility.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; export type WindowDimensions = { width: number, height: number }; export type CommunityPickerStore = { +chat: ?string, +calendar: ?string, }; const nonUserSpecificFieldsWeb = [ 'loadingStatuses', 'windowDimensions', 'lifecycleState', 'windowActive', 'pushApiPublicKey', 'keyserverStore', 'initialStateLoaded', '_persist', 'customServer', ]; export type AppState = { +navInfo: WebNavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +communityPickerStore: CommunityPickerStore, +windowDimensions: WindowDimensions, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +windowActive: boolean, +userPolicies: UserPolicies, +pushApiPublicKey: ?string, +_persist: ?PersistState, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, }; export type Action = $ReadOnly< | BaseAction | { +messageSourceMetadata?: MessageSourceMetadata, ... | { +type: 'UPDATE_NAV_INFO', +payload: Partial } | { +type: 'UPDATE_WINDOW_DIMENSIONS', +payload: WindowDimensions, } | { +type: 'UPDATE_WINDOW_ACTIVE', +payload: boolean, } | { +type: 'SET_INITIAL_REDUX_STATE', +payload: InitialReduxStateActionPayload, }, }, >; function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; - let storeOperations: StoreOperations = { - draftStoreOperations: [], - threadStoreOperations: [], - messageStoreOperations: [], - reportStoreOperations: [], - userStoreOperations: [], - keyserverStoreOperations: [], - communityStoreOperations: [], - integrityStoreOperations: [], - syncedMetadataStoreOperations: [], - auxUserStoreOperations: [], - threadActivityStoreOperations: [], - }; + let storeOperations: StoreOperations = {}; if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if ( (action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( oldState, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if (action.type === setInitialReduxState) { const { userInfos, keyserverInfos, actualizedCalendarQuery, ...rest } = action.payload; const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverInfos) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], ...keyserverInfos[keyserverID], actualizedCalendarQuery, }, }, }); } let newState = { ...state, ...rest, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, replaceOperations, ), initialStateLoaded: true, }; if (userInfos) { newState = { ...newState, userStore: { userInfos } }; } return validateStateAndQueueOpsProcessing(action, oldState, newState, { ...storeOperations, keyserverStoreOperations: [ - ...storeOperations.keyserverStoreOperations, + ...(storeOperations.keyserverStoreOperations ?? []), ...replaceOperations, ], }); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowDimensions: action.payload, }, storeOperations, ); } else if (action.type === updateWindowActiveActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowActive: action.payload, }, storeOperations, ); } else if (action.type === setNewSessionActionType) { const { keyserverID, sessionChange } = action.payload; if (!state.keyserverStore.keyserverInfos[keyserverID]) { if (sessionChange.cookie?.startsWith('user=')) { console.log( 'received sessionChange with user cookie, ' + `but keyserver ${keyserverID} is not in KeyserverStore!`, ); } return state; } if ( invalidSessionDowngrade( oldState, sessionChange.currentUserInfo, action.payload.preRequestUserState, keyserverID, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } const replaceOperation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], sessionID: sessionChange.sessionID, }, }, }; state = { ...state, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, [replaceOperation], ), }; storeOperations = { ...storeOperations, keyserverStoreOperations: [ - ...storeOperations.keyserverStoreOperations, + ...(storeOperations.keyserverStoreOperations ?? []), replaceOperation, ], }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if (action.type !== updateNavInfoActionType) { // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo // the state won't be cleared and the current user info determined in // baseReducer will be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return // a different result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we // never keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultWebState, nonUserSpecificFieldsWeb, ); } const baseReducerResult = baseReducer(state, action, onStateDifference); state = baseReducerResult.state; storeOperations = { ...baseReducerResult.storeOperations, keyserverStoreOperations: [ - ...storeOperations.keyserverStoreOperations, - ...baseReducerResult.storeOperations.keyserverStoreOperations, + ...(storeOperations.keyserverStoreOperations ?? []), + ...(baseReducerResult.storeOperations.keyserverStoreOperations ?? []), ], }; } const communityPickerStore = reduceCommunityPickerStore( state.communityPickerStore, action, ); state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), communityPickerStore, commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), }; return validateStateAndQueueOpsProcessing( action, oldState, state, storeOperations, ); } function validateStateAndQueueOpsProcessing( action: Action, oldState: AppState, state: AppState, storeOperations: StoreOperations, ): AppState { const updateActiveThreadOps: ThreadStoreOperation[] = []; if ( (state.navInfo.activeChatThreadID && !state.navInfo.pendingThread && !state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) || (!state.navInfo.activeChatThreadID && isLoggedIn(state)) ) { // Makes sure the active thread always exists state = { ...state, navInfo: { ...state.navInfo, activeChatThreadID: mostRecentlyReadThreadSelector(state), }, }; } const activeThread = activeThreadSelector(state); if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && getVisibility().hidden() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but visibilityjs reports the window is not visible', ); } if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && typeof document !== 'undefined' && document && 'hasFocus' in document && !document.hasFocus() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but document.hasFocus() is false', ); } if ( activeThread && !getVisibility().hidden() && typeof document !== 'undefined' && document && 'hasFocus' in document && document.hasFocus() && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread ) { // Makes sure a currently focused thread is never unread const activeThreadInfo = state.threadStore.threadInfos[activeThread]; updateActiveThreadOps.push({ type: 'replace', payload: { id: activeThread, threadInfo: { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }, }, }); } const oldActiveThread = activeThreadSelector(oldState); if ( activeThread && oldActiveThread !== activeThread && state.messageStore.threads[activeThread] ) { const now = Date.now(); const replaceOperation: ReplaceThreadActivityEntryOperation = { type: 'replace_thread_activity_entry', payload: { id: activeThread, threadActivityStoreEntry: { ...state[activeThread], lastNavigatedTo: now, }, }, }; const threadActivityStore = threadActivityStoreOpsHandlers.processStoreOperations( state.threadActivityStore, [replaceOperation], ); state = { ...state, threadActivityStore, }; storeOperations = { ...storeOperations, threadActivityStoreOperations: [ - ...storeOperations.threadActivityStoreOperations, + ...(storeOperations.threadActivityStoreOperations ?? []), replaceOperation, ], }; } if (updateActiveThreadOps.length > 0) { state = { ...state, threadStore: threadStoreOpsHandlers.processStoreOperations( state.threadStore, updateActiveThreadOps, ), }; storeOperations = { ...storeOperations, threadStoreOperations: [ - ...storeOperations.threadStoreOperations, + ...(storeOperations.threadStoreOperations ?? []), ...updateActiveThreadOps, ], }; } // The operations were already dispatched from the main tab // For now the `dispatchSource` field is not included in any of the // redux actions and this causes flow to throw an error. // As soon as one of the actions is updated, this fix (and the corresponding // one in tab-synchronization.js) can be removed. // $FlowFixMe if (action.dispatchSource === 'tab-sync') { return state; } return { ...state, dbOpsStore: queueDBOps( state.dbOpsStore, action.messageSourceMetadata, storeOperations, ), }; } export { nonUserSpecificFieldsWeb, reducer }; diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js index 50cd3abf7..2d0b74a5a 100644 --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -1,244 +1,244 @@ // @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 { threadActivityStoreOpsHandlers } from 'lib/ops/thread-activity-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 { entries } from 'lib/utils/objects.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, threadActivityStore: 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, }; } if ( data?.store?.threadActivityEntries && data.store.threadActivityEntries.length > 0 ) { result = { ...result, threadActivityStore: threadActivityStoreOpsHandlers.translateClientDBData( data.store.threadActivityEntries, ), }; } return result; } async function processDBStoreOperations( storeOperations: StoreOperations, userID?: ?string, ): Promise { const { draftStoreOperations, threadStoreOperations, reportStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, userStoreOperations, messageStoreOperations, threadActivityStoreOperations, } = 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); const convertedThreadActivityStoreOperations = threadActivityStoreOpsHandlers.convertOpsToClientDBOps( threadActivityStoreOperations, ); if ( convertedThreadStoreOperations.length === 0 && convertedReportStoreOperations.length === 0 && - draftStoreOperations.length === 0 && + (!draftStoreOperations || draftStoreOperations.length === 0) && convertedKeyserverStoreOperations.length === 0 && convertedCommunityStoreOperations.length === 0 && convertedIntegrityStoreOperations.length === 0 && convertedSyncedMetadataStoreOperations.length === 0 && convertedAuxUserStoreOperations.length === 0 && convertedUserStoreOperations.length === 0 && convertedMessageStoreOperations.length === 0 && convertedThreadActivityStoreOperations.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, threadActivityStoreOperations: convertedThreadActivityStoreOperations, }, }); } catch (e) { console.log(e); if (canUseDatabase) { window.alert(e.message); if ( entries(storeOperations).some( ([key, ops]) => key !== 'draftStoreOperations' && key !== 'reportStoreOperations' && ops.length > 0, ) ) { await sharedWorker.init({ clearDatabase: true, markAsCorrupted: true }); location.reload(); } } } } export { getClientDBStore, processDBStoreOperations };